From 565a0e9ff93a189df0fafddd4be42c4156efc3b1 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Mon, 12 Jun 2017 14:24:56 -0700 Subject: [PATCH] Implement MATCHES throughout SQL machinery. --- query-algebrizer/src/clauses/mod.rs | 8 +++++ query-algebrizer/src/errors.rs | 8 +++++ query-algebrizer/src/lib.rs | 1 + query-algebrizer/src/types.rs | 42 +++++++++++++++++++++++++++ query-sql/src/lib.rs | 45 +++++++++++++++++++++++++++-- query-translator/src/translate.rs | 8 +++++ query-translator/tests/translate.rs | 7 +++++ 7 files changed, 116 insertions(+), 3 deletions(-) diff --git a/query-algebrizer/src/clauses/mod.rs b/query-algebrizer/src/clauses/mod.rs index 22d7b455..d20d3b0e 100644 --- a/query-algebrizer/src/clauses/mod.rs +++ b/query-algebrizer/src/clauses/mod.rs @@ -54,6 +54,7 @@ use types::{ DatomsColumn, DatomsTable, EmptyBecause, + FulltextColumn, QualifiedAlias, QueryValue, SourceAlias, @@ -398,6 +399,13 @@ impl ConjoiningClauses { self.constrain_column_to_constant(table, column, bound_val); }, + Column::Fulltext(FulltextColumn::Rowid) | + Column::Fulltext(FulltextColumn::Text) => { + // We never expose `rowid` via queries. We do expose `text`, but only + // indirectly, by joining against `datoms`. Therefore, these are meaningless. + unimplemented!() + }, + Column::Fixed(DatomsColumn::ValueTypeTag) => { // I'm pretty sure this is meaningless right now, because we will never bind // a type tag to a variable -- there's no syntax for doing so. diff --git a/query-algebrizer/src/errors.rs b/query-algebrizer/src/errors.rs index 852e07fc..7f47f77d 100644 --- a/query-algebrizer/src/errors.rs +++ b/query-algebrizer/src/errors.rs @@ -22,6 +22,14 @@ use self::mentat_query::{ pub enum BindingError { NoBoundVariable, RepeatedBoundVariable, // TODO: include repeated variable(s). + + /// Expected `[[?x ?y]]` but got some other type of binding. Mentat is deliberately more strict + /// than Datomic: we won't try to make sense of non-obvious (and potentially erroneous) bindings. + ExpectedBindRel, + + /// Expected `[?x1 … ?xN]` or `[[?x1 … ?xN]]` but got some other number of bindings. Mentat is + /// deliberately more strict than Datomic: we prefer placeholders to omission. + InvalidNumberOfBindings { number: usize, expected: usize }, } error_chain! { diff --git a/query-algebrizer/src/lib.rs b/query-algebrizer/src/lib.rs index 9bf9b8d1..950c488a 100644 --- a/query-algebrizer/src/lib.rs +++ b/query-algebrizer/src/lib.rs @@ -217,6 +217,7 @@ pub use types::{ ComputedTable, DatomsColumn, DatomsTable, + FulltextColumn, OrderBy, QualifiedAlias, QueryValue, diff --git a/query-algebrizer/src/types.rs b/query-algebrizer/src/types.rs index 17553bad..e9f11e7d 100644 --- a/query-algebrizer/src/types.rs +++ b/query-algebrizer/src/types.rs @@ -85,6 +85,13 @@ pub enum DatomsColumn { ValueTypeTag, } +/// One of the named columns of our fulltext values table. +#[derive(PartialEq, Eq, Clone)] +pub enum FulltextColumn { + Rowid, + Text, +} + #[derive(PartialEq, Eq, Clone)] pub enum VariableColumn { Variable(Variable), @@ -94,6 +101,7 @@ pub enum VariableColumn { #[derive(PartialEq, Eq, Clone)] pub enum Column { Fixed(DatomsColumn), + Fulltext(FulltextColumn), Variable(VariableColumn), } @@ -157,11 +165,34 @@ impl Debug for Column { fn fmt(&self, f: &mut Formatter) -> Result { match self { &Column::Fixed(ref c) => c.fmt(f), + &Column::Fulltext(ref c) => c.fmt(f), &Column::Variable(ref v) => v.fmt(f), } } } +impl FulltextColumn { + pub fn as_str(&self) -> &'static str { + use self::FulltextColumn::*; + match *self { + Rowid => "rowid", + Text => "text", + } + } +} + +impl ColumnName for FulltextColumn { + fn column_name(&self) -> String { + self.as_str().to_string() + } +} + +impl Debug for FulltextColumn { + fn fmt(&self, f: &mut Formatter) -> Result { + write!(f, "{}", self.as_str()) + } +} + /// A specific instance of a table within a query. E.g., "datoms123". pub type TableAlias = String; @@ -301,6 +332,9 @@ pub enum ColumnConstraint { }, HasType(TableAlias, ValueType), NotExists(ComputedTable), + // TODO: Merge this with NumericInequality? I expect the fine-grained information to be + // valuable when optimizing. + Matches(QualifiedAlias, QueryValue), } #[derive(PartialEq, Eq, Debug)] @@ -411,6 +445,10 @@ impl Debug for ColumnConstraint { write!(f, "{:?} {:?} {:?}", left, operator, right) }, + &Matches(ref qa, ref thing) => { + write!(f, "{:?} MATCHES {:?}", qa, thing) + }, + &HasType(ref qa, value_type) => { write!(f, "{:?}.value_type_tag = {:?}", qa, value_type) }, @@ -426,6 +464,7 @@ pub enum EmptyBecause { ConflictingBindings { var: Variable, existing: TypedValue, desired: TypedValue }, TypeMismatch { var: Variable, existing: ValueTypeSet, desired: ValueTypeSet }, NoValidTypes(Variable), + NonAttributeArgument, NonNumericArgument, NonStringFulltextValue, UnresolvedIdent(NamespacedKeyword), @@ -451,6 +490,9 @@ impl Debug for EmptyBecause { &NoValidTypes(ref var) => { write!(f, "Type mismatch: {:?} has no valid types", var) }, + &NonAttributeArgument => { + write!(f, "Non-attribute argument in attribute place") + }, &NonNumericArgument => { write!(f, "Non-numeric argument in numeric place") }, diff --git a/query-sql/src/lib.rs b/query-sql/src/lib.rs index 470a8024..f6edbfa8 100644 --- a/query-sql/src/lib.rs +++ b/query-sql/src/lib.rs @@ -124,6 +124,14 @@ impl Constraint { right: right, } } + + pub fn fulltext_match(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint { + Constraint::Infix { + op: Op("MATCH"), // SQLite specific! + left: left, + right: right, + } + } } #[allow(dead_code)] @@ -198,6 +206,10 @@ fn push_column(qb: &mut QueryBuilder, col: &Column) -> BuildQueryResult { qb.push_sql(d.as_str()); Ok(()) }, + &Column::Fulltext(ref d) => { + qb.push_sql(d.as_str()); + Ok(()) + }, &Column::Variable(ref vc) => push_variable_column(qb, vc), } } @@ -555,22 +567,30 @@ impl SelectQuery { #[cfg(test)] mod tests { use super::*; + use std::rc::Rc; + use mentat_query_algebrizer::{ + Column, DatomsColumn, DatomsTable, + FulltextColumn, }; - fn build(c: &QueryFragment) -> String { + fn build_query(c: &QueryFragment) -> SQLQuery { let mut builder = SQLiteQueryBuilder::new(); c.push_sql(&mut builder) .map(|_| builder.finish()) - .unwrap().sql + .expect("to produce a query for the given constraint") + } + + fn build(c: &QueryFragment) -> String { + build_query(c).sql } #[test] fn test_in_constraint() { let none = Constraint::In { - left: ColumnOrExpression::Column(QualifiedAlias::new("datoms01".to_string(), DatomsColumn::Value)), + left: ColumnOrExpression::Column(QualifiedAlias::new("datoms01".to_string(), Column::Fixed(DatomsColumn::Value))), list: vec![], }; @@ -651,6 +671,25 @@ mod tests { "SELECT 0 AS `?a`, 0 AS `?b` WHERE 0 UNION ALL VALUES (0, 1), (1, 2)"); } + #[test] + fn test_matches_constraint() { + let c = Constraint::Infix { + op: Op("MATCHES"), + left: ColumnOrExpression::Column(QualifiedAlias("fulltext01".to_string(), Column::Fulltext(FulltextColumn::Text))), + right: ColumnOrExpression::Value(TypedValue::String(Rc::new("needle".to_string()))), + }; + let q = build_query(&c); + assert_eq!("`fulltext01`.text MATCHES $v0", q.sql); + assert_eq!(vec![("$v0".to_string(), Rc::new(mentat_sql::Value::Text("needle".to_string())))], q.args); + + let c = Constraint::Infix { + op: Op("="), + left: ColumnOrExpression::Column(QualifiedAlias("fulltext01".to_string(), Column::Fulltext(FulltextColumn::Rowid))), + right: ColumnOrExpression::Column(QualifiedAlias("datoms02".to_string(), Column::Fixed(DatomsColumn::Value))), + }; + assert_eq!("`fulltext01`.rowid = `datoms02`.v", build(&c)); + } + #[test] fn test_end_to_end() { // [:find ?x :where [?x 65537 ?v] [?x 65536 ?v]] diff --git a/query-translator/src/translate.rs b/query-translator/src/translate.rs index 0928aa90..af3ff6b5 100644 --- a/query-translator/src/translate.rs +++ b/query-translator/src/translate.rs @@ -148,6 +148,14 @@ impl ToConstraint for ColumnConstraint { } }, + Matches(left, right) => { + Constraint::Infix { + op: Op("MATCH"), + left: ColumnOrExpression::Column(left), + right: right.into(), + } + }, + HasType(table, value_type) => { let column = QualifiedAlias::new(table, DatomsColumn::ValueTypeTag).to_column(); Constraint::equal(column, ColumnOrExpression::Integer(value_type.value_type_tag())) diff --git a/query-translator/tests/translate.rs b/query-translator/tests/translate.rs index c37fe698..911860c0 100644 --- a/query-translator/tests/translate.rs +++ b/query-translator/tests/translate.rs @@ -69,6 +69,13 @@ fn prepopulated_typed_schema(foo_type: ValueType) -> Schema { value_type: foo_type, ..Default::default() }); + associate_ident(&mut schema, NamespacedKeyword::new("foo", "fts"), 100); + add_attribute(&mut schema, 100, Attribute { + value_type: ValueType::String, + index: true, + fulltext: true, + ..Default::default() + }); schema }