mentat/query-translator/tests/translate.rs
Richard Newman d8075aa07d Part 3: finish expansion and translation of complex or.
This commit turns complex `or` -- `or`s in which not all variables are
unified, or in which not all arms are the same shape -- into a
computed table.

We do this by building a template CC that shares some state with the
destination CC, applying each arm of the `or` to a copy of the template
as if it were a standalone query, then building a projection list and
creating a `ComputedTable::Union`. This is pushed into the destination
CC's `computed_tables` list.

Finally, the variables projected from the UNION are bound in the
destination CC, so that unification occurs, and projection of the
outermost query can use bindings established by the `or-join`.

This commit includes projection of type codes from heterogeneous `UNION`
arms: we compute a list of variables for which a definite type is
unknown in at least one arm, and force all arms to project either a type
tag column or a fixed type. It's important that each branch of a UNION
project the same columns in the same order, hence the projection of
fixed values.

The translator is similarly extended to project the type tag column name
or the known value_type_tag to support this.

Review comment: clarify union type extraction.
2017-04-12 19:21:45 -07:00

356 lines
14 KiB
Rust

// Copyright 2016 Mozilla
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
// this file except in compliance with the License. You may obtain a copy of the
// License at http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
extern crate mentat_core;
extern crate mentat_query;
extern crate mentat_query_algebrizer;
extern crate mentat_query_parser;
extern crate mentat_query_translator;
extern crate mentat_sql;
use std::rc::Rc;
use mentat_query::NamespacedKeyword;
use mentat_core::{
Attribute,
Entid,
Schema,
ValueType,
};
use mentat_query_parser::parse_find_string;
use mentat_query_algebrizer::algebrize;
use mentat_query_translator::{
query_to_select,
};
use mentat_sql::SQLQuery;
fn associate_ident(schema: &mut Schema, i: NamespacedKeyword, e: Entid) {
schema.entid_map.insert(e, i.clone());
schema.ident_map.insert(i.clone(), e);
}
fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
schema.schema_map.insert(e, a);
}
fn translate<T: Into<Option<u64>>>(schema: &Schema, input: &'static str, limit: T) -> SQLQuery {
let parsed = parse_find_string(input).expect("parse failed");
let mut algebrized = algebrize(schema, parsed).expect("algebrize failed");
algebrized.apply_limit(limit.into());
let select = query_to_select(algebrized);
select.query.to_sql_query().unwrap()
}
fn prepopulated_typed_schema(foo_type: ValueType) -> Schema {
let mut schema = Schema::default();
associate_ident(&mut schema, NamespacedKeyword::new("foo", "bar"), 99);
add_attribute(&mut schema, 99, Attribute {
value_type: foo_type,
..Default::default()
});
schema
}
fn prepopulated_schema() -> Schema {
prepopulated_typed_schema(ValueType::String)
}
fn make_arg(name: &'static str, value: &'static str) -> (String, Rc<String>) {
(name.to_string(), Rc::new(value.to_string()))
}
#[test]
fn test_scalar() {
let schema = prepopulated_schema();
let input = r#"[:find ?x . :where [?x :foo/bar "yyy"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 1");
assert_eq!(args, vec![make_arg("$v0", "yyy")]);
}
#[test]
fn test_tuple() {
let schema = prepopulated_schema();
let input = r#"[:find [?x] :where [?x :foo/bar "yyy"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 1");
assert_eq!(args, vec![make_arg("$v0", "yyy")]);
}
#[test]
fn test_coll() {
let schema = prepopulated_schema();
let input = r#"[:find [?x ...] :where [?x :foo/bar "yyy"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0");
assert_eq!(args, vec![make_arg("$v0", "yyy")]);
}
#[test]
fn test_rel() {
let schema = prepopulated_schema();
let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0");
assert_eq!(args, vec![make_arg("$v0", "yyy")]);
}
#[test]
fn test_limit() {
let schema = prepopulated_schema();
let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, 5);
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 5");
assert_eq!(args, vec![make_arg("$v0", "yyy")]);
}
#[test]
fn test_unknown_attribute_keyword_value() {
let schema = Schema::default();
let input = r#"[:find ?x :where [?x _ :ab/yyy]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
// Only match keywords, not strings: tag = 13.
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = $v0 AND `datoms00`.value_type_tag = 13");
assert_eq!(args, vec![make_arg("$v0", ":ab/yyy")]);
}
#[test]
fn test_unknown_attribute_string_value() {
let schema = Schema::default();
let input = r#"[:find ?x :where [?x _ "horses"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
// We expect all_datoms because we're querying for a string. Magic, that.
// We don't want keywords etc., so tag = 10.
assert_eq!(sql, "SELECT DISTINCT `all_datoms00`.e AS `?x` FROM `all_datoms` AS `all_datoms00` WHERE `all_datoms00`.v = $v0 AND `all_datoms00`.value_type_tag = 10");
assert_eq!(args, vec![make_arg("$v0", "horses")]);
}
#[test]
fn test_unknown_attribute_double_value() {
let schema = Schema::default();
let input = r#"[:find ?x :where [?x _ 9.95]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
// In general, doubles _could_ be 1.0, which might match a boolean or a ref. Set tag = 5 to
// make sure we only match numbers.
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = 9.95 AND `datoms00`.value_type_tag = 5");
assert_eq!(args, vec![]);
}
#[test]
fn test_unknown_attribute_integer_value() {
let schema = Schema::default();
let negative = r#"[:find ?x :where [?x _ -1]]"#;
let zero = r#"[:find ?x :where [?x _ 0]]"#;
let one = r#"[:find ?x :where [?x _ 1]]"#;
let two = r#"[:find ?x :where [?x _ 2]]"#;
// Can't match boolean; no need to filter it out.
let SQLQuery { sql, args } = translate(&schema, negative, None);
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = -1");
assert_eq!(args, vec![]);
// Excludes booleans.
let SQLQuery { sql, args } = translate(&schema, zero, None);
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE (`datoms00`.v = 0 AND `datoms00`.value_type_tag <> 1)");
assert_eq!(args, vec![]);
// Excludes booleans.
let SQLQuery { sql, args } = translate(&schema, one, None);
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE (`datoms00`.v = 1 AND `datoms00`.value_type_tag <> 1)");
assert_eq!(args, vec![]);
// Can't match boolean; no need to filter it out.
let SQLQuery { sql, args } = translate(&schema, two, None);
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = 2");
assert_eq!(args, vec![]);
}
#[test]
fn test_unknown_ident() {
let schema = Schema::default();
let impossible = r#"[:find ?x :where [?x :db/ident :no/exist]]"#;
let parsed = parse_find_string(impossible).expect("parse failed");
let algebrized = algebrize(&schema, parsed).expect("algebrize failed");
// This query cannot return results: the ident doesn't resolve for a ref-typed attribute.
assert!(algebrized.is_known_empty());
// If you insist…
let select = query_to_select(algebrized);
let sql = select.query.to_sql_query().unwrap().sql;
assert_eq!("SELECT 1 LIMIT 0", sql);
}
#[test]
fn test_numeric_less_than_unknown_attribute() {
let schema = Schema::default();
let input = r#"[:find ?x :where [?x _ ?y] [(< ?y 10)]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
// Although we infer numericness from numeric predicates, we've already assigned a table to the
// first pattern, and so this is _still_ `all_datoms`.
assert_eq!(sql, "SELECT DISTINCT `all_datoms00`.e AS `?x` FROM `all_datoms` AS `all_datoms00` WHERE `all_datoms00`.v < 10");
assert_eq!(args, vec![]);
}
#[test]
fn test_numeric_gte_known_attribute() {
let schema = prepopulated_typed_schema(ValueType::Double);
let input = r#"[:find ?x :where [?x :foo/bar ?y] [(>= ?y 12.9)]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v >= 12.9");
assert_eq!(args, vec![]);
}
#[test]
fn test_numeric_not_equals_known_attribute() {
let schema = prepopulated_typed_schema(ValueType::Long);
let input = r#"[:find ?x . :where [?x :foo/bar ?y] [(!= ?y 12)]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v <> 12 LIMIT 1");
assert_eq!(args, vec![]);
}
#[test]
fn test_simple_or_join() {
let mut schema = Schema::default();
associate_ident(&mut schema, NamespacedKeyword::new("page", "url"), 97);
associate_ident(&mut schema, NamespacedKeyword::new("page", "title"), 98);
associate_ident(&mut schema, NamespacedKeyword::new("page", "description"), 99);
for x in 97..100 {
add_attribute(&mut schema, x, Attribute {
value_type: ValueType::String,
..Default::default()
});
}
let input = r#"[:find [?url ?description]
:where
(or-join [?page]
[?page :page/url "http://foo.com/"]
[?page :page/title "Foo"])
[?page :page/url ?url]
[?page :page/description ?description]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
assert_eq!(sql, "SELECT `datoms01`.v AS `?url`, `datoms02`.v AS `?description` FROM `datoms` AS `datoms00`, `datoms` AS `datoms01`, `datoms` AS `datoms02` WHERE ((`datoms00`.a = 97 AND `datoms00`.v = $v0) OR (`datoms00`.a = 98 AND `datoms00`.v = $v1)) AND `datoms01`.a = 97 AND `datoms02`.a = 99 AND `datoms00`.e = `datoms01`.e AND `datoms00`.e = `datoms02`.e LIMIT 1");
assert_eq!(args, vec![make_arg("$v0", "http://foo.com/"), make_arg("$v1", "Foo")]);
}
#[test]
fn test_complex_or_join() {
let mut schema = Schema::default();
associate_ident(&mut schema, NamespacedKeyword::new("page", "save"), 95);
add_attribute(&mut schema, 95, Attribute {
value_type: ValueType::Ref,
..Default::default()
});
associate_ident(&mut schema, NamespacedKeyword::new("save", "title"), 96);
associate_ident(&mut schema, NamespacedKeyword::new("page", "url"), 97);
associate_ident(&mut schema, NamespacedKeyword::new("page", "title"), 98);
associate_ident(&mut schema, NamespacedKeyword::new("page", "description"), 99);
for x in 96..100 {
add_attribute(&mut schema, x, Attribute {
value_type: ValueType::String,
..Default::default()
});
}
let input = r#"[:find [?url ?description]
:where
(or-join [?page]
[?page :page/url "http://foo.com/"]
[?page :page/title "Foo"]
(and
[?page :page/save ?save]
[?save :save/title "Foo"]))
[?page :page/url ?url]
[?page :page/description ?description]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
assert_eq!(sql, "SELECT `datoms04`.v AS `?url`, \
`datoms05`.v AS `?description` \
FROM (SELECT `datoms00`.e AS `?page` \
FROM `datoms` AS `datoms00` \
WHERE `datoms00`.a = 97 \
AND `datoms00`.v = $v0 \
UNION \
SELECT `datoms01`.e AS `?page` \
FROM `datoms` AS `datoms01` \
WHERE `datoms01`.a = 98 \
AND `datoms01`.v = $v1 \
UNION \
SELECT `datoms02`.e AS `?page` \
FROM `datoms` AS `datoms02`, \
`datoms` AS `datoms03` \
WHERE `datoms02`.a = 95 \
AND `datoms03`.a = 96 \
AND `datoms03`.v = $v2 \
AND `datoms02`.v = `datoms03`.e) AS `c00`, \
`datoms` AS `datoms04`, \
`datoms` AS `datoms05` \
WHERE `datoms04`.a = 97 \
AND `datoms05`.a = 99 \
AND `c00`.`?page` = `datoms04`.e \
AND `c00`.`?page` = `datoms05`.e \
LIMIT 1");
assert_eq!(args, vec![make_arg("$v0", "http://foo.com/"),
make_arg("$v1", "Foo"),
make_arg("$v2", "Foo")]);
}
#[test]
fn test_complex_or_join_type_projection() {
let mut schema = Schema::default();
associate_ident(&mut schema, NamespacedKeyword::new("page", "title"), 98);
add_attribute(&mut schema, 98, Attribute {
value_type: ValueType::String,
..Default::default()
});
let input = r#"[:find [?y]
:where
(or
[6 :page/title ?y]
[5 _ ?y])]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
assert_eq!(sql, "SELECT `c00`.`?y` AS `?y`, \
`c00`.`?y_value_type_tag` AS `?y_value_type_tag` \
FROM (SELECT `datoms00`.v AS `?y`, \
10 AS `?y_value_type_tag` \
FROM `datoms` AS `datoms00` \
WHERE `datoms00`.e = 6 \
AND `datoms00`.a = 98 \
UNION \
SELECT `all_datoms01`.v AS `?y`, \
`all_datoms01`.value_type_tag AS `?y_value_type_tag` \
FROM `all_datoms` AS `all_datoms01` \
WHERE `all_datoms01`.e = 5) AS `c00` \
LIMIT 1");
assert_eq!(args, vec![]);
}