From 20aa11dcbdabd03b0274b849f640caff84b46882 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Wed, 14 Jun 2017 14:44:16 -0700 Subject: [PATCH] Support variable fulltext searches. (#479) r=nalexander --- query-algebrizer/src/clauses/fulltext.rs | 39 +++++++++++--- tests/query.rs | 65 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/query-algebrizer/src/clauses/fulltext.rs b/query-algebrizer/src/clauses/fulltext.rs index a0e077b7..d4057a3b 100644 --- a/query-algebrizer/src/clauses/fulltext.rs +++ b/query-algebrizer/src/clauses/fulltext.rs @@ -14,6 +14,8 @@ use mentat_core::{ ValueType, }; +use mentat_core::util::Either; + use mentat_query::{ Binding, FnArg, @@ -153,29 +155,50 @@ impl ConjoiningClauses { // - It's already bound, either by input or by a previous pattern like `ground`. // - It's not already bound, but it's a defined input of type Text. Not yet implemented: TODO. // - It's not bound. The query cannot be algebrized. - let search: TypedValue = match args.next().unwrap() { + let search: Either = match args.next().unwrap() { FnArg::Constant(NonIntegerConstant::Text(s)) => { - TypedValue::String(s) + Either::Left(TypedValue::String(s)) }, FnArg::Variable(in_var) => { match self.bound_value(&in_var) { - Some(t @ TypedValue::String(_)) => t, + Some(t @ TypedValue::String(_)) => Either::Left(t), Some(_) => bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "string".into(), 2)), None => { - if self.input_variables.contains(&in_var) && - self.known_type(&in_var) == Some(ValueType::String) { - // Sorry, we haven't implemented late binding. + // Regardless of whether we'll be providing a string later, or the value + // comes from a column, it must be a string. + if self.known_type(&in_var) != Some(ValueType::String) { + bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "string".into(), 2)); + } + + if self.input_variables.contains(&in_var) { + // Sorry, we haven't implemented late binding. + // TODO: implement this. + bail!(ErrorKind::UnboundVariable((*in_var.0).clone())); + } else { + // It must be bound earlier in the query. We already established that + // it must be a string column. + if let Some(binding) = self.column_bindings + .get(&in_var) + .and_then(|bindings| bindings.get(0).cloned()) { + Either::Right(binding) + } else { + bail!(ErrorKind::UnboundVariable((*in_var.0).clone())); + } } - bail!(ErrorKind::UnboundVariable((*in_var.0).clone())); }, } }, _ => bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "string".into(), 2)), }; + let qv = match search { + Either::Left(tv) => QueryValue::TypedValue(tv), + Either::Right(qa) => QueryValue::Column(qa), + }; + let constraint = ColumnConstraint::Matches(QualifiedAlias(fulltext_values_alias.clone(), Column::Fulltext(FulltextColumn::Text)), - QueryValue::TypedValue(search)); + qv); self.wheres.add_intersection(constraint); if let VariableOrPlaceholder::Variable(ref var) = b_entity { diff --git a/tests/query.rs b/tests/query.rs index 9b685de4..4685834f 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -14,6 +14,7 @@ extern crate time; extern crate mentat; extern crate mentat_core; extern crate mentat_db; +extern crate mentat_query_algebrizer; // For errors. use std::str::FromStr; @@ -28,6 +29,7 @@ use mentat_core::{ use mentat::{ NamespacedKeyword, + PlainSymbol, QueryInputs, QueryResults, Variable, @@ -259,12 +261,19 @@ fn test_instants_and_uuids() { fn test_fulltext() { let mut c = new_connection("").expect("Couldn't open conn."); let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); + conn.transact(&mut c, r#"[ + [:db/add "a" :db/ident :foo/term] + [:db/add "a" :db/valueType :db.type/string] + [:db/add "a" :db/fulltext false] + [:db/add "a" :db/cardinality :db.cardinality/many] + [:db/add "s" :db/ident :foo/fts] [:db/add "s" :db/valueType :db.type/string] [:db/add "s" :db/fulltext true] [:db/add "s" :db/cardinality :db.cardinality/many] ]"#).unwrap(); + let v = conn.transact(&mut c, r#"[ [:db/add "v" :foo/fts "hello darkness my old friend"] [:db/add "v" :foo/fts "I've come to talk with you again"] @@ -290,4 +299,60 @@ fn test_fulltext() { }, _ => panic!("Expected query to work."), } + + let a = conn.transact(&mut c, r#"[[:db/add "a" :foo/term "talk"]]"#) + .unwrap() + .tempids + .get("a").cloned() + .expect("a was mapped"); + + // If you use a non-constant search term, it must be bound earlier in the query. + let query = r#"[:find ?x ?val + :where + [(fulltext $ :foo/fts ?term) [[?x ?val]]] + [?a :foo/term ?term] + ]"#; + let r = conn.q_once(&mut c, query, None); + match r { + Err(Error(ErrorKind::QueryError(mentat_query_algebrizer::ErrorKind::InvalidArgument(PlainSymbol(s), ty, i)), _)) => { + assert_eq!(s, "fulltext"); + assert_eq!(ty, "string"); + assert_eq!(i, 2); + }, + _ => panic!("Expected query to fail."), + } + + // Bound to the wrong type? Error. + let query = r#"[:find ?x ?val + :where + [?a :foo/term ?term] + [(fulltext $ :foo/fts ?a) [[?x ?val]]]]"#; + let r = conn.q_once(&mut c, query, None); + match r { + Err(Error(ErrorKind::QueryError(mentat_query_algebrizer::ErrorKind::InvalidArgument(PlainSymbol(s), ty, i)), _)) => { + assert_eq!(s, "fulltext"); + assert_eq!(ty, "string"); + assert_eq!(i, 2); + }, + _ => panic!("Expected query to fail."), + } + + // If it's bound, and the right type, it'll work! + let query = r#"[:find ?x ?val + :in ?a + :where + [?a :foo/term ?term] + [(fulltext $ :foo/fts ?term) [[?x ?val]]]]"#; + let inputs = QueryInputs::with_value_sequence(vec![(Variable::from_valid_name("?a"), TypedValue::Ref(a))]); + let r = conn.q_once(&mut c, query, inputs); + match r { + Result::Ok(QueryResults::Rel(rels)) => { + assert_eq!(rels, vec![ + vec![TypedValue::Ref(v), + TypedValue::String("I've come to talk with you again".to_string().into()), + ] + ]); + }, + _ => panic!("Expected query to work."), + } }