Implement MATCHES throughout SQL machinery.
This commit is contained in:
parent
17c59bbff6
commit
565a0e9ff9
7 changed files with 116 additions and 3 deletions
|
@ -54,6 +54,7 @@ use types::{
|
||||||
DatomsColumn,
|
DatomsColumn,
|
||||||
DatomsTable,
|
DatomsTable,
|
||||||
EmptyBecause,
|
EmptyBecause,
|
||||||
|
FulltextColumn,
|
||||||
QualifiedAlias,
|
QualifiedAlias,
|
||||||
QueryValue,
|
QueryValue,
|
||||||
SourceAlias,
|
SourceAlias,
|
||||||
|
@ -398,6 +399,13 @@ impl ConjoiningClauses {
|
||||||
self.constrain_column_to_constant(table, column, bound_val);
|
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) => {
|
Column::Fixed(DatomsColumn::ValueTypeTag) => {
|
||||||
// I'm pretty sure this is meaningless right now, because we will never bind
|
// 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.
|
// a type tag to a variable -- there's no syntax for doing so.
|
||||||
|
|
|
@ -22,6 +22,14 @@ use self::mentat_query::{
|
||||||
pub enum BindingError {
|
pub enum BindingError {
|
||||||
NoBoundVariable,
|
NoBoundVariable,
|
||||||
RepeatedBoundVariable, // TODO: include repeated variable(s).
|
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! {
|
error_chain! {
|
||||||
|
|
|
@ -217,6 +217,7 @@ pub use types::{
|
||||||
ComputedTable,
|
ComputedTable,
|
||||||
DatomsColumn,
|
DatomsColumn,
|
||||||
DatomsTable,
|
DatomsTable,
|
||||||
|
FulltextColumn,
|
||||||
OrderBy,
|
OrderBy,
|
||||||
QualifiedAlias,
|
QualifiedAlias,
|
||||||
QueryValue,
|
QueryValue,
|
||||||
|
|
|
@ -85,6 +85,13 @@ pub enum DatomsColumn {
|
||||||
ValueTypeTag,
|
ValueTypeTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One of the named columns of our fulltext values table.
|
||||||
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
|
pub enum FulltextColumn {
|
||||||
|
Rowid,
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone)]
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
pub enum VariableColumn {
|
pub enum VariableColumn {
|
||||||
Variable(Variable),
|
Variable(Variable),
|
||||||
|
@ -94,6 +101,7 @@ pub enum VariableColumn {
|
||||||
#[derive(PartialEq, Eq, Clone)]
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
pub enum Column {
|
pub enum Column {
|
||||||
Fixed(DatomsColumn),
|
Fixed(DatomsColumn),
|
||||||
|
Fulltext(FulltextColumn),
|
||||||
Variable(VariableColumn),
|
Variable(VariableColumn),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,11 +165,34 @@ impl Debug for Column {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
match self {
|
match self {
|
||||||
&Column::Fixed(ref c) => c.fmt(f),
|
&Column::Fixed(ref c) => c.fmt(f),
|
||||||
|
&Column::Fulltext(ref c) => c.fmt(f),
|
||||||
&Column::Variable(ref v) => v.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".
|
/// A specific instance of a table within a query. E.g., "datoms123".
|
||||||
pub type TableAlias = String;
|
pub type TableAlias = String;
|
||||||
|
|
||||||
|
@ -301,6 +332,9 @@ pub enum ColumnConstraint {
|
||||||
},
|
},
|
||||||
HasType(TableAlias, ValueType),
|
HasType(TableAlias, ValueType),
|
||||||
NotExists(ComputedTable),
|
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)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
|
@ -411,6 +445,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)
|
||||||
},
|
},
|
||||||
|
@ -426,6 +464,7 @@ pub enum EmptyBecause {
|
||||||
ConflictingBindings { var: Variable, existing: TypedValue, desired: TypedValue },
|
ConflictingBindings { var: Variable, existing: TypedValue, desired: TypedValue },
|
||||||
TypeMismatch { var: Variable, existing: ValueTypeSet, desired: ValueTypeSet },
|
TypeMismatch { var: Variable, existing: ValueTypeSet, desired: ValueTypeSet },
|
||||||
NoValidTypes(Variable),
|
NoValidTypes(Variable),
|
||||||
|
NonAttributeArgument,
|
||||||
NonNumericArgument,
|
NonNumericArgument,
|
||||||
NonStringFulltextValue,
|
NonStringFulltextValue,
|
||||||
UnresolvedIdent(NamespacedKeyword),
|
UnresolvedIdent(NamespacedKeyword),
|
||||||
|
@ -451,6 +490,9 @@ impl Debug for EmptyBecause {
|
||||||
&NoValidTypes(ref var) => {
|
&NoValidTypes(ref var) => {
|
||||||
write!(f, "Type mismatch: {:?} has no valid types", var)
|
write!(f, "Type mismatch: {:?} has no valid types", var)
|
||||||
},
|
},
|
||||||
|
&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")
|
||||||
},
|
},
|
||||||
|
|
|
@ -124,6 +124,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)]
|
||||||
|
@ -198,6 +206,10 @@ fn push_column(qb: &mut QueryBuilder, col: &Column) -> BuildQueryResult {
|
||||||
qb.push_sql(d.as_str());
|
qb.push_sql(d.as_str());
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
&Column::Fulltext(ref d) => {
|
||||||
|
qb.push_sql(d.as_str());
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
&Column::Variable(ref vc) => push_variable_column(qb, vc),
|
&Column::Variable(ref vc) => push_variable_column(qb, vc),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -555,22 +567,30 @@ impl SelectQuery {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use mentat_query_algebrizer::{
|
use mentat_query_algebrizer::{
|
||||||
|
Column,
|
||||||
DatomsColumn,
|
DatomsColumn,
|
||||||
DatomsTable,
|
DatomsTable,
|
||||||
|
FulltextColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn build(c: &QueryFragment) -> String {
|
fn build_query(c: &QueryFragment) -> 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(c: &QueryFragment) -> String {
|
||||||
|
build_query(c).sql
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_in_constraint() {
|
fn test_in_constraint() {
|
||||||
let none = Constraint::In {
|
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![],
|
list: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -651,6 +671,25 @@ mod tests {
|
||||||
"SELECT 0 AS `?a`, 0 AS `?b` WHERE 0 UNION ALL VALUES (0, 1), (1, 2)");
|
"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]
|
#[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]]
|
||||||
|
|
|
@ -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) => {
|
HasType(table, value_type) => {
|
||||||
let column = QualifiedAlias::new(table, DatomsColumn::ValueTypeTag).to_column();
|
let column = QualifiedAlias::new(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()))
|
||||||
|
|
|
@ -69,6 +69,13 @@ fn prepopulated_typed_schema(foo_type: ValueType) -> Schema {
|
||||||
value_type: foo_type,
|
value_type: foo_type,
|
||||||
..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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue