From bf38105fef59b702bb33eb46474cc4ddb375f22c Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Mon, 6 Mar 2017 20:18:38 -0800 Subject: [PATCH] (#362) Part 4: handle unknown attributes by expanding type codes. r=nalexander Also, don't run any SQL at all if an algebrized query is known to return no results. --- core/src/lib.rs | 23 ++++ query-algebrizer/src/cc.rs | 61 +++++++++- query-algebrizer/src/lib.rs | 7 ++ query-projector/src/lib.rs | 97 ++++++++++++--- query-sql/src/lib.rs | 182 ++++++++++++++++++++++++---- query-translator/src/lib.rs | 1 - query-translator/src/translate.rs | 89 ++++++++++++-- query-translator/tests/translate.rs | 109 +++++++++++++++-- src/query.rs | 6 + 9 files changed, 500 insertions(+), 75 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index 36637680..0756a371 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -74,11 +74,13 @@ impl TypedValue { &TypedValue::Keyword(_) => ValueType::Keyword, } } + } // Put this here rather than in `db` simply because it's widely needed. pub trait SQLValueType { fn value_type_tag(&self) -> i32; + fn accommodates_integer(&self, int: i64) -> bool; } impl SQLValueType for ValueType { @@ -94,6 +96,27 @@ impl SQLValueType for ValueType { ValueType::Keyword => 13, } } + + /// Returns true if the provided integer is in the SQLite value space of this type. For + /// example, `1` is how we encode `true`. + /// + /// ``` + /// use mentat_core::{ValueType, SQLValueType}; + /// assert!(ValueType::Boolean.accommodates_integer(1)); + /// assert!(!ValueType::Boolean.accommodates_integer(-1)); + /// assert!(!ValueType::Boolean.accommodates_integer(10)); + /// assert!(!ValueType::String.accommodates_integer(10)); + /// ``` + fn accommodates_integer(&self, int: i64) -> bool { + use ValueType::*; + match *self { + Instant | Long | Double => true, + Ref => int >= 0, + Boolean => (int == 0) || (int == 1), + ValueType::String => false, + Keyword => false, + } + } } #[test] diff --git a/query-algebrizer/src/cc.rs b/query-algebrizer/src/cc.rs index c169e70a..4863f923 100644 --- a/query-algebrizer/src/cc.rs +++ b/query-algebrizer/src/cc.rs @@ -134,23 +134,39 @@ pub fn default_table_aliaser() -> TableAliaser { #[derive(PartialEq, Eq)] pub enum ColumnConstraint { + EqualsColumn(QualifiedAlias, QualifiedAlias), EqualsEntity(QualifiedAlias, Entid), EqualsValue(QualifiedAlias, TypedValue), - EqualsColumn(QualifiedAlias, QualifiedAlias), + + // This is different: a numeric value can only apply to the 'v' column, and it implicitly + // constrains the `value_type_tag` column. For instance, a primitive long on `datoms00` of `5` + // cannot be a boolean, so `datoms00.value_type_tag` must be in the set `#{0, 4, 5}`. + // Note that `5 = 5.0` in SQLite, and we preserve that here. + EqualsPrimitiveLong(TableAlias, i64), + + HasType(TableAlias, ValueType), } impl Debug for ColumnConstraint { fn fmt(&self, f: &mut Formatter) -> Result { use self::ColumnConstraint::*; match self { + &EqualsColumn(ref qa1, ref qa2) => { + write!(f, "{:?} = {:?}", qa1, qa2) + } &EqualsEntity(ref qa, ref entid) => { write!(f, "{:?} = entity({:?})", qa, entid) } &EqualsValue(ref qa, ref typed_value) => { write!(f, "{:?} = value({:?})", qa, typed_value) } - &EqualsColumn(ref qa1, ref qa2) => { - write!(f, "{:?} = {:?}", qa1, qa2) + + &EqualsPrimitiveLong(ref qa, value) => { + write!(f, "{:?}.v = primitive({:?})", qa, value) + } + + &HasType(ref qa, value_type) => { + write!(f, "{:?}.value_type_tag = {:?}", qa, value_type) } } } @@ -181,7 +197,7 @@ impl Debug for ColumnConstraint { ///--------------------------------------------------------------------------------------- pub struct ConjoiningClauses { /// `true` if this set of clauses cannot yield results in the context of the current schema. - is_known_empty: bool, + pub is_known_empty: bool, /// A function used to generate an alias for a table -- e.g., from "datoms" to "datoms123". aliaser: TableAliaser, @@ -264,6 +280,10 @@ impl ConjoiningClauses { self.constrain_column_to_entity(table, DatomsColumn::Attribute, attribute) } + pub fn constrain_value_to_numeric(&mut self, table: TableAlias, value: i64) { + self.wheres.push(ColumnConstraint::EqualsPrimitiveLong(table, value)) + } + /// Constrains the var if there's no existing type. /// Returns `false` if it's impossible for this type to apply (because there's a conflicting /// type already known). @@ -601,7 +621,18 @@ impl ConjoiningClauses { if let Some(ValueType::Ref) = value_type { self.constrain_column_to_entity(col.clone(), DatomsColumn::Value, i); } else { - unimplemented!(); + // If we have a pattern like: + // + // `[123 ?a 1]` + // + // then `1` could be an entid (ref), a long, a boolean, or an instant. + // + // We represent these constraints during execution: + // + // - Constraining the value column to the plain numeric value '1'. + // - Constraining its type column to one of a set of types. + // + self.constrain_value_to_numeric(col.clone(), i); }, PatternValuePlace::IdentOrKeyword(ref kw) => { // If we know the valueType, then we can determine whether this is an ident or a @@ -637,7 +668,26 @@ impl ConjoiningClauses { // TODO: if we don't know the type of the attribute because we don't know the // attribute, we can actually work backwards to the set of appropriate attributes // from the type of the value itself! #292. + let typed_value_type = typed_value.value_type(); self.constrain_column_to_constant(col.clone(), DatomsColumn::Value, typed_value); + + // If we can't already determine the range of values in the DB from the attribute, + // then we must also constrain the type tag. + // + // Input values might be: + // + // - A long. This is handled by EntidOrInteger. + // - A boolean. This is unambiguous. + // - A double. This is currently unambiguous, though note that SQLite will equate 5.0 with 5. + // - A string. This is unambiguous. + // - A keyword. This is unambiguous. + // + // Because everything we handle here is unambiguous, we generate a single type + // restriction from the value type of the typed value. + if value_type.is_none() { + self.wheres.push(ColumnConstraint::HasType(col.clone(), typed_value_type)); + } + }, } @@ -789,6 +839,7 @@ mod testing { // TODO: implement expand_type_tags. assert_eq!(cc.wheres, vec![ ColumnConstraint::EqualsValue(d0_v, TypedValue::Boolean(true)), + ColumnConstraint::HasType("datoms00".to_string(), ValueType::Boolean), ]); } diff --git a/query-algebrizer/src/lib.rs b/query-algebrizer/src/lib.rs index 57158301..f0189e64 100644 --- a/query-algebrizer/src/lib.rs +++ b/query-algebrizer/src/lib.rs @@ -34,6 +34,9 @@ pub struct AlgebraicQuery { } impl AlgebraicQuery { + /** + * Apply a new limit to this query, if one is provided and any existing limit is larger. + */ pub fn apply_limit(&mut self, limit: Option) { match self.limit { None => self.limit = limit, @@ -47,6 +50,10 @@ impl AlgebraicQuery { }, }; } + + pub fn is_known_empty(&self) -> bool { + self.cc.is_known_empty + } } #[allow(dead_code)] diff --git a/query-projector/src/lib.rs b/query-projector/src/lib.rs index 061bdff1..5ebbfe1a 100644 --- a/query-projector/src/lib.rs +++ b/query-projector/src/lib.rs @@ -100,6 +100,36 @@ impl QueryResults { &Rel(ref v) => v.len(), } } + + pub fn is_empty(&self) -> bool { + use QueryResults::*; + match self { + &Scalar(ref o) => o.is_none(), + &Tuple(ref o) => o.is_none(), + &Coll(ref v) => v.is_empty(), + &Rel(ref v) => v.is_empty(), + } + } + + pub fn empty(spec: &FindSpec) -> QueryResults { + use self::FindSpec::*; + match spec { + &FindScalar(_) => QueryResults::Scalar(None), + &FindTuple(_) => QueryResults::Tuple(None), + &FindColl(_) => QueryResults::Coll(vec![]), + &FindRel(_) => QueryResults::Rel(vec![]), + } + } + + pub fn empty_factory(spec: &FindSpec) -> Box QueryResults> { + use self::FindSpec::*; + match spec { + &FindScalar(_) => Box::new(|| QueryResults::Scalar(None)), + &FindTuple(_) => Box::new(|| QueryResults::Tuple(None)), + &FindColl(_) => Box::new(|| QueryResults::Coll(vec![])), + &FindRel(_) => Box::new(|| QueryResults::Rel(vec![])), + } + } } type Index = i32; // See rusqlite::RowIndex. @@ -216,6 +246,24 @@ pub trait Projector { fn project<'stmt>(&self, rows: Rows<'stmt>) -> Result; } +/// A projector that produces a `QueryResult` containing fixed data. +/// Takes a boxed function that should return an empty result set of the desired type. +struct ConstantProjector { + results_factory: Box QueryResults>, +} + +impl ConstantProjector { + fn new(results_factory: Box QueryResults>) -> ConstantProjector { + ConstantProjector { results_factory: results_factory } + } +} + +impl Projector for ConstantProjector { + fn project<'stmt>(&self, _: Rows<'stmt>) -> Result { + Ok((self.results_factory)()) + } +} + struct ScalarProjector { template: TypedIndex, } @@ -396,28 +444,39 @@ pub struct CombinedProjection { pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection { use self::FindSpec::*; - match query.find_spec { - FindColl(ref element) => { - let (cols, templates) = project_elements(1, iter::once(element), query); - CollProjector::combine(cols, templates) - }, + if query.is_known_empty() { + // Do a few gyrations to produce empty results of the right kind for the query. + let empty = QueryResults::empty_factory(&query.find_spec); + let constant_projector = ConstantProjector::new(empty); + CombinedProjection { + sql_projection: Projection::One, - FindScalar(ref element) => { - let (cols, templates) = project_elements(1, iter::once(element), query); - ScalarProjector::combine(cols, templates) - }, + datalog_projector: Box::new(constant_projector), + } + } else { + match query.find_spec { + FindColl(ref element) => { + let (cols, templates) = project_elements(1, iter::once(element), query); + CollProjector::combine(cols, templates) + }, - FindRel(ref elements) => { - let column_count = query.find_spec.expected_column_count(); - let (cols, templates) = project_elements(column_count, elements, query); - RelProjector::combine(column_count, cols, templates) - }, + FindScalar(ref element) => { + let (cols, templates) = project_elements(1, iter::once(element), query); + ScalarProjector::combine(cols, templates) + }, - FindTuple(ref elements) => { - let column_count = query.find_spec.expected_column_count(); - let (cols, templates) = project_elements(column_count, elements, query); - TupleProjector::combine(column_count, cols, templates) - }, + FindRel(ref elements) => { + let column_count = query.find_spec.expected_column_count(); + let (cols, templates) = project_elements(column_count, elements, query); + RelProjector::combine(column_count, cols, templates) + }, + + FindTuple(ref elements) => { + let column_count = query.find_spec.expected_column_count(); + let (cols, templates) = project_elements(column_count, elements, query); + TupleProjector::combine(column_count, cols, templates) + }, + } } } diff --git a/query-sql/src/lib.rs b/query-sql/src/lib.rs index 85bf9542..eba64de0 100644 --- a/query-sql/src/lib.rs +++ b/query-sql/src/lib.rs @@ -44,6 +44,7 @@ use mentat_sql::{ pub enum ColumnOrExpression { Column(QualifiedAlias), Entid(Entid), // Because it's so common. + Integer(i32), // We use these for type codes etc. Value(TypedValue), } @@ -64,11 +65,26 @@ pub enum Constraint { Infix { op: Op, left: ColumnOrExpression, - right: ColumnOrExpression + right: ColumnOrExpression, + }, + And { + constraints: Vec, + }, + In { + left: ColumnOrExpression, + list: Vec, } } impl Constraint { + pub fn not_equal(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint { + Constraint::Infix { + op: Op("<>"), // ANSI SQL for future-proofing! + left: left, + right: right, + } + } + pub fn equal(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint { Constraint::Infix { op: Op("="), @@ -86,6 +102,12 @@ enum JoinOp { // Short-hand for a list of tables all inner-joined. pub struct TableList(pub Vec); +impl TableList { + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + pub struct Join { left: TableOrSubquery, op: JoinOp, @@ -102,6 +124,7 @@ enum TableOrSubquery { pub enum FromClause { TableList(TableList), // Short-hand for a pile of inner joins. Join(Join), + Nothing, } pub struct SelectQuery { @@ -119,6 +142,35 @@ fn push_column(qb: &mut QueryBuilder, col: &DatomsColumn) { //--------------------------------------------------------- // Turn that representation into SQL. + +/// A helper macro to sequentially process an iterable sequence, +/// evaluating a block between each pair of items. +/// +/// This is used to simply and efficiently produce output like +/// +/// ```sql +/// 1, 2, 3 +/// ``` +/// +/// or +/// +/// ```sql +/// x = 1 AND y = 2 +/// ``` +/// +/// without producing an intermediate string sequence. +macro_rules! interpose { + ( $name: ident, $across: expr, $body: block, $inter: block ) => { + let mut seq = $across.iter(); + if let Some($name) = seq.next() { + $body; + for $name in seq { + $inter; + $body; + } + } + } +} impl QueryFragment for ColumnOrExpression { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { use self::ColumnOrExpression::*; @@ -133,6 +185,10 @@ impl QueryFragment for ColumnOrExpression { out.push_sql(entid.to_string().as_str()); Ok(()) }, + &Integer(integer) => { + out.push_sql(integer.to_string().as_str()); + Ok(()) + }, &Value(ref v) => { out.push_typed_value(v) }, @@ -172,19 +228,6 @@ impl QueryFragment for Op { } } -macro_rules! interpose { - ( $name: ident, $across: expr, $body: block, $inter: block ) => { - let mut seq = $across.iter(); - if let Some($name) = seq.next() { - $body; - for $name in seq { - $inter; - $body; - } - } - } -} - impl QueryFragment for Constraint { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { use self::Constraint::*; @@ -195,7 +238,26 @@ impl QueryFragment for Constraint { op.push_sql(out)?; out.push_sql(" "); right.push_sql(out) - } + }, + + &And { ref constraints } => { + out.push_sql("("); + interpose!(constraint, constraints, + { constraint.push_sql(out)? }, + { out.push_sql(" AND ") }); + out.push_sql(")"); + Ok(()) + }, + + &In { ref left, ref list } => { + left.push_sql(out)?; + out.push_sql(" IN ("); + interpose!(item, list, + { item.push_sql(out)? }, + { out.push_sql(", ") }); + out.push_sql(")"); + Ok(()) + }, } } } @@ -249,8 +311,19 @@ impl QueryFragment for FromClause { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { use self::FromClause::*; match self { - &TableList(ref table_list) => table_list.push_sql(out), - &Join(ref join) => join.push_sql(out), + &TableList(ref table_list) => { + if table_list.is_empty() { + Ok(()) + } else { + out.push_sql(" FROM "); + table_list.push_sql(out) + } + }, + &Join(ref join) => { + out.push_sql(" FROM "); + join.push_sql(out) + }, + &Nothing => Ok(()), } } } @@ -259,19 +332,15 @@ impl QueryFragment for SelectQuery { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { out.push_sql("SELECT "); self.projection.push_sql(out)?; - - out.push_sql(" FROM "); self.from.push_sql(out)?; - if self.constraints.is_empty() { - return Ok(()); + if !self.constraints.is_empty() { + out.push_sql(" WHERE "); + interpose!(constraint, self.constraints, + { constraint.push_sql(out)? }, + { out.push_sql(" AND ") }); } - out.push_sql(" WHERE "); - interpose!(constraint, self.constraints, - { constraint.push_sql(out)? }, - { out.push_sql(" AND ") }); - // Guaranteed to be positive: u64. if let Some(limit) = self.limit { out.push_sql(" LIMIT "); @@ -294,6 +363,67 @@ mod tests { use super::*; use mentat_query_algebrizer::DatomsTable; + fn build_constraint(c: Constraint) -> String { + let mut builder = SQLiteQueryBuilder::new(); + c.push_sql(&mut builder) + .map(|_| builder.finish()) + .unwrap().sql + } + + #[test] + fn test_in_constraint() { + let none = Constraint::In { + left: ColumnOrExpression::Column(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value)), + list: vec![], + }; + + let one = Constraint::In { + left: ColumnOrExpression::Column(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value)), + list: vec![ + ColumnOrExpression::Entid(123), + ], + }; + + let three = Constraint::In { + left: ColumnOrExpression::Column(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value)), + list: vec![ + ColumnOrExpression::Entid(123), + ColumnOrExpression::Entid(456), + ColumnOrExpression::Entid(789), + ], + }; + + assert_eq!("`datoms01`.v IN ()", build_constraint(none)); + assert_eq!("`datoms01`.v IN (123)", build_constraint(one)); + assert_eq!("`datoms01`.v IN (123, 456, 789)", build_constraint(three)); + } + + #[test] + fn test_and_constraint() { + let c = Constraint::And { + constraints: vec![ + Constraint::And { + constraints: vec![ + Constraint::Infix { + op: Op("="), + left: ColumnOrExpression::Entid(123), + right: ColumnOrExpression::Entid(456), + }, + Constraint::Infix { + op: Op("="), + left: ColumnOrExpression::Entid(789), + right: ColumnOrExpression::Entid(246), + }, + ], + }, + ], + }; + + // Two sets of parens: the outermost AND only has one child, + // but still contributes parens. + assert_eq!("((123 = 456 AND 789 = 246))", build_constraint(c)); + } + #[test] fn test_end_to_end() { diff --git a/query-translator/src/lib.rs b/query-translator/src/lib.rs index e78302ac..30a8395c 100644 --- a/query-translator/src/lib.rs +++ b/query-translator/src/lib.rs @@ -23,6 +23,5 @@ pub use mentat_query_sql::{ pub use translate::{ cc_to_exists, - cc_to_select, query_to_select, }; diff --git a/query-translator/src/translate.rs b/query-translator/src/translate.rs index 0f947656..f85422fd 100644 --- a/query-translator/src/translate.rs +++ b/query-translator/src/translate.rs @@ -10,6 +10,12 @@ #![allow(dead_code, unused_imports)] +use mentat_core::{ + SQLValueType, + TypedValue, + ValueType, +}; + use mentat_query::{ Element, FindSpec, @@ -64,45 +70,106 @@ impl ToConstraint for ColumnConstraint { match self { EqualsEntity(qa, entid) => Constraint::equal(qa.to_column(), ColumnOrExpression::Entid(entid)), + EqualsValue(qa, tv) => Constraint::equal(qa.to_column(), ColumnOrExpression::Value(tv)), + EqualsColumn(left, right) => Constraint::equal(left.to_column(), right.to_column()), + + EqualsPrimitiveLong(table, value) => { + let value_column = QualifiedAlias(table.clone(), DatomsColumn::Value).to_column(); + let tag_column = QualifiedAlias(table, DatomsColumn::ValueTypeTag).to_column(); + + /// A bare long in a query might match a ref, an instant, a long (obviously), or a + /// double. If it's negative, it can't match a ref, but that's OK -- it won't! + /// + /// However, '1' and '0' are used to represent booleans, and some integers are also + /// used to represent FTS values. We don't want to accidentally match those. + /// + /// We ask `SQLValueType` whether this value is in range for how booleans are + /// represented in the database. + /// + /// We only hit this code path when the attribute is unknown, so we're querying + /// `all_datoms`. That means we don't see FTS IDs at all -- they're transparently + /// replaced by their strings. If that changes, then you should also exclude the + /// string type code (10) here. + let must_exclude_boolean = ValueType::Boolean.accommodates_integer(value); + if must_exclude_boolean { + Constraint::And { + constraints: vec![ + Constraint::equal(value_column, + ColumnOrExpression::Value(TypedValue::Long(value))), + Constraint::not_equal(tag_column, + ColumnOrExpression::Integer(ValueType::Boolean.value_type_tag())), + ], + } + } else { + Constraint::equal(value_column, ColumnOrExpression::Value(TypedValue::Long(value))) + } + }, + + HasType(table, value_type) => { + let column = QualifiedAlias(table, DatomsColumn::ValueTypeTag).to_column(); + Constraint::equal(column, ColumnOrExpression::Integer(value_type.value_type_tag())) + }, } } } -pub struct CombinedSelectQuery { +pub struct ProjectedSelect{ pub query: SelectQuery, pub projector: Box, } +/// Returns a `SelectQuery` that queries for the provided `cc`. Note that this _always_ returns a +/// query that runs SQL. The next level up the call stack can check for known-empty queries if +/// needed. fn cc_to_select_query>>(projection: Projection, cc: ConjoiningClauses, limit: T) -> SelectQuery { + let from = if cc.from.is_empty() { + FromClause::Nothing + } else { + FromClause::TableList(TableList(cc.from)) + }; SelectQuery { projection: projection, - from: FromClause::TableList(TableList(cc.from)), + from: from, constraints: cc.wheres .into_iter() .map(|c| c.to_constraint()) .collect(), - limit: limit.into(), + limit: if cc.is_known_empty { Some(0) } else { limit.into() }, + } +} + +/// Return a query that projects `1` if the `cc` matches the store, and returns no results +/// if it doesn't. +pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery { + if cc.is_known_empty { + // In this case we can produce a very simple query that returns no results. + SelectQuery { + projection: Projection::One, + from: FromClause::Nothing, + constraints: vec![], + limit: Some(0), + } + } else { + cc_to_select_query(Projection::One, cc, 1) } } /// Consume a provided `ConjoiningClauses` to yield a new -/// `SelectQuery`. A projection list must also be provided. -pub fn cc_to_select(projection: CombinedProjection, cc: ConjoiningClauses, limit: Option) -> CombinedSelectQuery { +/// `ProjectedSelect`. A projection list must also be provided. +fn cc_to_select(projection: CombinedProjection, cc: ConjoiningClauses, limit: Option) -> ProjectedSelect { let CombinedProjection { sql_projection, datalog_projector } = projection; - CombinedSelectQuery { + ProjectedSelect { query: cc_to_select_query(sql_projection, cc, limit), projector: datalog_projector, } } -pub fn query_to_select(query: AlgebraicQuery) -> CombinedSelectQuery { +/// Consume a provided `AlgebraicQuery` to yield a new +/// `ProjectedSelect`. +pub fn query_to_select(query: AlgebraicQuery) -> ProjectedSelect { cc_to_select(query_projection(&query), query.cc, query.limit) } - -pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery { - cc_to_select_query(Projection::One, cc, 1) -} diff --git a/query-translator/tests/translate.rs b/query-translator/tests/translate.rs index 21442afe..1f58ddfe 100644 --- a/query-translator/tests/translate.rs +++ b/query-translator/tests/translate.rs @@ -41,6 +41,14 @@ fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) { schema.schema_map.insert(e, a); } +fn translate>>(schema: &Schema, input: &'static str, limit: T) -> SQLQuery { + let parsed = parse_find_string(input).expect("parse failed"); + let mut algebrized = algebrize(schema, parsed); + algebrized.apply_limit(limit.into()); + let select = query_to_select(algebrized); + select.query.to_sql_query().unwrap() +} + #[test] fn test_coll() { let mut schema = Schema::default(); @@ -51,10 +59,7 @@ fn test_coll() { }); let input = r#"[:find [?x ...] :where [?x :foo/bar "yyy"]]"#; - let parsed = parse_find_string(input).expect("parse failed"); - let algebrized = algebrize(&schema, parsed); - let select = query_to_select(algebrized); - let SQLQuery { sql, args } = select.query.to_sql_query().unwrap(); + 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"); assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]); } @@ -69,10 +74,7 @@ fn test_rel() { }); let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#; - let parsed = parse_find_string(input).expect("parse failed"); - let algebrized = algebrize(&schema, parsed); - let select = query_to_select(algebrized); - let SQLQuery { sql, args } = select.query.to_sql_query().unwrap(); + 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"); assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]); } @@ -87,11 +89,92 @@ fn test_limit() { }); let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#; - let parsed = parse_find_string(input).expect("parse failed"); - let mut algebrized = algebrize(&schema, parsed); - algebrized.limit = Some(5); - let select = query_to_select(algebrized); - let SQLQuery { sql, args } = select.query.to_sql_query().unwrap(); + let SQLQuery { sql, args } = translate(&schema, input, 5); assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 5"); assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]); } + +#[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 `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = $v0 AND `datoms00`.value_type_tag = 13"); + assert_eq!(args, vec![("$v0".to_string(), ":ab/yyy".to_string())]); +} + +#[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 `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![("$v0".to_string(), "horses".to_string())]); +} + +#[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 `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 `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 `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 `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 `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); + + // 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); +} diff --git a/src/query.rs b/src/query.rs index 7b511ca1..1b1d0c90 100644 --- a/src/query.rs +++ b/src/query.rs @@ -66,6 +66,12 @@ pub fn q_once<'sqlite, 'schema, 'query, T, U> let parsed = parse_find_string(query)?; let mut algebrized = algebrize(schema, parsed); + + if algebrized.is_known_empty() { + // We don't need to do any SQL work at all. + return Ok(QueryResults::empty(&algebrized.find_spec)); + } + algebrized.apply_limit(limit.into()); let select = query_to_select(algebrized);