Compare commits
24 commits
master
...
fluffyemil
Author | SHA1 | Date | |
---|---|---|---|
|
daaff7074d | ||
|
649eff08a5 | ||
|
60592a87e6 | ||
|
114f71fa5e | ||
|
76d0223c2a | ||
|
4b45827be7 | ||
|
71d3aa29ed | ||
|
6544ca1594 | ||
|
62fda71fbc | ||
|
08cae4bc7c | ||
|
c203046c16 | ||
|
678a116130 | ||
|
f7fb22ae7e | ||
|
0165a842ef | ||
|
f8e75d817e | ||
|
7135eeac49 | ||
|
720fbf3d01 | ||
|
ff136b2546 | ||
|
e947a32c59 | ||
|
f4e2a0471f | ||
|
dcc05c643c | ||
|
8754cf224b | ||
|
f807b16db1 | ||
|
c1409078fa |
15 changed files with 920 additions and 43 deletions
|
@ -38,8 +38,20 @@ pub type ResultParser<O, I> = Expected<FnParser<I, fn(I) -> ParseResult<O, I>>>;
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! assert_parses_to {
|
macro_rules! assert_parses_to {
|
||||||
( $parser: expr, $input: expr, $expected: expr ) => {{
|
( $parser: expr, $input: expr, $expected: expr ) => {{
|
||||||
let mut par = $parser();
|
let par = $parser();
|
||||||
let result = par.parse($input.with_spans().into_atom_stream()).map(|x| x.0); // TODO: check remainder of stream.
|
let result = par.skip(eof()).parse($input.with_spans().into_atom_stream()).map(|x| x.0);
|
||||||
|
assert_eq!(result, Ok($expected));
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `assert_edn_parses_to!` simplifies some of the boilerplate around running a parser function
|
||||||
|
/// against string input and expecting a certain result.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! assert_edn_parses_to {
|
||||||
|
( $parser: expr, $input: expr, $expected: expr ) => {{
|
||||||
|
let par = $parser();
|
||||||
|
let input = edn::parse::value($input).expect("to be able to parse input as EDN");
|
||||||
|
let result = par.skip(eof()).parse(input.into_atom_stream()).map(|x| x.0);
|
||||||
assert_eq!(result, Ok($expected));
|
assert_eq!(result, Ok($expected));
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,11 +56,15 @@ use types::{
|
||||||
};
|
};
|
||||||
|
|
||||||
mod or;
|
mod or;
|
||||||
|
mod not;
|
||||||
mod pattern;
|
mod pattern;
|
||||||
mod predicate;
|
mod predicate;
|
||||||
mod resolve;
|
mod resolve;
|
||||||
|
|
||||||
use validate::validate_or_join;
|
use validate::{
|
||||||
|
validate_not_join,
|
||||||
|
validate_or_join,
|
||||||
|
};
|
||||||
|
|
||||||
// We do this a lot for errors.
|
// We do this a lot for errors.
|
||||||
trait RcCloned<T> {
|
trait RcCloned<T> {
|
||||||
|
@ -96,6 +100,7 @@ fn unit_type_set(t: ValueType) -> HashSet<ValueType> {
|
||||||
///
|
///
|
||||||
/// - Ordinary pattern clauses turn into `FROM` parts and `WHERE` parts using `=`.
|
/// - Ordinary pattern clauses turn into `FROM` parts and `WHERE` parts using `=`.
|
||||||
/// - Predicate clauses turn into the same, but with other functions.
|
/// - Predicate clauses turn into the same, but with other functions.
|
||||||
|
/// - Function clauses turn `WHERE` parts using function-specific comparisons.
|
||||||
/// - `not` turns into `NOT EXISTS` with `WHERE` clauses inside the subquery to
|
/// - `not` turns into `NOT EXISTS` with `WHERE` clauses inside the subquery to
|
||||||
/// bind it to the outer variables, or adds simple `WHERE` clauses to the outer
|
/// bind it to the outer variables, or adds simple `WHERE` clauses to the outer
|
||||||
/// clause.
|
/// clause.
|
||||||
|
@ -581,11 +586,17 @@ impl ConjoiningClauses {
|
||||||
WhereClause::Pred(p) => {
|
WhereClause::Pred(p) => {
|
||||||
self.apply_predicate(schema, p)
|
self.apply_predicate(schema, p)
|
||||||
},
|
},
|
||||||
|
WhereClause::WhereFn(f) => {
|
||||||
|
self.apply_where_fn(schema, f)
|
||||||
|
},
|
||||||
WhereClause::OrJoin(o) => {
|
WhereClause::OrJoin(o) => {
|
||||||
validate_or_join(&o)
|
validate_or_join(&o)
|
||||||
//?;
|
//?;
|
||||||
//self.apply_or_join(schema, o)
|
//self.apply_or_join(schema, o)
|
||||||
},
|
},
|
||||||
|
WhereClause::NotJoin(n) => {
|
||||||
|
validate_not_join(&n)
|
||||||
|
},
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@
|
||||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||||
// specific language governing permissions and limitations under the License.
|
// specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
Schema,
|
Schema,
|
||||||
TypedValue,
|
TypedValue,
|
||||||
|
@ -268,6 +266,7 @@ mod testing {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use mentat_core::attribute::Unique;
|
use mentat_core::attribute::Unique;
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
|
|
|
@ -10,35 +10,48 @@
|
||||||
|
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
Schema,
|
Schema,
|
||||||
|
TypedValue,
|
||||||
|
ValueType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mentat_query::{
|
use mentat_query::{
|
||||||
|
Binding,
|
||||||
|
FnArg,
|
||||||
|
NonIntegerConstant,
|
||||||
Predicate,
|
Predicate,
|
||||||
|
SrcVar,
|
||||||
|
VariableOrPlaceholder,
|
||||||
|
WhereFn,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clauses::ConjoiningClauses;
|
use clauses::ConjoiningClauses;
|
||||||
|
|
||||||
use errors::{
|
use errors::{
|
||||||
Result,
|
Error,
|
||||||
ErrorKind,
|
ErrorKind,
|
||||||
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use types::{
|
use types::{
|
||||||
ColumnConstraint,
|
ColumnConstraint,
|
||||||
|
DatomsColumn,
|
||||||
|
DatomsTable,
|
||||||
|
FulltextColumn,
|
||||||
|
FulltextQualifiedAlias,
|
||||||
NumericComparison,
|
NumericComparison,
|
||||||
|
QualifiedAlias,
|
||||||
|
QueryValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Application of predicates.
|
/// Application of predicates.
|
||||||
impl ConjoiningClauses {
|
impl ConjoiningClauses {
|
||||||
/// There are several kinds of predicates/functions in our Datalog:
|
/// There are several kinds of predicates in our Datalog:
|
||||||
/// - A limited set of binary comparison operators: < > <= >= !=.
|
/// - A limited set of binary comparison operators: < > <= >= !=.
|
||||||
/// These are converted into SQLite binary comparisons and some type constraints.
|
/// These are converted into SQLite binary comparisons and some type constraints.
|
||||||
/// - A set of predicates like `fulltext` and `get-else` that are translated into
|
|
||||||
/// SQL `MATCH`es or joins, yielding bindings.
|
|
||||||
/// - In the future, some predicates that are implemented via function calls in SQLite.
|
/// - In the future, some predicates that are implemented via function calls in SQLite.
|
||||||
///
|
///
|
||||||
/// At present we have implemented only the five built-in comparison binary operators.
|
/// At present we have implemented only the five built-in comparison binary operators.
|
||||||
pub fn apply_predicate<'s, 'p>(&mut self, schema: &'s Schema, predicate: Predicate) -> Result<()> {
|
pub fn apply_predicate<'s>(&mut self, schema: &'s Schema, predicate: Predicate) -> Result<()> {
|
||||||
// Because we'll be growing the set of built-in predicates, handling each differently,
|
// Because we'll be growing the set of built-in predicates, handling each differently,
|
||||||
// and ultimately allowing user-specified predicates, we match on the predicate name first.
|
// and ultimately allowing user-specified predicates, we match on the predicate name first.
|
||||||
if let Some(op) = NumericComparison::from_datalog_operator(predicate.operator.0.as_str()) {
|
if let Some(op) = NumericComparison::from_datalog_operator(predicate.operator.0.as_str()) {
|
||||||
|
@ -53,7 +66,7 @@ impl ConjoiningClauses {
|
||||||
/// - Ensures that the predicate functions name a known operator.
|
/// - Ensures that the predicate functions name a known operator.
|
||||||
/// - Accumulates a `NumericInequality` constraint into the `wheres` list.
|
/// - Accumulates a `NumericInequality` constraint into the `wheres` list.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
pub fn apply_numeric_predicate<'s, 'p>(&mut self, schema: &'s Schema, comparison: NumericComparison, predicate: Predicate) -> Result<()> {
|
pub fn apply_numeric_predicate<'s>(&mut self, schema: &'s Schema, comparison: NumericComparison, predicate: Predicate) -> Result<()> {
|
||||||
if predicate.args.len() != 2 {
|
if predicate.args.len() != 2 {
|
||||||
bail!(ErrorKind::InvalidNumberOfArguments(predicate.operator.clone(), predicate.args.len(), 2));
|
bail!(ErrorKind::InvalidNumberOfArguments(predicate.operator.clone(), predicate.args.len(), 2));
|
||||||
}
|
}
|
||||||
|
@ -81,6 +94,161 @@ impl ConjoiningClauses {
|
||||||
self.wheres.add_intersection(constraint);
|
self.wheres.add_intersection(constraint);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// There are several kinds of functions binding variables in our Datalog:
|
||||||
|
/// - A set of functions like `fulltext` and `get-else` that are translated into
|
||||||
|
/// SQL `MATCH`es or joins, yielding bindings.
|
||||||
|
/// - In the future, some functions that are implemented via function calls in SQLite.
|
||||||
|
///
|
||||||
|
/// At present we have implemented only the `fulltext` operator.
|
||||||
|
pub fn apply_where_fn<'s>(&mut self, schema: &'s Schema, where_fn: WhereFn) -> Result<()> {
|
||||||
|
// Because we'll be growing the set of built-in functions, handling each differently, and
|
||||||
|
// ultimately allowing user-specified functions, we match on the function name first.
|
||||||
|
match where_fn.operator.0.as_str() {
|
||||||
|
"fulltext" => self.apply_fulltext(schema, where_fn),
|
||||||
|
_ => bail!(ErrorKind::UnknownFunction(where_fn.operator.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function:
|
||||||
|
/// - Resolves variables and converts types to those more amenable to SQL.
|
||||||
|
/// - Ensures that the predicate functions name a known operator.
|
||||||
|
/// - Accumulates a `NumericInequality` constraint into the `wheres` list.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
pub fn apply_fulltext<'s>(&mut self, schema: &'s Schema, where_fn: WhereFn) -> Result<()> {
|
||||||
|
if where_fn.args.len() != 3 {
|
||||||
|
bail!(ErrorKind::InvalidNumberOfArguments(where_fn.operator.clone(), where_fn.args.len(), 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: binding-specific error messages.
|
||||||
|
let mut bindings = match where_fn.binding {
|
||||||
|
Binding::BindRel(bindings) => {
|
||||||
|
if bindings.len() > 4 {
|
||||||
|
bail!(ErrorKind::InvalidNumberOfArguments(where_fn.operator.clone(), bindings.len(), 4));
|
||||||
|
}
|
||||||
|
bindings.into_iter()
|
||||||
|
},
|
||||||
|
_ => bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "bindings".into(), 999)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Go from arguments -- parser output -- to columns or values.
|
||||||
|
// Any variables that aren't bound by this point in the linear processing of clauses will
|
||||||
|
// cause the application of the predicate to fail.
|
||||||
|
let mut args = where_fn.args.into_iter();
|
||||||
|
|
||||||
|
// TODO: process source variables.
|
||||||
|
match args.next().unwrap() {
|
||||||
|
FnArg::SrcVar(SrcVar::DefaultSrc) => {},
|
||||||
|
_ => bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "source variable".into(), 0)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: accept placeholder and set of attributes. Alternately, consider putting the search
|
||||||
|
// term before the attribute arguments and collect the (variadic) attributes into a set.
|
||||||
|
// let a: Entid = self.resolve_attribute_argument(&where_fn.operator, 1, args.next().unwrap())?;
|
||||||
|
//
|
||||||
|
// TODO: allow non-constant attributes.
|
||||||
|
// TODO: improve the expression of this matching, possibly by using attribute_for_* uniformly.
|
||||||
|
let a = match args.next().unwrap() {
|
||||||
|
FnArg::Ident(i) => schema.get_entid(&i),
|
||||||
|
// Must be an entid.
|
||||||
|
FnArg::EntidOrInteger(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let a = a.ok_or(ErrorKind::InvalidArgument(where_fn.operator.clone(), "attribute".into(), 1))?;
|
||||||
|
let attribute = schema.attribute_for_entid(a).cloned().ok_or(ErrorKind::InvalidArgument(where_fn.operator.clone(), "attribute".into(), 1))?;
|
||||||
|
|
||||||
|
let fulltext_values = DatomsTable::FulltextValues;
|
||||||
|
let datoms_table = DatomsTable::Datoms;
|
||||||
|
|
||||||
|
let fulltext_values_alias = (self.aliaser)(fulltext_values);
|
||||||
|
let datoms_table_alias = (self.aliaser)(datoms_table);
|
||||||
|
|
||||||
|
// TODO: constrain types in more general cases?
|
||||||
|
self.constrain_attribute(datoms_table_alias.clone(), a);
|
||||||
|
|
||||||
|
self.wheres.add_intersection(ColumnConstraint::Equals(
|
||||||
|
QualifiedAlias(datoms_table_alias.clone(), DatomsColumn::Value),
|
||||||
|
QueryValue::FulltextColumn(FulltextQualifiedAlias(fulltext_values_alias.clone(), FulltextColumn::Rowid))));
|
||||||
|
|
||||||
|
// search is either text or a variable.
|
||||||
|
// TODO: should this just use `resolve_argument`? Should it add a new `resolve_*` function?
|
||||||
|
let search = match args.next().unwrap() {
|
||||||
|
FnArg::Variable(var) => {
|
||||||
|
self.column_bindings
|
||||||
|
.get(&var)
|
||||||
|
.and_then(|cols| cols.first().map(|col| QueryValue::Column(col.clone())))
|
||||||
|
.ok_or_else(|| Error::from_kind(ErrorKind::UnboundVariable(var.name())))?
|
||||||
|
},
|
||||||
|
FnArg::Constant(NonIntegerConstant::Text(s)) => {
|
||||||
|
QueryValue::TypedValue(TypedValue::typed_string(s.as_str()))
|
||||||
|
},
|
||||||
|
_ => bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "string".into(), 2)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: should we build the FQA in ::Matches, preventing nonsense like matching on ::Rowid?
|
||||||
|
let constraint = ColumnConstraint::Matches(FulltextQualifiedAlias(fulltext_values_alias.clone(), FulltextColumn::Text), search);
|
||||||
|
self.wheres.add_intersection(constraint);
|
||||||
|
|
||||||
|
if let Some(VariableOrPlaceholder::Variable(var)) = bindings.next() {
|
||||||
|
// TODO: can we just check for late binding here?
|
||||||
|
// Do we have, or will we have, an external binding for this variable?
|
||||||
|
if self.bound_value(&var).is_some() || self.input_variables.contains(&var) {
|
||||||
|
// That's a paddlin'!
|
||||||
|
bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "illegal bound variable".into(), 999))
|
||||||
|
}
|
||||||
|
self.constrain_var_to_type(var.clone(), ValueType::Ref);
|
||||||
|
|
||||||
|
let entity_alias = QualifiedAlias(datoms_table_alias.clone(), DatomsColumn::Entity);
|
||||||
|
self.column_bindings.entry(var).or_insert(vec![]).push(entity_alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(VariableOrPlaceholder::Variable(var)) = bindings.next() {
|
||||||
|
// TODO: can we just check for late binding here?
|
||||||
|
// Do we have, or will we have, an external binding for this variable?
|
||||||
|
if self.bound_value(&var).is_some() || self.input_variables.contains(&var) {
|
||||||
|
// That's a paddlin'!
|
||||||
|
bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "illegal bound variable".into(), 999))
|
||||||
|
}
|
||||||
|
self.constrain_var_to_type(var.clone(), ValueType::String);
|
||||||
|
|
||||||
|
// TODO: figure out how to represent a FulltextQualifiedAlias.
|
||||||
|
// let value_alias = FulltextQualifiedAlias(fulltext_values_alias.clone(), FulltextColumn::Text);
|
||||||
|
// self.column_bindings.entry(var).or_insert(vec![]).push(value_alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(VariableOrPlaceholder::Variable(var)) = bindings.next() {
|
||||||
|
// TODO: can we just check for late binding here?
|
||||||
|
// Do we have, or will we have, an external binding for this variable?
|
||||||
|
if self.bound_value(&var).is_some() || self.input_variables.contains(&var) {
|
||||||
|
// That's a paddlin'!
|
||||||
|
bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "illegal bound variable".into(), 999))
|
||||||
|
}
|
||||||
|
self.constrain_var_to_type(var.clone(), ValueType::Ref);
|
||||||
|
|
||||||
|
let tx_alias = QualifiedAlias(datoms_table_alias.clone(), DatomsColumn::Tx);
|
||||||
|
self.column_bindings.entry(var).or_insert(vec![]).push(tx_alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(VariableOrPlaceholder::Variable(var)) = bindings.next() {
|
||||||
|
// TODO: can we just check for late binding here?
|
||||||
|
// Do we have, or will we have, an external binding for this variable?
|
||||||
|
if self.bound_value(&var).is_some() || self.input_variables.contains(&var) {
|
||||||
|
// That's a paddlin'!
|
||||||
|
bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "illegal bound variable".into(), 999))
|
||||||
|
}
|
||||||
|
self.constrain_var_to_type(var.clone(), ValueType::Double);
|
||||||
|
|
||||||
|
// TODO: produce this using SQLite's matchinfo.
|
||||||
|
self.value_bindings.insert(var.clone(), TypedValue::Double(0.0.into()));
|
||||||
|
|
||||||
|
// TODO: figure out how to represent a constant binding.
|
||||||
|
// self.column_bindings.entry(var).or_insert(vec![]).push(score_alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -88,6 +256,8 @@ mod testing {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use mentat_core::attribute::Unique;
|
use mentat_core::attribute::Unique;
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
Attribute,
|
Attribute,
|
||||||
|
@ -96,13 +266,16 @@ mod testing {
|
||||||
};
|
};
|
||||||
|
|
||||||
use mentat_query::{
|
use mentat_query::{
|
||||||
|
Binding,
|
||||||
FnArg,
|
FnArg,
|
||||||
NamespacedKeyword,
|
NamespacedKeyword,
|
||||||
Pattern,
|
Pattern,
|
||||||
PatternNonValuePlace,
|
PatternNonValuePlace,
|
||||||
PatternValuePlace,
|
PatternValuePlace,
|
||||||
PlainSymbol,
|
PlainSymbol,
|
||||||
|
SrcVar,
|
||||||
Variable,
|
Variable,
|
||||||
|
VariableOrPlaceholder,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clauses::{
|
use clauses::{
|
||||||
|
@ -229,4 +402,68 @@ mod testing {
|
||||||
.collect(),
|
.collect(),
|
||||||
ValueType::String));
|
ValueType::String));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_fulltext() {
|
||||||
|
let mut cc = ConjoiningClauses::default();
|
||||||
|
let mut schema = Schema::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()
|
||||||
|
});
|
||||||
|
|
||||||
|
let op = PlainSymbol::new("fulltext");
|
||||||
|
cc.apply_fulltext(&schema, WhereFn {
|
||||||
|
operator: op,
|
||||||
|
args: vec![
|
||||||
|
FnArg::SrcVar(SrcVar::DefaultSrc),
|
||||||
|
FnArg::Ident(NamespacedKeyword::new("foo", "fts")),
|
||||||
|
FnArg::Constant(NonIntegerConstant::Text(Rc::new("needle".into()))),
|
||||||
|
],
|
||||||
|
binding: Binding::BindRel(vec![VariableOrPlaceholder::Variable(Variable::from_valid_name("?entity")),
|
||||||
|
VariableOrPlaceholder::Variable(Variable::from_valid_name("?value")),
|
||||||
|
VariableOrPlaceholder::Variable(Variable::from_valid_name("?tx")),
|
||||||
|
VariableOrPlaceholder::Variable(Variable::from_valid_name("?score"))]),
|
||||||
|
}).expect("to be able to apply_fulltext");
|
||||||
|
|
||||||
|
assert!(!cc.is_known_empty);
|
||||||
|
|
||||||
|
// Finally, expand column bindings.
|
||||||
|
cc.expand_column_bindings();
|
||||||
|
assert!(!cc.is_known_empty);
|
||||||
|
|
||||||
|
let clauses = cc.wheres;
|
||||||
|
assert_eq!(clauses.len(), 3);
|
||||||
|
|
||||||
|
assert_eq!(clauses.0[0], ColumnConstraint::Equals(QualifiedAlias("datoms01".to_string(), DatomsColumn::Attribute),
|
||||||
|
QueryValue::Entid(100)).into());
|
||||||
|
assert_eq!(clauses.0[1], ColumnConstraint::Equals(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value),
|
||||||
|
QueryValue::FulltextColumn(FulltextQualifiedAlias("fulltext_values00".to_string(), FulltextColumn::Rowid))).into());
|
||||||
|
assert_eq!(clauses.0[2], ColumnConstraint::Matches(FulltextQualifiedAlias("fulltext_values00".to_string(), FulltextColumn::Text),
|
||||||
|
QueryValue::TypedValue(TypedValue::String(Rc::new("needle".into())))).into());
|
||||||
|
|
||||||
|
let bindings = cc.column_bindings;
|
||||||
|
assert_eq!(bindings.len(), 2);
|
||||||
|
|
||||||
|
assert_eq!(bindings.get(&Variable::from_valid_name("?entity")).expect("column binding for ?entity").clone(),
|
||||||
|
vec![QualifiedAlias("datoms01".to_string(), DatomsColumn::Entity)]);
|
||||||
|
assert_eq!(bindings.get(&Variable::from_valid_name("?tx")).expect("column binding for ?tx").clone(),
|
||||||
|
vec![QualifiedAlias("datoms01".to_string(), DatomsColumn::Tx)]);
|
||||||
|
|
||||||
|
let known_types = cc.known_types;
|
||||||
|
assert_eq!(known_types.len(), 4);
|
||||||
|
|
||||||
|
assert_eq!(known_types.get(&Variable::from_valid_name("?entity")).expect("known types for ?entity").clone(),
|
||||||
|
vec![ValueType::Ref].into_iter().collect());
|
||||||
|
assert_eq!(known_types.get(&Variable::from_valid_name("?value")).expect("known types for ?value").clone(),
|
||||||
|
vec![ValueType::String].into_iter().collect());
|
||||||
|
assert_eq!(known_types.get(&Variable::from_valid_name("?tx")).expect("known types for ?tx").clone(),
|
||||||
|
vec![ValueType::Ref].into_iter().collect());
|
||||||
|
assert_eq!(known_types.get(&Variable::from_valid_name("?score")).expect("known types for ?score").clone(),
|
||||||
|
vec![ValueType::Double].into_iter().collect());
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -56,7 +56,7 @@ impl ConjoiningClauses {
|
||||||
Constant(NonIntegerConstant::Text(_)) |
|
Constant(NonIntegerConstant::Text(_)) |
|
||||||
Constant(NonIntegerConstant::BigInteger(_)) => {
|
Constant(NonIntegerConstant::BigInteger(_)) => {
|
||||||
self.mark_known_empty(EmptyBecause::NonNumericArgument);
|
self.mark_known_empty(EmptyBecause::NonNumericArgument);
|
||||||
bail!(ErrorKind::NonNumericArgument(function.clone(), position));
|
bail!(ErrorKind::InvalidArgument(function.clone(), "numeric".into(), position));
|
||||||
},
|
},
|
||||||
Constant(NonIntegerConstant::Float(f)) => Ok(QueryValue::TypedValue(TypedValue::Double(f))),
|
Constant(NonIntegerConstant::Float(f)) => Ok(QueryValue::TypedValue(TypedValue::Double(f))),
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,9 +35,9 @@ error_chain! {
|
||||||
display("unbound variable: {}", name)
|
display("unbound variable: {}", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
NonNumericArgument(function: PlainSymbol, position: usize) {
|
InvalidArgument(function: PlainSymbol, expected_type: String, position: usize) {
|
||||||
description("invalid argument")
|
description("invalid argument")
|
||||||
display("invalid argument to {}: expected numeric in position {}.", function, position)
|
display("invalid argument to {}: expected {} in position {}.", function, expected_type, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
NonMatchingVariablesInOrClause {
|
NonMatchingVariablesInOrClause {
|
||||||
|
@ -45,6 +45,12 @@ error_chain! {
|
||||||
description("non-matching variables in 'or' clause")
|
description("non-matching variables in 'or' clause")
|
||||||
display("non-matching variables in 'or' clause")
|
display("non-matching variables in 'or' clause")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NonMatchingVariablesInNotClause {
|
||||||
|
// TODO: flesh out.
|
||||||
|
description("non-matching variables in 'not' clause")
|
||||||
|
display("non-matching variables in 'not' clause")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,8 @@ pub use types::{
|
||||||
ColumnIntersection,
|
ColumnIntersection,
|
||||||
DatomsColumn,
|
DatomsColumn,
|
||||||
DatomsTable,
|
DatomsTable,
|
||||||
|
FulltextColumn,
|
||||||
|
FulltextQualifiedAlias,
|
||||||
QualifiedAlias,
|
QualifiedAlias,
|
||||||
QueryValue,
|
QueryValue,
|
||||||
SourceAlias,
|
SourceAlias,
|
||||||
|
|
|
@ -71,6 +71,23 @@ impl DatomsColumn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One of the named columns of our fulltext values table.
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
|
pub enum FulltextColumn {
|
||||||
|
Rowid,
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FulltextColumn {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
use self::FulltextColumn::*;
|
||||||
|
match *self {
|
||||||
|
Rowid => "rowid",
|
||||||
|
Text => "text",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A specific instance of a table within a query. E.g., "datoms123".
|
/// A specific instance of a table within a query. E.g., "datoms123".
|
||||||
pub type TableAlias = String;
|
pub type TableAlias = String;
|
||||||
|
|
||||||
|
@ -94,6 +111,16 @@ impl Debug for QualifiedAlias {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A particular column of a particular aliased fulltext table. E.g., "fulltext_values123", Rowid.
|
||||||
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
|
pub struct FulltextQualifiedAlias(pub TableAlias, pub FulltextColumn);
|
||||||
|
|
||||||
|
impl Debug for FulltextQualifiedAlias {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
write!(f, "{}.{}", self.0, self.1.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl QualifiedAlias {
|
impl QualifiedAlias {
|
||||||
pub fn for_type_tag(&self) -> QualifiedAlias {
|
pub fn for_type_tag(&self) -> QualifiedAlias {
|
||||||
QualifiedAlias(self.0.clone(), DatomsColumn::ValueTypeTag)
|
QualifiedAlias(self.0.clone(), DatomsColumn::ValueTypeTag)
|
||||||
|
@ -103,6 +130,7 @@ impl QualifiedAlias {
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum QueryValue {
|
pub enum QueryValue {
|
||||||
Column(QualifiedAlias),
|
Column(QualifiedAlias),
|
||||||
|
FulltextColumn(FulltextQualifiedAlias),
|
||||||
Entid(Entid),
|
Entid(Entid),
|
||||||
TypedValue(TypedValue),
|
TypedValue(TypedValue),
|
||||||
|
|
||||||
|
@ -120,6 +148,9 @@ impl Debug for QueryValue {
|
||||||
&Column(ref qa) => {
|
&Column(ref qa) => {
|
||||||
write!(f, "{:?}", qa)
|
write!(f, "{:?}", qa)
|
||||||
},
|
},
|
||||||
|
&FulltextColumn(ref qa) => {
|
||||||
|
write!(f, "{:?}", qa)
|
||||||
|
},
|
||||||
&Entid(ref entid) => {
|
&Entid(ref entid) => {
|
||||||
write!(f, "entity({:?})", entid)
|
write!(f, "entity({:?})", entid)
|
||||||
},
|
},
|
||||||
|
@ -192,6 +223,9 @@ pub enum ColumnConstraint {
|
||||||
right: QueryValue,
|
right: QueryValue,
|
||||||
},
|
},
|
||||||
HasType(TableAlias, ValueType),
|
HasType(TableAlias, ValueType),
|
||||||
|
// TODO: Merge this with NumericInequality? I expect the fine-grained information to be
|
||||||
|
// valuable when optimizing.
|
||||||
|
Matches(FulltextQualifiedAlias, QueryValue),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
|
@ -290,6 +324,10 @@ impl Debug for ColumnConstraint {
|
||||||
write!(f, "{:?} {:?} {:?}", left, operator, right)
|
write!(f, "{:?} {:?} {:?}", left, operator, right)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
&Matches(ref qa, ref thing) => {
|
||||||
|
write!(f, "{:?} MATCHES {:?}", qa, thing)
|
||||||
|
},
|
||||||
|
|
||||||
&HasType(ref qa, value_type) => {
|
&HasType(ref qa, value_type) => {
|
||||||
write!(f, "{:?}.value_type_tag = {:?}", qa, value_type)
|
write!(f, "{:?}.value_type_tag = {:?}", qa, value_type)
|
||||||
},
|
},
|
||||||
|
@ -301,6 +339,7 @@ impl Debug for ColumnConstraint {
|
||||||
pub enum EmptyBecause {
|
pub enum EmptyBecause {
|
||||||
// Var, existing, desired.
|
// Var, existing, desired.
|
||||||
TypeMismatch(Variable, HashSet<ValueType>, ValueType),
|
TypeMismatch(Variable, HashSet<ValueType>, ValueType),
|
||||||
|
NonAttributeArgument,
|
||||||
NonNumericArgument,
|
NonNumericArgument,
|
||||||
NonStringFulltextValue,
|
NonStringFulltextValue,
|
||||||
UnresolvedIdent(NamespacedKeyword),
|
UnresolvedIdent(NamespacedKeyword),
|
||||||
|
@ -319,6 +358,9 @@ impl Debug for EmptyBecause {
|
||||||
write!(f, "Type mismatch: {:?} can't be {:?}, because it's already {:?}",
|
write!(f, "Type mismatch: {:?} can't be {:?}, because it's already {:?}",
|
||||||
var, desired, existing)
|
var, desired, existing)
|
||||||
},
|
},
|
||||||
|
&NonAttributeArgument => {
|
||||||
|
write!(f, "Non-attribute argument in attribute place")
|
||||||
|
},
|
||||||
&NonNumericArgument => {
|
&NonNumericArgument => {
|
||||||
write!(f, "Non-numeric argument in numeric place")
|
write!(f, "Non-numeric argument in numeric place")
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,6 +13,7 @@ use std::collections::BTreeSet;
|
||||||
use mentat_query::{
|
use mentat_query::{
|
||||||
ContainsVariables,
|
ContainsVariables,
|
||||||
OrJoin,
|
OrJoin,
|
||||||
|
NotJoin,
|
||||||
Variable,
|
Variable,
|
||||||
UnifyVars,
|
UnifyVars,
|
||||||
};
|
};
|
||||||
|
@ -74,6 +75,23 @@ pub fn validate_or_join(or_join: &OrJoin) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn validate_not_join(not_join: &NotJoin) -> Result<()> {
|
||||||
|
// Grab our mentioned variables and ensure that the rules are followed.
|
||||||
|
match not_join.unify_vars {
|
||||||
|
UnifyVars::Implicit => {
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
UnifyVars::Explicit(ref vars) => {
|
||||||
|
// The joined vars must each appear somewhere in the clauses' mentioned variables.
|
||||||
|
let var_set: BTreeSet<Variable> = vars.iter().cloned().collect();
|
||||||
|
if !var_set.is_subset(¬_join.collect_mentioned_variables()) {
|
||||||
|
bail!(ErrorKind::NonMatchingVariablesInNotClause);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
extern crate mentat_core;
|
extern crate mentat_core;
|
||||||
|
@ -96,7 +114,15 @@ mod tests {
|
||||||
|
|
||||||
use clauses::ident;
|
use clauses::ident;
|
||||||
|
|
||||||
use super::validate_or_join;
|
use errors::{
|
||||||
|
Error,
|
||||||
|
ErrorKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
validate_not_join,
|
||||||
|
validate_or_join,
|
||||||
|
};
|
||||||
|
|
||||||
fn value_ident(ns: &str, name: &str) -> PatternValuePlace {
|
fn value_ident(ns: &str, name: &str) -> PatternValuePlace {
|
||||||
PatternValuePlace::IdentOrKeyword(::std::rc::Rc::new(NamespacedKeyword::new(ns, name)))
|
PatternValuePlace::IdentOrKeyword(::std::rc::Rc::new(NamespacedKeyword::new(ns, name)))
|
||||||
|
@ -229,4 +255,131 @@ mod tests {
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Tests that the top-level form is a valid `not`, returning the clauses.
|
||||||
|
fn valid_not_join(parsed: FindQuery, expected_unify: UnifyVars) -> Vec<WhereClause> {
|
||||||
|
// Filter out all the clauses that are not `not`s.
|
||||||
|
let mut nots = parsed.where_clauses.into_iter().filter(|x| match x {
|
||||||
|
&WhereClause::NotJoin(_) => true,
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// There should be only one not clause.
|
||||||
|
let clause = nots.next().unwrap();
|
||||||
|
assert_eq!(None, nots.next());
|
||||||
|
|
||||||
|
match clause {
|
||||||
|
WhereClause::NotJoin(not_join) => {
|
||||||
|
// It's valid: the variables are the same in each branch.
|
||||||
|
assert_eq!((), validate_not_join(¬_join).unwrap());
|
||||||
|
assert_eq!(expected_unify, not_join.unify_vars);
|
||||||
|
not_join.clauses
|
||||||
|
},
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that a `not` is valid if it is implicit.
|
||||||
|
#[test]
|
||||||
|
fn test_success_not() {
|
||||||
|
let query = r#"[:find ?name
|
||||||
|
:where [?id :artist/name ?name]
|
||||||
|
(not [?id :artist/country :country/CA]
|
||||||
|
[?id :artist/country :country/GB])]"#;
|
||||||
|
let parsed = parse_find_string(query).expect("expected successful parse");
|
||||||
|
let clauses = valid_not_join(parsed, UnifyVars::Implicit);
|
||||||
|
|
||||||
|
// Check each part of the body
|
||||||
|
let mut parts = clauses.into_iter();
|
||||||
|
match (parts.next(), parts.next(), parts.next()) {
|
||||||
|
(Some(clause1), Some(clause2), None) => {
|
||||||
|
assert_eq!(
|
||||||
|
clause1,
|
||||||
|
WhereClause::Pattern(Pattern {
|
||||||
|
source: None,
|
||||||
|
entity: PatternNonValuePlace::Variable(Variable::from_valid_name("?id")),
|
||||||
|
attribute: ident("artist", "country"),
|
||||||
|
value: value_ident("country", "CA"),
|
||||||
|
tx: PatternNonValuePlace::Placeholder,
|
||||||
|
}));
|
||||||
|
assert_eq!(
|
||||||
|
clause2,
|
||||||
|
WhereClause::Pattern(Pattern {
|
||||||
|
source: None,
|
||||||
|
entity: PatternNonValuePlace::Variable(Variable::from_valid_name("?id")),
|
||||||
|
attribute: ident("artist", "country"),
|
||||||
|
value: value_ident("country", "GB"),
|
||||||
|
tx: PatternNonValuePlace::Placeholder,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_success_not_join() {
|
||||||
|
let query = r#"[:find ?artist
|
||||||
|
:where [?artist :artist/name]
|
||||||
|
(not-join [?artist]
|
||||||
|
[?release :release/artists ?artist]
|
||||||
|
[?release :release/year 1970])]"#;
|
||||||
|
let parsed = parse_find_string(query).expect("expected successful parse");
|
||||||
|
let clauses = valid_not_join(parsed, UnifyVars::Explicit(vec![Variable::from_valid_name("?artist")]));
|
||||||
|
|
||||||
|
// Let's do some detailed parse checks.
|
||||||
|
let mut parts = clauses.into_iter();
|
||||||
|
match (parts.next(), parts.next(), parts.next()) {
|
||||||
|
(Some(clause1), Some(clause2), None) => {
|
||||||
|
assert_eq!(
|
||||||
|
clause1,
|
||||||
|
WhereClause::Pattern(Pattern {
|
||||||
|
source: None,
|
||||||
|
entity: PatternNonValuePlace::Variable(Variable::from_valid_name("?release")),
|
||||||
|
attribute: ident("release", "artists"),
|
||||||
|
value: PatternValuePlace::Variable(Variable::from_valid_name("?artist")),
|
||||||
|
tx: PatternNonValuePlace::Placeholder,
|
||||||
|
}));
|
||||||
|
assert_eq!(
|
||||||
|
clause2,
|
||||||
|
WhereClause::Pattern(Pattern {
|
||||||
|
source: None,
|
||||||
|
entity: PatternNonValuePlace::Variable(Variable::from_valid_name("?release")),
|
||||||
|
attribute: ident("release", "year"),
|
||||||
|
value: PatternValuePlace::EntidOrInteger(1970),
|
||||||
|
tx: PatternNonValuePlace::Placeholder,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that a `not-join` that does not use the joining var fails to validate.
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_explicit_not_join_non_matching_join_vars() {
|
||||||
|
let query = r#"[:find ?artist
|
||||||
|
:where [?artist :artist/name]
|
||||||
|
(not-join [?artist]
|
||||||
|
[?release :release/artists "Pink Floyd"]
|
||||||
|
[?release :release/year 1970])]"#;
|
||||||
|
let parsed = parse_find_string(query).expect("expected successful parse");
|
||||||
|
let mut nots = parsed.where_clauses.iter().filter(|&x| match *x {
|
||||||
|
WhereClause::NotJoin(_) => true,
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let clause = nots.next().unwrap().clone();
|
||||||
|
assert_eq!(None, nots.next());
|
||||||
|
|
||||||
|
match clause {
|
||||||
|
WhereClause::NotJoin(not_join) => { let result = validate_not_join(¬_join);
|
||||||
|
assert!(result.is_err());
|
||||||
|
match result.err().unwrap() {
|
||||||
|
Error(ErrorKind::NonMatchingVariablesInNotClause, _) => { assert!(true); },
|
||||||
|
x => panic!("expected NonMatchingVariablesInNotClause error, got {:?}", x),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -35,6 +35,7 @@ use self::mentat_parser_utils::value_and_span::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::mentat_query::{
|
use self::mentat_query::{
|
||||||
|
Binding,
|
||||||
Element,
|
Element,
|
||||||
FindQuery,
|
FindQuery,
|
||||||
FindSpec,
|
FindSpec,
|
||||||
|
@ -42,6 +43,7 @@ use self::mentat_query::{
|
||||||
FromValue,
|
FromValue,
|
||||||
OrJoin,
|
OrJoin,
|
||||||
OrWhereClause,
|
OrWhereClause,
|
||||||
|
NotJoin,
|
||||||
Pattern,
|
Pattern,
|
||||||
PatternNonValuePlace,
|
PatternNonValuePlace,
|
||||||
PatternValuePlace,
|
PatternValuePlace,
|
||||||
|
@ -50,7 +52,9 @@ use self::mentat_query::{
|
||||||
SrcVar,
|
SrcVar,
|
||||||
UnifyVars,
|
UnifyVars,
|
||||||
Variable,
|
Variable,
|
||||||
|
VariableOrPlaceholder,
|
||||||
WhereClause,
|
WhereClause,
|
||||||
|
WhereFn,
|
||||||
};
|
};
|
||||||
|
|
||||||
error_chain! {
|
error_chain! {
|
||||||
|
@ -129,11 +133,56 @@ def_parser!(Where, pattern_non_value_place, PatternNonValuePlace, {
|
||||||
satisfy_map(PatternNonValuePlace::from_value)
|
satisfy_map(PatternNonValuePlace::from_value)
|
||||||
});
|
});
|
||||||
|
|
||||||
def_matches_plain_symbol!(Where, and, "and");
|
|
||||||
|
|
||||||
def_matches_plain_symbol!(Where, or, "or");
|
def_parser!(Where, and, edn::ValueAndSpan, {
|
||||||
|
satisfy(|v: edn::ValueAndSpan| {
|
||||||
|
if let edn::SpannedValue::PlainSymbol(ref s) = v.inner {
|
||||||
|
s.0.as_str() == "and"
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
def_matches_plain_symbol!(Where, or_join, "or-join");
|
def_parser!(Where, or, edn::ValueAndSpan, {
|
||||||
|
satisfy(|v: edn::ValueAndSpan| {
|
||||||
|
if let edn::SpannedValue::PlainSymbol(ref s) = v.inner {
|
||||||
|
s.0.as_str() == "or"
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
def_parser!(Where, or_join, edn::ValueAndSpan, {
|
||||||
|
satisfy(|v: edn::ValueAndSpan| {
|
||||||
|
if let edn::SpannedValue::PlainSymbol(ref s) = v.inner {
|
||||||
|
s.0.as_str() == "or-join"
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
def_parser!(Where, not, edn::ValueAndSpan, {
|
||||||
|
satisfy(|v: edn::ValueAndSpan| {
|
||||||
|
if let edn::SpannedValue::PlainSymbol(ref s) = v.inner {
|
||||||
|
s.0.as_str() == "not"
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
def_parser!(Where, not_join, edn::ValueAndSpan, {
|
||||||
|
satisfy(|v: edn::ValueAndSpan| {
|
||||||
|
if let edn::SpannedValue::PlainSymbol(ref s) = v.inner {
|
||||||
|
s.0.as_str() == "not-join"
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
def_parser!(Where, rule_vars, Vec<Variable>, {
|
def_parser!(Where, rule_vars, Vec<Variable>, {
|
||||||
seq()
|
seq()
|
||||||
|
@ -182,6 +231,33 @@ def_parser!(Where, or_join_clause, WhereClause, {
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
def_parser!(Where, not_clause, WhereClause, {
|
||||||
|
seq()
|
||||||
|
.of_exactly(Where::not()
|
||||||
|
.with(many1(Where::clause()))
|
||||||
|
.map(|clauses| {
|
||||||
|
WhereClause::NotJoin(
|
||||||
|
NotJoin {
|
||||||
|
unify_vars: UnifyVars::Implicit,
|
||||||
|
clauses: clauses,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
def_parser!(Where, not_join_clause, WhereClause, {
|
||||||
|
seq()
|
||||||
|
.of_exactly(Where::not_join()
|
||||||
|
.with(Where::rule_vars())
|
||||||
|
.and(many1(Where::clause()))
|
||||||
|
.map(|(vars, clauses)| {
|
||||||
|
WhereClause::NotJoin(
|
||||||
|
NotJoin {
|
||||||
|
unify_vars: UnifyVars::Explicit(vars),
|
||||||
|
clauses: clauses,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
/// A vector containing just a parenthesized filter expression.
|
/// A vector containing just a parenthesized filter expression.
|
||||||
def_parser!(Where, pred, WhereClause, {
|
def_parser!(Where, pred, WhereClause, {
|
||||||
// Accept either a nested list or a nested vector here:
|
// Accept either a nested list or a nested vector here:
|
||||||
|
@ -198,6 +274,25 @@ def_parser!(Where, pred, WhereClause, {
|
||||||
})))
|
})))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// A vector containing a parenthesized function expression and a binding.
|
||||||
|
def_parser!(Where, where_fn, WhereClause, {
|
||||||
|
// Accept either a nested list or a nested vector here:
|
||||||
|
// `[(foo ?x ?y) binding]` or `[[foo ?x ?y] binding]`
|
||||||
|
vector()
|
||||||
|
.of_exactly(
|
||||||
|
(seq().of_exactly(
|
||||||
|
(Query::predicate_fn(), Query::arguments())),
|
||||||
|
Bind::binding())
|
||||||
|
.map(|((f, args), binding)| {
|
||||||
|
WhereClause::WhereFn(
|
||||||
|
WhereFn {
|
||||||
|
operator: f.0,
|
||||||
|
args: args,
|
||||||
|
binding: binding,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
def_parser!(Where, pattern, WhereClause, {
|
def_parser!(Where, pattern, WhereClause, {
|
||||||
vector()
|
vector()
|
||||||
.of_exactly(
|
.of_exactly(
|
||||||
|
@ -246,8 +341,11 @@ def_parser!(Where, clause, WhereClause, {
|
||||||
// We don't yet handle source vars.
|
// We don't yet handle source vars.
|
||||||
try(Where::or_join_clause()),
|
try(Where::or_join_clause()),
|
||||||
try(Where::or_clause()),
|
try(Where::or_clause()),
|
||||||
|
try(Where::not_join_clause()),
|
||||||
|
try(Where::not_clause()),
|
||||||
|
|
||||||
try(Where::pred()),
|
try(Where::pred()),
|
||||||
|
try(Where::where_fn()),
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -262,6 +360,8 @@ def_matches_plain_symbol!(Find, period, ".");
|
||||||
|
|
||||||
def_matches_plain_symbol!(Find, ellipsis, "...");
|
def_matches_plain_symbol!(Find, ellipsis, "...");
|
||||||
|
|
||||||
|
def_matches_plain_symbol!(Find, placeholder, "_");
|
||||||
|
|
||||||
def_parser!(Find, find_scalar, FindSpec, {
|
def_parser!(Find, find_scalar, FindSpec, {
|
||||||
Query::variable()
|
Query::variable()
|
||||||
.skip(Find::period())
|
.skip(Find::period())
|
||||||
|
@ -366,6 +466,47 @@ def_parser!(Find, query, FindQuery, {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub struct Bind;
|
||||||
|
|
||||||
|
def_parser!(Bind, bind_scalar, Binding, {
|
||||||
|
Query::variable()
|
||||||
|
.skip(eof())
|
||||||
|
.map(|var: Variable| -> Binding { Binding::BindScalar(var) })
|
||||||
|
});
|
||||||
|
|
||||||
|
def_parser!(Bind, variable_or_placeholder, VariableOrPlaceholder, {
|
||||||
|
Query::variable().map(VariableOrPlaceholder::Variable)
|
||||||
|
.or(Find::placeholder().map(|_| VariableOrPlaceholder::Placeholder))
|
||||||
|
});
|
||||||
|
|
||||||
|
def_parser!(Bind, bind_coll, Binding, {
|
||||||
|
vector()
|
||||||
|
.of_exactly(Query::variable()
|
||||||
|
.skip(Find::ellipsis()))
|
||||||
|
.map(Binding::BindColl)
|
||||||
|
});
|
||||||
|
|
||||||
|
def_parser!(Bind, bind_rel, Binding, {
|
||||||
|
vector().of_exactly(
|
||||||
|
many1::<Vec<VariableOrPlaceholder>, _>(Bind::variable_or_placeholder())
|
||||||
|
.map(Binding::BindRel))
|
||||||
|
});
|
||||||
|
|
||||||
|
def_parser!(Bind, bind_tuple, Binding, {
|
||||||
|
many1::<Vec<VariableOrPlaceholder>, _>(Bind::variable_or_placeholder())
|
||||||
|
.skip(eof())
|
||||||
|
.map(Binding::BindTuple)
|
||||||
|
});
|
||||||
|
|
||||||
|
def_parser!(Bind, binding, Binding, {
|
||||||
|
// Any one of the four binding types might apply, so we combine them with `choice`. Our parsers
|
||||||
|
// consume input, so we need to wrap them in `try` so that they operate independently.
|
||||||
|
choice([try(Bind::bind_scalar()),
|
||||||
|
try(Bind::bind_coll()),
|
||||||
|
try(Bind::bind_tuple()),
|
||||||
|
try(Bind::bind_rel())])
|
||||||
|
});
|
||||||
|
|
||||||
pub fn parse_find_string(string: &str) -> Result<FindQuery> {
|
pub fn parse_find_string(string: &str) -> Result<FindQuery> {
|
||||||
let expr = edn::parse::value(string)?;
|
let expr = edn::parse::value(string)?;
|
||||||
Find::query()
|
Find::query()
|
||||||
|
@ -385,6 +526,7 @@ mod test {
|
||||||
use self::combine::Parser;
|
use self::combine::Parser;
|
||||||
use self::edn::OrderedFloat;
|
use self::edn::OrderedFloat;
|
||||||
use self::mentat_query::{
|
use self::mentat_query::{
|
||||||
|
Binding,
|
||||||
Element,
|
Element,
|
||||||
FindSpec,
|
FindSpec,
|
||||||
NonIntegerConstant,
|
NonIntegerConstant,
|
||||||
|
@ -393,6 +535,7 @@ mod test {
|
||||||
PatternValuePlace,
|
PatternValuePlace,
|
||||||
SrcVar,
|
SrcVar,
|
||||||
Variable,
|
Variable,
|
||||||
|
VariableOrPlaceholder,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -548,6 +691,49 @@ mod test {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_not() {
|
||||||
|
let e = edn::PlainSymbol::new("?e");
|
||||||
|
let a = edn::PlainSymbol::new("?a");
|
||||||
|
let v = edn::PlainSymbol::new("?v");
|
||||||
|
|
||||||
|
assert_edn_parses_to!(Where::not_clause,
|
||||||
|
"(not [?e ?a ?v])",
|
||||||
|
WhereClause::NotJoin(
|
||||||
|
NotJoin {
|
||||||
|
unify_vars: UnifyVars::Implicit,
|
||||||
|
clauses: vec![
|
||||||
|
WhereClause::Pattern(Pattern {
|
||||||
|
source: None,
|
||||||
|
entity: PatternNonValuePlace::Variable(variable(e)),
|
||||||
|
attribute: PatternNonValuePlace::Variable(variable(a)),
|
||||||
|
value: PatternValuePlace::Variable(variable(v)),
|
||||||
|
tx: PatternNonValuePlace::Placeholder,
|
||||||
|
})],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_not_join() {
|
||||||
|
let e = edn::PlainSymbol::new("?e");
|
||||||
|
let a = edn::PlainSymbol::new("?a");
|
||||||
|
let v = edn::PlainSymbol::new("?v");
|
||||||
|
|
||||||
|
assert_edn_parses_to!(Where::not_join_clause,
|
||||||
|
"(not-join [?e] [?e ?a ?v])",
|
||||||
|
WhereClause::NotJoin(
|
||||||
|
NotJoin {
|
||||||
|
unify_vars: UnifyVars::Explicit(vec![variable(e.clone())]),
|
||||||
|
clauses: vec![WhereClause::Pattern(Pattern {
|
||||||
|
source: None,
|
||||||
|
entity: PatternNonValuePlace::Variable(variable(e)),
|
||||||
|
attribute: PatternNonValuePlace::Variable(variable(a)),
|
||||||
|
value: PatternValuePlace::Variable(variable(v)),
|
||||||
|
tx: PatternNonValuePlace::Placeholder,
|
||||||
|
})],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_sp_variable() {
|
fn test_find_sp_variable() {
|
||||||
let sym = edn::PlainSymbol::new("?x");
|
let sym = edn::PlainSymbol::new("?x");
|
||||||
|
@ -598,4 +784,86 @@ mod test {
|
||||||
FindSpec::FindTuple(vec![Element::Variable(variable(vx)),
|
FindSpec::FindTuple(vec![Element::Variable(variable(vx)),
|
||||||
Element::Variable(variable(vy))]));
|
Element::Variable(variable(vy))]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bind_scalar() {
|
||||||
|
let vx = edn::PlainSymbol::new("?x");
|
||||||
|
assert_edn_parses_to!(|| vector().of_exactly(Bind::binding()),
|
||||||
|
"[?x]",
|
||||||
|
Binding::BindScalar(variable(vx)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bind_coll() {
|
||||||
|
let vx = edn::PlainSymbol::new("?x");
|
||||||
|
assert_edn_parses_to!(|| vector().of_exactly(Bind::binding()),
|
||||||
|
"[[?x ...]]",
|
||||||
|
Binding::BindColl(variable(vx)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bind_rel() {
|
||||||
|
let vx = edn::PlainSymbol::new("?x");
|
||||||
|
let vy = edn::PlainSymbol::new("?y");
|
||||||
|
let vw = edn::PlainSymbol::new("?w");
|
||||||
|
assert_edn_parses_to!(|| vector().of_exactly(Bind::binding()),
|
||||||
|
"[[?x ?y _ ?w]]",
|
||||||
|
Binding::BindRel(vec![VariableOrPlaceholder::Variable(variable(vx)),
|
||||||
|
VariableOrPlaceholder::Variable(variable(vy)),
|
||||||
|
VariableOrPlaceholder::Placeholder,
|
||||||
|
VariableOrPlaceholder::Variable(variable(vw)),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bind_tuple() {
|
||||||
|
let vx = edn::PlainSymbol::new("?x");
|
||||||
|
let vy = edn::PlainSymbol::new("?y");
|
||||||
|
let vw = edn::PlainSymbol::new("?w");
|
||||||
|
assert_edn_parses_to!(|| vector().of_exactly(Bind::binding()),
|
||||||
|
"[?x ?y _ ?w]",
|
||||||
|
Binding::BindTuple(vec![VariableOrPlaceholder::Variable(variable(vx)),
|
||||||
|
VariableOrPlaceholder::Variable(variable(vy)),
|
||||||
|
VariableOrPlaceholder::Placeholder,
|
||||||
|
VariableOrPlaceholder::Variable(variable(vw)),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_where_fn() {
|
||||||
|
assert_edn_parses_to!(Where::where_fn,
|
||||||
|
"[(f ?x 1) ?y]",
|
||||||
|
WhereClause::WhereFn(WhereFn {
|
||||||
|
operator: edn::PlainSymbol::new("f"),
|
||||||
|
args: vec![FnArg::Variable(Variable::from_valid_name("?x")),
|
||||||
|
FnArg::EntidOrInteger(1)],
|
||||||
|
binding: Binding::BindScalar(Variable::from_valid_name("?y")),
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_edn_parses_to!(Where::where_fn,
|
||||||
|
"[(f ?x) [?y ...]]",
|
||||||
|
WhereClause::WhereFn(WhereFn {
|
||||||
|
operator: edn::PlainSymbol::new("f"),
|
||||||
|
args: vec![FnArg::Variable(Variable::from_valid_name("?x"))],
|
||||||
|
binding: Binding::BindColl(Variable::from_valid_name("?y")),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// assert_edn_parses_to!(Where::where_fn,
|
||||||
|
// "[(f) [?y _]]",
|
||||||
|
// WhereClause::WhereFn(WhereFn {
|
||||||
|
// operator: edn::PlainSymbol::new("f"),
|
||||||
|
// args: vec![],
|
||||||
|
// binding: Binding::BindRel(vec![VariableOrPlaceholder::Variable(Variable::from_valid_name("?y")),
|
||||||
|
// VariableOrPlaceholder::Placeholder]),
|
||||||
|
// }));
|
||||||
|
|
||||||
|
assert_edn_parses_to!(Where::where_fn,
|
||||||
|
"[(f) _ ?y]",
|
||||||
|
WhereClause::WhereFn(WhereFn {
|
||||||
|
operator: edn::PlainSymbol::new("f"),
|
||||||
|
args: vec![],
|
||||||
|
binding: Binding::BindTuple(vec![VariableOrPlaceholder::Placeholder,
|
||||||
|
VariableOrPlaceholder::Variable(Variable::from_valid_name("?y"))]),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -210,7 +210,7 @@ fn project_elements<'a, I: IntoIterator<Item = &'a Element>>(
|
||||||
let columns = query.cc
|
let columns = query.cc
|
||||||
.column_bindings
|
.column_bindings
|
||||||
.get(var)
|
.get(var)
|
||||||
.expect("Every variable has a binding");
|
.expect(format!("Every variable should have a binding, but {} does not", var.as_str()).as_str());
|
||||||
|
|
||||||
let qa = columns[0].clone();
|
let qa = columns[0].clone();
|
||||||
let name = column_name(var);
|
let name = column_name(var);
|
||||||
|
|
|
@ -20,6 +20,8 @@ use mentat_core::{
|
||||||
|
|
||||||
use mentat_query_algebrizer::{
|
use mentat_query_algebrizer::{
|
||||||
DatomsColumn,
|
DatomsColumn,
|
||||||
|
FulltextColumn,
|
||||||
|
FulltextQualifiedAlias,
|
||||||
QualifiedAlias,
|
QualifiedAlias,
|
||||||
QueryValue,
|
QueryValue,
|
||||||
SourceAlias,
|
SourceAlias,
|
||||||
|
@ -44,6 +46,7 @@ use mentat_sql::{
|
||||||
/// implementation for each storage backend. Passing `TypedValue`s here allows for that.
|
/// implementation for each storage backend. Passing `TypedValue`s here allows for that.
|
||||||
pub enum ColumnOrExpression {
|
pub enum ColumnOrExpression {
|
||||||
Column(QualifiedAlias),
|
Column(QualifiedAlias),
|
||||||
|
FulltextColumn(FulltextQualifiedAlias),
|
||||||
Entid(Entid), // Because it's so common.
|
Entid(Entid), // Because it's so common.
|
||||||
Integer(i32), // We use these for type codes etc.
|
Integer(i32), // We use these for type codes etc.
|
||||||
Long(i64),
|
Long(i64),
|
||||||
|
@ -55,6 +58,7 @@ impl From<QueryValue> for ColumnOrExpression {
|
||||||
fn from(v: QueryValue) -> Self {
|
fn from(v: QueryValue) -> Self {
|
||||||
match v {
|
match v {
|
||||||
QueryValue::Column(c) => ColumnOrExpression::Column(c),
|
QueryValue::Column(c) => ColumnOrExpression::Column(c),
|
||||||
|
QueryValue::FulltextColumn(c) => ColumnOrExpression::FulltextColumn(c),
|
||||||
QueryValue::Entid(e) => ColumnOrExpression::Entid(e),
|
QueryValue::Entid(e) => ColumnOrExpression::Entid(e),
|
||||||
QueryValue::PrimitiveLong(v) => ColumnOrExpression::Long(v),
|
QueryValue::PrimitiveLong(v) => ColumnOrExpression::Long(v),
|
||||||
QueryValue::TypedValue(v) => ColumnOrExpression::Value(v),
|
QueryValue::TypedValue(v) => ColumnOrExpression::Value(v),
|
||||||
|
@ -109,6 +113,14 @@ impl Constraint {
|
||||||
right: right,
|
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)]
|
#[allow(dead_code)]
|
||||||
|
@ -157,6 +169,11 @@ fn push_column(qb: &mut QueryBuilder, col: &DatomsColumn) {
|
||||||
qb.push_sql(col.as_str());
|
qb.push_sql(col.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We know that FulltextColumns are safe to serialize.
|
||||||
|
fn push_fulltext_column(qb: &mut QueryBuilder, col: &FulltextColumn) {
|
||||||
|
qb.push_sql(col.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------
|
//---------------------------------------------------------
|
||||||
// Turn that representation into SQL.
|
// Turn that representation into SQL.
|
||||||
|
|
||||||
|
@ -199,6 +216,12 @@ impl QueryFragment for ColumnOrExpression {
|
||||||
push_column(out, column);
|
push_column(out, column);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
&FulltextColumn(FulltextQualifiedAlias(ref table, ref column)) => {
|
||||||
|
out.push_identifier(table.as_str())?;
|
||||||
|
out.push_sql(".");
|
||||||
|
push_fulltext_column(out, column);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
&Entid(entid) => {
|
&Entid(entid) => {
|
||||||
out.push_sql(entid.to_string().as_str());
|
out.push_sql(entid.to_string().as_str());
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -406,13 +429,20 @@ impl SelectQuery {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use mentat_query_algebrizer::DatomsTable;
|
use mentat_query_algebrizer::DatomsTable;
|
||||||
|
|
||||||
fn build_constraint(c: Constraint) -> String {
|
fn build_constraint_query(c: Constraint) -> SQLQuery {
|
||||||
let mut builder = SQLiteQueryBuilder::new();
|
let mut builder = SQLiteQueryBuilder::new();
|
||||||
c.push_sql(&mut builder)
|
c.push_sql(&mut builder)
|
||||||
.map(|_| builder.finish())
|
.map(|_| builder.finish())
|
||||||
.unwrap().sql
|
.expect("to produce a query for the given constraint")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_constraint(c: Constraint) -> String {
|
||||||
|
build_constraint_query(c).sql
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -469,6 +499,25 @@ mod tests {
|
||||||
assert_eq!("((123 = 456 AND 789 = 246))", build_constraint(c));
|
assert_eq!("((123 = 456 AND 789 = 246))", build_constraint(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matches_constraint() {
|
||||||
|
let c = Constraint::Infix {
|
||||||
|
op: Op("MATCHES"),
|
||||||
|
left: ColumnOrExpression::FulltextColumn(FulltextQualifiedAlias("fulltext01".to_string(), FulltextColumn::Text)),
|
||||||
|
right: ColumnOrExpression::Value(TypedValue::String(Rc::new("needle".to_string()))),
|
||||||
|
};
|
||||||
|
let q = build_constraint_query(c);
|
||||||
|
assert_eq!("`fulltext01`.text MATCHES $v0", q.sql);
|
||||||
|
assert_eq!(vec![("$v0".to_string(), Rc::new("needle".to_string()))], q.args);
|
||||||
|
|
||||||
|
let c = Constraint::Infix {
|
||||||
|
op: Op("="),
|
||||||
|
left: ColumnOrExpression::FulltextColumn(FulltextQualifiedAlias("fulltext01".to_string(), FulltextColumn::Rowid)),
|
||||||
|
right: ColumnOrExpression::Column(QualifiedAlias("datoms02".to_string(), DatomsColumn::Value)),
|
||||||
|
};
|
||||||
|
assert_eq!("`fulltext01`.rowid = `datoms02`.v", build_constraint(c));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_end_to_end() {
|
fn test_end_to_end() {
|
||||||
// [:find ?x :where [?x 65537 ?v] [?x 65536 ?v]]
|
// [:find ?x :where [?x 65537 ?v] [?x 65536 ?v]]
|
||||||
|
|
|
@ -32,6 +32,8 @@ use mentat_query_algebrizer::{
|
||||||
ConjoiningClauses,
|
ConjoiningClauses,
|
||||||
DatomsColumn,
|
DatomsColumn,
|
||||||
DatomsTable,
|
DatomsTable,
|
||||||
|
FulltextColumn,
|
||||||
|
FulltextQualifiedAlias,
|
||||||
QualifiedAlias,
|
QualifiedAlias,
|
||||||
QueryValue,
|
QueryValue,
|
||||||
SourceAlias,
|
SourceAlias,
|
||||||
|
@ -69,6 +71,12 @@ impl ToColumn for QualifiedAlias {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToColumn for FulltextQualifiedAlias {
|
||||||
|
fn to_column(self) -> ColumnOrExpression {
|
||||||
|
ColumnOrExpression::FulltextColumn(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ToConstraint for ColumnIntersection {
|
impl ToConstraint for ColumnIntersection {
|
||||||
fn to_constraint(self) -> Constraint {
|
fn to_constraint(self) -> Constraint {
|
||||||
Constraint::And {
|
Constraint::And {
|
||||||
|
@ -108,6 +116,11 @@ impl ToConstraint for ColumnConstraint {
|
||||||
Equals(left, QueryValue::Column(right)) =>
|
Equals(left, QueryValue::Column(right)) =>
|
||||||
Constraint::equal(left.to_column(), right.to_column()),
|
Constraint::equal(left.to_column(), right.to_column()),
|
||||||
|
|
||||||
|
Equals(left, QueryValue::FulltextColumn(right)) =>
|
||||||
|
// TODO: figure out if this is the correct abstraction. Can we make it so that
|
||||||
|
// FulltextColumns::Text is not accepted here?
|
||||||
|
Constraint::equal(left.to_column(), right.to_column()),
|
||||||
|
|
||||||
Equals(qa, QueryValue::PrimitiveLong(value)) => {
|
Equals(qa, QueryValue::PrimitiveLong(value)) => {
|
||||||
let tag_column = qa.for_type_tag().to_column();
|
let tag_column = qa.for_type_tag().to_column();
|
||||||
let value_column = qa.to_column();
|
let value_column = qa.to_column();
|
||||||
|
@ -148,6 +161,14 @@ impl ToConstraint for ColumnConstraint {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Matches(left, right) => {
|
||||||
|
Constraint::Infix {
|
||||||
|
op: Op("MATCH"),
|
||||||
|
left: ColumnOrExpression::FulltextColumn(left),
|
||||||
|
right: right.into(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
HasType(table, value_type) => {
|
HasType(table, value_type) => {
|
||||||
let column = QualifiedAlias(table, DatomsColumn::ValueTypeTag).to_column();
|
let column = QualifiedAlias(table, DatomsColumn::ValueTypeTag).to_column();
|
||||||
Constraint::equal(column, ColumnOrExpression::Integer(value_type.value_type_tag()))
|
Constraint::equal(column, ColumnOrExpression::Integer(value_type.value_type_tag()))
|
||||||
|
|
|
@ -58,6 +58,13 @@ fn prepopulated_schema() -> Schema {
|
||||||
value_type: ValueType::String,
|
value_type: ValueType::String,
|
||||||
..Default::default()
|
..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
|
schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,3 +249,13 @@ fn test_numeric_not_equals_known_attribute() {
|
||||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v <> 12 LIMIT 1");
|
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![]);
|
assert_eq!(args, vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fulltext() {
|
||||||
|
let schema = prepopulated_schema();
|
||||||
|
|
||||||
|
let input = r#"[:find ?entity ?value ?tx ?score :where [(fulltext $ :foo/fts "needle") [?entity ?value ?tx ?score]]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, input, None);
|
||||||
|
assert_eq!(sql, "SELECT `datoms00`.e AS `?entity`, `datoms00`.v AS `?value`, `datoms00`.tx AS `?tx`, 0.0 AS `?score` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 1");
|
||||||
|
assert_eq!(args, vec![make_arg("$v0", "needle")]);
|
||||||
|
}
|
||||||
|
|
|
@ -147,7 +147,11 @@ impl FromValue<SrcVar> for SrcVar {
|
||||||
impl SrcVar {
|
impl SrcVar {
|
||||||
pub fn from_symbol(sym: &PlainSymbol) -> Option<SrcVar> {
|
pub fn from_symbol(sym: &PlainSymbol) -> Option<SrcVar> {
|
||||||
if sym.is_src_symbol() {
|
if sym.is_src_symbol() {
|
||||||
|
if sym.0 == "$" {
|
||||||
|
Some(SrcVar::DefaultSrc)
|
||||||
|
} else {
|
||||||
Some(SrcVar::NamedSrc(sym.plain_name().to_string()))
|
Some(SrcVar::NamedSrc(sym.plain_name().to_string()))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -185,16 +189,34 @@ pub enum FnArg {
|
||||||
|
|
||||||
impl FromValue<FnArg> for FnArg {
|
impl FromValue<FnArg> for FnArg {
|
||||||
fn from_value(v: edn::ValueAndSpan) -> Option<FnArg> {
|
fn from_value(v: edn::ValueAndSpan) -> Option<FnArg> {
|
||||||
// TODO: support SrcVars.
|
use edn::SpannedValue::*;
|
||||||
Variable::from_value(v.clone()) // TODO: don't clone!
|
|
||||||
.and_then(|v| Some(FnArg::Variable(v)))
|
|
||||||
.or_else(|| {
|
|
||||||
println!("from_value {}", v.inner);
|
|
||||||
match v.inner {
|
match v.inner {
|
||||||
edn::SpannedValue::Integer(i) => Some(FnArg::EntidOrInteger(i)),
|
Integer(x) =>
|
||||||
edn::SpannedValue::Float(f) => Some(FnArg::Constant(NonIntegerConstant::Float(f))),
|
Some(FnArg::EntidOrInteger(x)),
|
||||||
_ => unimplemented!(),
|
PlainSymbol(ref x) if x.is_src_symbol() =>
|
||||||
}})
|
SrcVar::from_symbol(x).map(FnArg::SrcVar),
|
||||||
|
PlainSymbol(ref x) if x.is_var_symbol() =>
|
||||||
|
Variable::from_symbol(x).map(FnArg::Variable),
|
||||||
|
PlainSymbol(_) => None,
|
||||||
|
NamespacedKeyword(ref x) =>
|
||||||
|
Some(FnArg::Ident(x.clone())),
|
||||||
|
Boolean(x) =>
|
||||||
|
Some(FnArg::Constant(NonIntegerConstant::Boolean(x))),
|
||||||
|
Float(x) =>
|
||||||
|
Some(FnArg::Constant(NonIntegerConstant::Float(x))),
|
||||||
|
BigInteger(ref x) =>
|
||||||
|
Some(FnArg::Constant(NonIntegerConstant::BigInteger(x.clone()))),
|
||||||
|
Text(ref x) =>
|
||||||
|
// TODO: intern strings. #398.
|
||||||
|
Some(FnArg::Constant(NonIntegerConstant::Text(Rc::new(x.clone())))),
|
||||||
|
Nil |
|
||||||
|
NamespacedSymbol(_) |
|
||||||
|
Keyword(_) |
|
||||||
|
Vector(_) |
|
||||||
|
List(_) |
|
||||||
|
Set(_) |
|
||||||
|
Map(_) => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -455,6 +477,25 @@ impl FindSpec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Datomic accepts variable or placeholder. DataScript accepts recursive bindings. Mentat sticks
|
||||||
|
// to the non-recursive form Datomic accepts, which is much simpler to process.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VariableOrPlaceholder {
|
||||||
|
Placeholder,
|
||||||
|
Variable(Variable),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone,Debug,Eq,PartialEq)]
|
||||||
|
pub enum Binding {
|
||||||
|
BindRel(Vec<VariableOrPlaceholder>),
|
||||||
|
|
||||||
|
BindColl(Variable),
|
||||||
|
|
||||||
|
BindTuple(Vec<VariableOrPlaceholder>),
|
||||||
|
|
||||||
|
BindScalar(Variable),
|
||||||
|
}
|
||||||
|
|
||||||
// Note that the "implicit blank" rule applies.
|
// Note that the "implicit blank" rule applies.
|
||||||
// A pattern with a reversed attribute — :foo/_bar — is reversed
|
// A pattern with a reversed attribute — :foo/_bar — is reversed
|
||||||
// at the point of parsing. These `Pattern` instances only represent
|
// at the point of parsing. These `Pattern` instances only represent
|
||||||
|
@ -510,6 +551,13 @@ pub struct Predicate {
|
||||||
pub args: Vec<FnArg>,
|
pub args: Vec<FnArg>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WhereFn {
|
||||||
|
pub operator: PlainSymbol,
|
||||||
|
pub args: Vec<FnArg>,
|
||||||
|
pub binding: Binding,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum UnifyVars {
|
pub enum UnifyVars {
|
||||||
/// `Implicit` means the variables in an `or` or `not` are derived from the enclosed pattern.
|
/// `Implicit` means the variables in an `or` or `not` are derived from the enclosed pattern.
|
||||||
|
@ -570,14 +618,19 @@ pub struct OrJoin {
|
||||||
pub clauses: Vec<OrWhereClause>,
|
pub clauses: Vec<OrWhereClause>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct NotJoin {
|
||||||
|
pub unify_vars: UnifyVars,
|
||||||
|
pub clauses: Vec<WhereClause>,
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum WhereClause {
|
pub enum WhereClause {
|
||||||
Not,
|
NotJoin(NotJoin),
|
||||||
NotJoin,
|
|
||||||
OrJoin(OrJoin),
|
OrJoin(OrJoin),
|
||||||
Pred(Predicate),
|
Pred(Predicate),
|
||||||
WhereFn,
|
WhereFn(WhereFn),
|
||||||
RuleExpr,
|
RuleExpr,
|
||||||
Pattern(Pattern),
|
Pattern(Pattern),
|
||||||
}
|
}
|
||||||
|
@ -628,9 +681,8 @@ impl ContainsVariables for WhereClause {
|
||||||
&OrJoin(ref o) => o.accumulate_mentioned_variables(acc),
|
&OrJoin(ref o) => o.accumulate_mentioned_variables(acc),
|
||||||
&Pred(ref p) => p.accumulate_mentioned_variables(acc),
|
&Pred(ref p) => p.accumulate_mentioned_variables(acc),
|
||||||
&Pattern(ref p) => p.accumulate_mentioned_variables(acc),
|
&Pattern(ref p) => p.accumulate_mentioned_variables(acc),
|
||||||
&Not => (),
|
&NotJoin(ref n) => n.accumulate_mentioned_variables(acc),
|
||||||
&NotJoin => (),
|
&WhereFn(_) => (),
|
||||||
&WhereFn => (),
|
|
||||||
&RuleExpr => (),
|
&RuleExpr => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -654,6 +706,14 @@ impl ContainsVariables for OrJoin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ContainsVariables for NotJoin {
|
||||||
|
fn accumulate_mentioned_variables(&self, acc: &mut BTreeSet<Variable>) {
|
||||||
|
for clause in &self.clauses {
|
||||||
|
clause.accumulate_mentioned_variables(acc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ContainsVariables for Predicate {
|
impl ContainsVariables for Predicate {
|
||||||
fn accumulate_mentioned_variables(&self, acc: &mut BTreeSet<Variable>) {
|
fn accumulate_mentioned_variables(&self, acc: &mut BTreeSet<Variable>) {
|
||||||
for arg in &self.args {
|
for arg in &self.args {
|
||||||
|
|
Loading…
Reference in a new issue