Extend inequalities to Instants. (#439) r=fluffyemily,nalexander
This commit is contained in:
parent
ea0e9d4c7b
commit
eaf3e7fc4b
13 changed files with 484 additions and 49 deletions
|
@ -51,6 +51,53 @@ macro_rules! coerce_to_typed_value {
|
||||||
} }
|
} }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ValueTypes {
|
||||||
|
fn potential_types(&self, schema: &Schema) -> Result<ValueTypeSet>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueTypes for FnArg {
|
||||||
|
fn potential_types(&self, schema: &Schema) -> Result<ValueTypeSet> {
|
||||||
|
Ok(match self {
|
||||||
|
&FnArg::EntidOrInteger(x) => {
|
||||||
|
if ValueType::Ref.accommodates_integer(x) {
|
||||||
|
// TODO: also see if it's a valid entid?
|
||||||
|
ValueTypeSet::of_longs()
|
||||||
|
} else {
|
||||||
|
ValueTypeSet::of_one(ValueType::Long)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
&FnArg::IdentOrKeyword(ref x) => {
|
||||||
|
if schema.get_entid(x).is_some() {
|
||||||
|
ValueTypeSet::of_keywords()
|
||||||
|
} else {
|
||||||
|
ValueTypeSet::of_one(ValueType::Keyword)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
&FnArg::Variable(_) => {
|
||||||
|
ValueTypeSet::any()
|
||||||
|
},
|
||||||
|
|
||||||
|
&FnArg::Constant(NonIntegerConstant::BigInteger(_)) => {
|
||||||
|
// Not yet implemented.
|
||||||
|
bail!(ErrorKind::UnsupportedArgument)
|
||||||
|
},
|
||||||
|
|
||||||
|
// These don't make sense here. TODO: split FnArg into scalar and non-scalar…
|
||||||
|
&FnArg::Vector(_) |
|
||||||
|
&FnArg::SrcVar(_) => bail!(ErrorKind::UnsupportedArgument),
|
||||||
|
|
||||||
|
// These are all straightforward.
|
||||||
|
&FnArg::Constant(NonIntegerConstant::Boolean(_)) => ValueTypeSet::of_one(ValueType::Boolean),
|
||||||
|
&FnArg::Constant(NonIntegerConstant::Instant(_)) => ValueTypeSet::of_one(ValueType::Instant),
|
||||||
|
&FnArg::Constant(NonIntegerConstant::Uuid(_)) => ValueTypeSet::of_one(ValueType::Uuid),
|
||||||
|
&FnArg::Constant(NonIntegerConstant::Float(_)) => ValueTypeSet::of_one(ValueType::Double),
|
||||||
|
&FnArg::Constant(NonIntegerConstant::Text(_)) => ValueTypeSet::of_one(ValueType::String),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum ValueConversion {
|
pub enum ValueConversion {
|
||||||
Val(TypedValue),
|
Val(TypedValue),
|
||||||
Impossible(EmptyBecause),
|
Impossible(EmptyBecause),
|
||||||
|
|
|
@ -109,7 +109,7 @@ mod testing {
|
||||||
ColumnIntersection,
|
ColumnIntersection,
|
||||||
DatomsColumn,
|
DatomsColumn,
|
||||||
DatomsTable,
|
DatomsTable,
|
||||||
NumericComparison,
|
Inequality,
|
||||||
QualifiedAlias,
|
QualifiedAlias,
|
||||||
QueryValue,
|
QueryValue,
|
||||||
SourceAlias,
|
SourceAlias,
|
||||||
|
@ -364,8 +364,8 @@ mod testing {
|
||||||
assert!(!cc.is_known_empty());
|
assert!(!cc.is_known_empty());
|
||||||
assert_eq!(cc.wheres, ColumnIntersection(vec![
|
assert_eq!(cc.wheres, ColumnIntersection(vec![
|
||||||
ColumnConstraintOrAlternation::Constraint(ColumnConstraint::Equals(d0a.clone(), age.clone())),
|
ColumnConstraintOrAlternation::Constraint(ColumnConstraint::Equals(d0a.clone(), age.clone())),
|
||||||
ColumnConstraintOrAlternation::Constraint(ColumnConstraint::NumericInequality {
|
ColumnConstraintOrAlternation::Constraint(ColumnConstraint::Inequality {
|
||||||
operator: NumericComparison::LessThan,
|
operator: Inequality::LessThan,
|
||||||
left: QueryValue::Column(d0v.clone()),
|
left: QueryValue::Column(d0v.clone()),
|
||||||
right: QueryValue::TypedValue(TypedValue::Long(30)),
|
right: QueryValue::TypedValue(TypedValue::Long(30)),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -751,7 +751,7 @@ mod testing {
|
||||||
ColumnConstraint,
|
ColumnConstraint,
|
||||||
DatomsColumn,
|
DatomsColumn,
|
||||||
DatomsTable,
|
DatomsTable,
|
||||||
NumericComparison,
|
Inequality,
|
||||||
QualifiedAlias,
|
QualifiedAlias,
|
||||||
QueryValue,
|
QueryValue,
|
||||||
SourceAlias,
|
SourceAlias,
|
||||||
|
@ -960,8 +960,8 @@ mod testing {
|
||||||
assert!(!cc.is_known_empty());
|
assert!(!cc.is_known_empty());
|
||||||
assert_eq!(cc.wheres, ColumnIntersection(vec![
|
assert_eq!(cc.wheres, ColumnIntersection(vec![
|
||||||
ColumnConstraintOrAlternation::Constraint(ColumnConstraint::Equals(d0a.clone(), age.clone())),
|
ColumnConstraintOrAlternation::Constraint(ColumnConstraint::Equals(d0a.clone(), age.clone())),
|
||||||
ColumnConstraintOrAlternation::Constraint(ColumnConstraint::NumericInequality {
|
ColumnConstraintOrAlternation::Constraint(ColumnConstraint::Inequality {
|
||||||
operator: NumericComparison::LessThan,
|
operator: Inequality::LessThan,
|
||||||
left: QueryValue::Column(d0v.clone()),
|
left: QueryValue::Column(d0v.clone()),
|
||||||
right: QueryValue::TypedValue(TypedValue::Long(30)),
|
right: QueryValue::TypedValue(TypedValue::Long(30)),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -10,14 +10,18 @@
|
||||||
|
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
Schema,
|
Schema,
|
||||||
|
ValueType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mentat_query::{
|
use mentat_query::{
|
||||||
|
FnArg,
|
||||||
Predicate,
|
Predicate,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clauses::ConjoiningClauses;
|
use clauses::ConjoiningClauses;
|
||||||
|
|
||||||
|
use clauses::convert::ValueTypes;
|
||||||
|
|
||||||
use errors::{
|
use errors::{
|
||||||
Result,
|
Result,
|
||||||
ErrorKind,
|
ErrorKind,
|
||||||
|
@ -25,7 +29,9 @@ use errors::{
|
||||||
|
|
||||||
use types::{
|
use types::{
|
||||||
ColumnConstraint,
|
ColumnConstraint,
|
||||||
NumericComparison,
|
EmptyBecause,
|
||||||
|
Inequality,
|
||||||
|
ValueTypeSet,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Application of predicates.
|
/// Application of predicates.
|
||||||
|
@ -39,19 +45,25 @@ impl ConjoiningClauses {
|
||||||
pub fn apply_predicate<'s>(&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) = Inequality::from_datalog_operator(predicate.operator.0.as_str()) {
|
||||||
self.apply_numeric_predicate(schema, op, predicate)
|
self.apply_inequality(schema, op, predicate)
|
||||||
} else {
|
} else {
|
||||||
bail!(ErrorKind::UnknownFunction(predicate.operator.clone()))
|
bail!(ErrorKind::UnknownFunction(predicate.operator.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn potential_types(&self, schema: &Schema, fn_arg: &FnArg) -> Result<ValueTypeSet> {
|
||||||
|
match fn_arg {
|
||||||
|
&FnArg::Variable(ref v) => Ok(self.known_type_set(v)),
|
||||||
|
_ => fn_arg.potential_types(schema),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// This function:
|
/// This function:
|
||||||
/// - Resolves variables and converts types to those more amenable to SQL.
|
/// - Resolves variables and converts types to those more amenable to SQL.
|
||||||
/// - 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 an `Inequality` constraint into the `wheres` list.
|
||||||
#[allow(unused_variables)]
|
pub fn apply_inequality<'s>(&mut self, schema: &'s Schema, comparison: Inequality, 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));
|
||||||
}
|
}
|
||||||
|
@ -60,21 +72,74 @@ impl ConjoiningClauses {
|
||||||
// Any variables that aren't bound by this point in the linear processing of clauses will
|
// Any variables that aren't bound by this point in the linear processing of clauses will
|
||||||
// cause the application of the predicate to fail.
|
// cause the application of the predicate to fail.
|
||||||
let mut args = predicate.args.into_iter();
|
let mut args = predicate.args.into_iter();
|
||||||
let left = self.resolve_numeric_argument(&predicate.operator, 0, args.next().unwrap())?;
|
let left = args.next().expect("two args");
|
||||||
let right = self.resolve_numeric_argument(&predicate.operator, 1, args.next().unwrap())?;
|
let right = args.next().expect("two args");
|
||||||
|
|
||||||
// These arguments must be variables or numeric constants.
|
|
||||||
// TODO: generalize argument resolution and validation for different kinds of predicates:
|
|
||||||
// as we process `(< ?x 5)` we are able to check or deduce that `?x` is numeric, and either
|
|
||||||
// simplify the pattern or optimize the rest of the query.
|
|
||||||
// To do so needs a slightly more sophisticated representation of type constraints — a set,
|
|
||||||
// not a single `Option`.
|
|
||||||
|
|
||||||
|
// The types we're handling here must be the intersection of the possible types of the arguments,
|
||||||
|
// the known types of any variables, and the types supported by our inequality operators.
|
||||||
|
let supported_types = comparison.supported_types();
|
||||||
|
let mut left_types = self.potential_types(schema, &left)?
|
||||||
|
.intersection(&supported_types);
|
||||||
|
if left_types.is_empty() {
|
||||||
|
bail!(ErrorKind::InvalidArgument(predicate.operator.clone(), "numeric or instant", 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut right_types = self.potential_types(schema, &right)?
|
||||||
|
.intersection(&supported_types);
|
||||||
|
if right_types.is_empty() {
|
||||||
|
bail!(ErrorKind::InvalidArgument(predicate.operator.clone(), "numeric or instant", 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We would like to allow longs to compare to doubles.
|
||||||
|
// Do this by expanding the type sets. `resolve_numeric_argument` will
|
||||||
|
// use `Long` by preference.
|
||||||
|
if right_types.contains(ValueType::Long) {
|
||||||
|
right_types.insert(ValueType::Double);
|
||||||
|
}
|
||||||
|
if left_types.contains(ValueType::Long) {
|
||||||
|
left_types.insert(ValueType::Double);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shared_types = left_types.intersection(&right_types);
|
||||||
|
if shared_types.is_empty() {
|
||||||
|
// In isolation these are both valid inputs to the operator, but the query cannot
|
||||||
|
// succeed because the types don't match.
|
||||||
|
self.mark_known_empty(
|
||||||
|
if let Some(var) = left.as_variable().or_else(|| right.as_variable()) {
|
||||||
|
EmptyBecause::TypeMismatch {
|
||||||
|
var: var.clone(),
|
||||||
|
existing: left_types,
|
||||||
|
desired: right_types,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EmptyBecause::KnownTypeMismatch {
|
||||||
|
left: left_types,
|
||||||
|
right: right_types,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect the intersection to be Long, Long+Double, Double, or Instant.
|
||||||
|
let left_v;
|
||||||
|
let right_v;
|
||||||
|
if shared_types == ValueTypeSet::of_one(ValueType::Instant) {
|
||||||
|
left_v = self.resolve_instant_argument(&predicate.operator, 0, left)?;
|
||||||
|
right_v = self.resolve_instant_argument(&predicate.operator, 1, right)?;
|
||||||
|
} else if !shared_types.is_empty() && shared_types.is_subset(&ValueTypeSet::of_numeric_types()) {
|
||||||
|
left_v = self.resolve_numeric_argument(&predicate.operator, 0, left)?;
|
||||||
|
right_v = self.resolve_numeric_argument(&predicate.operator, 1, right)?;
|
||||||
|
} else {
|
||||||
|
bail!(ErrorKind::InvalidArgument(predicate.operator.clone(), "numeric or instant", 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// These arguments must be variables or instant/numeric constants.
|
||||||
// TODO: static evaluation. #383.
|
// TODO: static evaluation. #383.
|
||||||
let constraint = ColumnConstraint::NumericInequality {
|
let constraint = ColumnConstraint::Inequality {
|
||||||
operator: comparison,
|
operator: comparison,
|
||||||
left: left,
|
left: left_v,
|
||||||
right: right,
|
right: right_v,
|
||||||
};
|
};
|
||||||
self.wheres.add_intersection(constraint);
|
self.wheres.add_intersection(constraint);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -119,7 +184,7 @@ mod testing {
|
||||||
/// Apply two patterns: a pattern and a numeric predicate.
|
/// Apply two patterns: a pattern and a numeric predicate.
|
||||||
/// Verify that after application of the predicate we know that the value
|
/// Verify that after application of the predicate we know that the value
|
||||||
/// must be numeric.
|
/// must be numeric.
|
||||||
fn test_apply_numeric_predicate() {
|
fn test_apply_inequality() {
|
||||||
let mut cc = ConjoiningClauses::default();
|
let mut cc = ConjoiningClauses::default();
|
||||||
let mut schema = Schema::default();
|
let mut schema = Schema::default();
|
||||||
|
|
||||||
|
@ -141,8 +206,8 @@ mod testing {
|
||||||
assert!(!cc.is_known_empty());
|
assert!(!cc.is_known_empty());
|
||||||
|
|
||||||
let op = PlainSymbol::new("<");
|
let op = PlainSymbol::new("<");
|
||||||
let comp = NumericComparison::from_datalog_operator(op.plain_name()).unwrap();
|
let comp = Inequality::from_datalog_operator(op.plain_name()).unwrap();
|
||||||
assert!(cc.apply_numeric_predicate(&schema, comp, Predicate {
|
assert!(cc.apply_inequality(&schema, comp, Predicate {
|
||||||
operator: op,
|
operator: op,
|
||||||
args: vec![
|
args: vec![
|
||||||
FnArg::Variable(Variable::from_valid_name("?y")), FnArg::EntidOrInteger(10),
|
FnArg::Variable(Variable::from_valid_name("?y")), FnArg::EntidOrInteger(10),
|
||||||
|
@ -162,8 +227,8 @@ mod testing {
|
||||||
|
|
||||||
let clauses = cc.wheres;
|
let clauses = cc.wheres;
|
||||||
assert_eq!(clauses.len(), 1);
|
assert_eq!(clauses.len(), 1);
|
||||||
assert_eq!(clauses.0[0], ColumnConstraint::NumericInequality {
|
assert_eq!(clauses.0[0], ColumnConstraint::Inequality {
|
||||||
operator: NumericComparison::LessThan,
|
operator: Inequality::LessThan,
|
||||||
left: QueryValue::Column(cc.column_bindings.get(&y).unwrap()[0].clone()),
|
left: QueryValue::Column(cc.column_bindings.get(&y).unwrap()[0].clone()),
|
||||||
right: QueryValue::TypedValue(TypedValue::Long(10)),
|
right: QueryValue::TypedValue(TypedValue::Long(10)),
|
||||||
}.into());
|
}.into());
|
||||||
|
@ -201,8 +266,8 @@ mod testing {
|
||||||
assert!(!cc.is_known_empty());
|
assert!(!cc.is_known_empty());
|
||||||
|
|
||||||
let op = PlainSymbol::new(">=");
|
let op = PlainSymbol::new(">=");
|
||||||
let comp = NumericComparison::from_datalog_operator(op.plain_name()).unwrap();
|
let comp = Inequality::from_datalog_operator(op.plain_name()).unwrap();
|
||||||
assert!(cc.apply_numeric_predicate(&schema, comp, Predicate {
|
assert!(cc.apply_inequality(&schema, comp, Predicate {
|
||||||
operator: op,
|
operator: op,
|
||||||
args: vec![
|
args: vec![
|
||||||
FnArg::Variable(Variable::from_valid_name("?y")), FnArg::EntidOrInteger(10),
|
FnArg::Variable(Variable::from_valid_name("?y")), FnArg::EntidOrInteger(10),
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
TypedValue,
|
TypedValue,
|
||||||
|
ValueType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mentat_query::{
|
use mentat_query::{
|
||||||
|
@ -55,7 +56,7 @@ impl ConjoiningClauses {
|
||||||
Constant(NonIntegerConstant::Boolean(_)) |
|
Constant(NonIntegerConstant::Boolean(_)) |
|
||||||
Constant(NonIntegerConstant::Text(_)) |
|
Constant(NonIntegerConstant::Text(_)) |
|
||||||
Constant(NonIntegerConstant::Uuid(_)) |
|
Constant(NonIntegerConstant::Uuid(_)) |
|
||||||
Constant(NonIntegerConstant::Instant(_)) | // Instants are covered elsewhere.
|
Constant(NonIntegerConstant::Instant(_)) | // Instants are covered below.
|
||||||
Constant(NonIntegerConstant::BigInteger(_)) |
|
Constant(NonIntegerConstant::BigInteger(_)) |
|
||||||
Vector(_) => {
|
Vector(_) => {
|
||||||
self.mark_known_empty(EmptyBecause::NonNumericArgument);
|
self.mark_known_empty(EmptyBecause::NonNumericArgument);
|
||||||
|
@ -65,6 +66,36 @@ impl ConjoiningClauses {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Just like `resolve_numeric_argument`, but for `ValueType::Instant`.
|
||||||
|
pub fn resolve_instant_argument(&mut self, function: &PlainSymbol, position: usize, arg: FnArg) -> Result<QueryValue> {
|
||||||
|
use self::FnArg::*;
|
||||||
|
match arg {
|
||||||
|
FnArg::Variable(var) => {
|
||||||
|
self.constrain_var_to_type(var.clone(), ValueType::Instant);
|
||||||
|
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())))
|
||||||
|
},
|
||||||
|
Constant(NonIntegerConstant::Instant(v)) => {
|
||||||
|
Ok(QueryValue::TypedValue(TypedValue::Instant(v)))
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: should we allow integers if they seem to be timestamps? It's ambiguous…
|
||||||
|
EntidOrInteger(_) |
|
||||||
|
IdentOrKeyword(_) |
|
||||||
|
SrcVar(_) |
|
||||||
|
Constant(NonIntegerConstant::Boolean(_)) |
|
||||||
|
Constant(NonIntegerConstant::Float(_)) |
|
||||||
|
Constant(NonIntegerConstant::Text(_)) |
|
||||||
|
Constant(NonIntegerConstant::Uuid(_)) |
|
||||||
|
Constant(NonIntegerConstant::BigInteger(_)) |
|
||||||
|
Vector(_) => {
|
||||||
|
self.mark_known_empty(EmptyBecause::NonInstantArgument);
|
||||||
|
bail!(ErrorKind::InvalidArgument(function.clone(), "instant", position));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Take a function argument and turn it into a `QueryValue` suitable for use in a concrete
|
/// Take a function argument and turn it into a `QueryValue` suitable for use in a concrete
|
||||||
/// constraint.
|
/// constraint.
|
||||||
|
|
|
@ -39,6 +39,11 @@ error_chain! {
|
||||||
}
|
}
|
||||||
|
|
||||||
errors {
|
errors {
|
||||||
|
UnsupportedArgument {
|
||||||
|
description("unexpected FnArg")
|
||||||
|
display("unexpected FnArg")
|
||||||
|
}
|
||||||
|
|
||||||
InputTypeDisagreement(var: PlainSymbol, declared: ValueType, provided: ValueType) {
|
InputTypeDisagreement(var: PlainSymbol, declared: ValueType, provided: ValueType) {
|
||||||
description("input type disagreement")
|
description("input type disagreement")
|
||||||
display("value of type {} provided for var {}, expected {}", provided, var, declared)
|
display("value of type {} provided for var {}, expected {}", provided, var, declared)
|
||||||
|
|
|
@ -56,6 +56,11 @@ pub use clauses::{
|
||||||
QueryInputs,
|
QueryInputs,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use types::{
|
||||||
|
EmptyBecause,
|
||||||
|
ValueTypeSet,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AlgebraicQuery {
|
pub struct AlgebraicQuery {
|
||||||
default_source: SrcVar,
|
default_source: SrcVar,
|
||||||
|
|
|
@ -274,10 +274,11 @@ impl From<Order> for OrderBy {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
/// Define the different numeric inequality operators that we support.
|
/// Define the different inequality operators that we support.
|
||||||
/// Note that we deliberately don't just use "<=" and friends as strings:
|
/// Note that we deliberately don't just use "<=" and friends as strings:
|
||||||
/// Datalog and SQL don't use the same operators (e.g., `<>` and `!=`).
|
/// Datalog and SQL don't use the same operators (e.g., `<>` and `!=`).
|
||||||
pub enum NumericComparison {
|
/// These are applicable to numbers and instants.
|
||||||
|
pub enum Inequality {
|
||||||
LessThan,
|
LessThan,
|
||||||
LessThanOrEquals,
|
LessThanOrEquals,
|
||||||
GreaterThan,
|
GreaterThan,
|
||||||
|
@ -285,9 +286,9 @@ pub enum NumericComparison {
|
||||||
NotEquals,
|
NotEquals,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NumericComparison {
|
impl Inequality {
|
||||||
pub fn to_sql_operator(self) -> &'static str {
|
pub fn to_sql_operator(self) -> &'static str {
|
||||||
use self::NumericComparison::*;
|
use self::Inequality::*;
|
||||||
match self {
|
match self {
|
||||||
LessThan => "<",
|
LessThan => "<",
|
||||||
LessThanOrEquals => "<=",
|
LessThanOrEquals => "<=",
|
||||||
|
@ -297,21 +298,28 @@ impl NumericComparison {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_datalog_operator(s: &str) -> Option<NumericComparison> {
|
pub fn from_datalog_operator(s: &str) -> Option<Inequality> {
|
||||||
match s {
|
match s {
|
||||||
"<" => Some(NumericComparison::LessThan),
|
"<" => Some(Inequality::LessThan),
|
||||||
"<=" => Some(NumericComparison::LessThanOrEquals),
|
"<=" => Some(Inequality::LessThanOrEquals),
|
||||||
">" => Some(NumericComparison::GreaterThan),
|
">" => Some(Inequality::GreaterThan),
|
||||||
">=" => Some(NumericComparison::GreaterThanOrEquals),
|
">=" => Some(Inequality::GreaterThanOrEquals),
|
||||||
"!=" => Some(NumericComparison::NotEquals),
|
"!=" => Some(Inequality::NotEquals),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The built-in inequality operators apply to Long, Double, and Instant.
|
||||||
|
pub fn supported_types(&self) -> ValueTypeSet {
|
||||||
|
let mut ts = ValueTypeSet::of_numeric_types();
|
||||||
|
ts.insert(ValueType::Instant);
|
||||||
|
ts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for NumericComparison {
|
impl Debug for Inequality {
|
||||||
fn fmt(&self, f: &mut Formatter) -> ::std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> ::std::fmt::Result {
|
||||||
use self::NumericComparison::*;
|
use self::Inequality::*;
|
||||||
f.write_str(match self {
|
f.write_str(match self {
|
||||||
&LessThan => "<",
|
&LessThan => "<",
|
||||||
&LessThanOrEquals => "<=",
|
&LessThanOrEquals => "<=",
|
||||||
|
@ -325,15 +333,13 @@ impl Debug for NumericComparison {
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum ColumnConstraint {
|
pub enum ColumnConstraint {
|
||||||
Equals(QualifiedAlias, QueryValue),
|
Equals(QualifiedAlias, QueryValue),
|
||||||
NumericInequality {
|
Inequality {
|
||||||
operator: NumericComparison,
|
operator: Inequality,
|
||||||
left: QueryValue,
|
left: QueryValue,
|
||||||
right: QueryValue,
|
right: QueryValue,
|
||||||
},
|
},
|
||||||
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),
|
Matches(QualifiedAlias, QueryValue),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,7 +447,7 @@ impl Debug for ColumnConstraint {
|
||||||
write!(f, "{:?} = {:?}", qa1, thing)
|
write!(f, "{:?} = {:?}", qa1, thing)
|
||||||
},
|
},
|
||||||
|
|
||||||
&NumericInequality { operator, ref left, ref right } => {
|
&Inequality { operator, ref left, ref right } => {
|
||||||
write!(f, "{:?} {:?} {:?}", left, operator, right)
|
write!(f, "{:?} {:?} {:?}", left, operator, right)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -462,9 +468,15 @@ impl Debug for ColumnConstraint {
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
pub enum EmptyBecause {
|
pub enum EmptyBecause {
|
||||||
ConflictingBindings { var: Variable, existing: TypedValue, desired: TypedValue },
|
ConflictingBindings { var: Variable, existing: TypedValue, desired: TypedValue },
|
||||||
|
|
||||||
|
// A variable is known to be of two conflicting sets of types.
|
||||||
TypeMismatch { var: Variable, existing: ValueTypeSet, desired: ValueTypeSet },
|
TypeMismatch { var: Variable, existing: ValueTypeSet, desired: ValueTypeSet },
|
||||||
|
|
||||||
|
// The same, but for non-variables.
|
||||||
|
KnownTypeMismatch { left: ValueTypeSet, right: ValueTypeSet },
|
||||||
NoValidTypes(Variable),
|
NoValidTypes(Variable),
|
||||||
NonAttributeArgument,
|
NonAttributeArgument,
|
||||||
|
NonInstantArgument,
|
||||||
NonNumericArgument,
|
NonNumericArgument,
|
||||||
NonStringFulltextValue,
|
NonStringFulltextValue,
|
||||||
UnresolvedIdent(NamespacedKeyword),
|
UnresolvedIdent(NamespacedKeyword),
|
||||||
|
@ -487,12 +499,19 @@ 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)
|
||||||
},
|
},
|
||||||
|
&KnownTypeMismatch { ref left, ref right } => {
|
||||||
|
write!(f, "Type mismatch: {:?} can't be compared to {:?}",
|
||||||
|
left, right)
|
||||||
|
},
|
||||||
&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 => {
|
&NonAttributeArgument => {
|
||||||
write!(f, "Non-attribute argument in attribute place")
|
write!(f, "Non-attribute argument in attribute place")
|
||||||
},
|
},
|
||||||
|
&NonInstantArgument => {
|
||||||
|
write!(f, "Non-instant argument in instant place")
|
||||||
|
},
|
||||||
&NonNumericArgument => {
|
&NonNumericArgument => {
|
||||||
write!(f, "Non-numeric argument in numeric place")
|
write!(f, "Non-numeric argument in numeric place")
|
||||||
},
|
},
|
||||||
|
@ -612,6 +631,10 @@ impl ValueTypeSet {
|
||||||
self.0.iter().next()
|
self.0.iter().next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_subset(&self, other: &ValueTypeSet) -> bool {
|
||||||
|
self.0.is_subset(&other.0)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn contains(&self, vt: ValueType) -> bool {
|
pub fn contains(&self, vt: ValueType) -> bool {
|
||||||
self.0.contains(&vt)
|
self.0.contains(&vt)
|
||||||
}
|
}
|
||||||
|
|
149
query-algebrizer/tests/predicate.rs
Normal file
149
query-algebrizer/tests/predicate.rs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
// Copyright 2016 Mozilla
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||||
|
// this file except in compliance with the License. You may obtain a copy of the
|
||||||
|
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
// Unless required by applicable law or agreed to in writing, software distributed
|
||||||
|
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||||
|
// specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
extern crate mentat_core;
|
||||||
|
extern crate mentat_query;
|
||||||
|
extern crate mentat_query_algebrizer;
|
||||||
|
extern crate mentat_query_parser;
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use mentat_core::{
|
||||||
|
Attribute,
|
||||||
|
Entid,
|
||||||
|
Schema,
|
||||||
|
ValueType,
|
||||||
|
TypedValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
use mentat_query_parser::{
|
||||||
|
parse_find_string,
|
||||||
|
};
|
||||||
|
|
||||||
|
use mentat_query::{
|
||||||
|
NamespacedKeyword,
|
||||||
|
PlainSymbol,
|
||||||
|
Variable,
|
||||||
|
};
|
||||||
|
|
||||||
|
use mentat_query_algebrizer::{
|
||||||
|
BindingError,
|
||||||
|
ConjoiningClauses,
|
||||||
|
ComputedTable,
|
||||||
|
EmptyBecause,
|
||||||
|
Error,
|
||||||
|
ErrorKind,
|
||||||
|
QueryInputs,
|
||||||
|
ValueTypeSet,
|
||||||
|
algebrize,
|
||||||
|
algebrize_with_inputs,
|
||||||
|
};
|
||||||
|
|
||||||
|
// These are helpers that tests use to build Schema instances.
|
||||||
|
#[cfg(test)]
|
||||||
|
fn associate_ident(schema: &mut Schema, i: NamespacedKeyword, e: Entid) {
|
||||||
|
schema.entid_map.insert(e, i.clone());
|
||||||
|
schema.ident_map.insert(i.clone(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
|
||||||
|
schema.schema_map.insert(e, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepopulated_schema() -> Schema {
|
||||||
|
let mut schema = Schema::default();
|
||||||
|
associate_ident(&mut schema, NamespacedKeyword::new("foo", "date"), 65);
|
||||||
|
associate_ident(&mut schema, NamespacedKeyword::new("foo", "double"), 66);
|
||||||
|
add_attribute(&mut schema, 65, Attribute {
|
||||||
|
value_type: ValueType::Instant,
|
||||||
|
multival: false,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
add_attribute(&mut schema, 66, Attribute {
|
||||||
|
value_type: ValueType::Double,
|
||||||
|
multival: false,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
schema
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bails(schema: &Schema, input: &str) -> Error {
|
||||||
|
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||||
|
algebrize(schema.into(), parsed).expect_err("algebrize to have failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bails_with_inputs(schema: &Schema, input: &str, inputs: QueryInputs) -> Error {
|
||||||
|
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||||
|
algebrize_with_inputs(schema, parsed, 0, inputs).expect_err("algebrize to have failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alg(schema: &Schema, input: &str) -> ConjoiningClauses {
|
||||||
|
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||||
|
algebrize(schema.into(), parsed).expect("algebrizing to have succeeded").cc
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_instant_predicates_require_instants() {
|
||||||
|
let schema = prepopulated_schema();
|
||||||
|
|
||||||
|
// You can't use a string for an inequality: this is a straight-up error.
|
||||||
|
let query = r#"[:find ?e
|
||||||
|
:where
|
||||||
|
[?e :foo/date ?t]
|
||||||
|
[(> ?t "2017-06-16T00:56:41.257Z")]]"#;
|
||||||
|
match bails(&schema, query).0 {
|
||||||
|
ErrorKind::InvalidArgument(op, why, idx) => {
|
||||||
|
assert_eq!(op, PlainSymbol::new(">"));
|
||||||
|
assert_eq!(why, "numeric or instant");
|
||||||
|
assert_eq!(idx, 1);
|
||||||
|
},
|
||||||
|
_ => panic!("Expected InvalidArgument."),
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = r#"[:find ?e
|
||||||
|
:where
|
||||||
|
[?e :foo/date ?t]
|
||||||
|
[(> "2017-06-16T00:56:41.257Z", ?t)]]"#;
|
||||||
|
match bails(&schema, query).0 {
|
||||||
|
ErrorKind::InvalidArgument(op, why, idx) => {
|
||||||
|
assert_eq!(op, PlainSymbol::new(">"));
|
||||||
|
assert_eq!(why, "numeric or instant");
|
||||||
|
assert_eq!(idx, 0); // We get this right.
|
||||||
|
},
|
||||||
|
_ => panic!("Expected InvalidArgument."),
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can try using a number, which is valid input to a numeric predicate.
|
||||||
|
// In this store and query, though, that means we expect `?t` to be both
|
||||||
|
// an instant and a number, so the query is known-empty.
|
||||||
|
let query = r#"[:find ?e
|
||||||
|
:where
|
||||||
|
[?e :foo/date ?t]
|
||||||
|
[(> ?t 1234512345)]]"#;
|
||||||
|
let cc = alg(&schema, query);
|
||||||
|
assert!(cc.is_known_empty());
|
||||||
|
assert_eq!(cc.empty_because.unwrap(),
|
||||||
|
EmptyBecause::TypeMismatch {
|
||||||
|
var: Variable::from_valid_name("?t"),
|
||||||
|
existing: ValueTypeSet::of_one(ValueType::Instant),
|
||||||
|
desired: ValueTypeSet::of_numeric_types(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// You can compare doubles to longs.
|
||||||
|
let query = r#"[:find ?e
|
||||||
|
:where
|
||||||
|
[?e :foo/double ?t]
|
||||||
|
[(< ?t 1234512345)]]"#;
|
||||||
|
let cc = alg(&schema, query);
|
||||||
|
assert!(!cc.is_known_empty());
|
||||||
|
assert_eq!(cc.known_type(&Variable::from_valid_name("?t")).expect("?t is known"),
|
||||||
|
ValueType::Double);
|
||||||
|
}
|
|
@ -140,7 +140,7 @@ impl ToConstraint for ColumnConstraint {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
NumericInequality { operator, left, right } => {
|
Inequality { operator, left, right } => {
|
||||||
Constraint::Infix {
|
Constraint::Infix {
|
||||||
op: Op(operator.to_sql_operator()),
|
op: Op(operator.to_sql_operator()),
|
||||||
left: left.into(),
|
left: left.into(),
|
||||||
|
|
|
@ -317,6 +317,56 @@ fn test_numeric_not_equals_known_attribute() {
|
||||||
assert_eq!(args, vec![]);
|
assert_eq!(args, vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compare_long_to_double_constants() {
|
||||||
|
let schema = prepopulated_typed_schema(ValueType::Double);
|
||||||
|
|
||||||
|
let query = r#"[:find ?e .
|
||||||
|
:where
|
||||||
|
[?e :foo/bar ?v]
|
||||||
|
[(< 99.0 1234512345)]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT `datoms00`.e AS `?e` FROM `datoms` AS `datoms00` \
|
||||||
|
WHERE `datoms00`.a = 99 \
|
||||||
|
AND 9.9e1 < 1234512345 \
|
||||||
|
LIMIT 1");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compare_long_to_double() {
|
||||||
|
let schema = prepopulated_typed_schema(ValueType::Double);
|
||||||
|
|
||||||
|
// You can compare longs to doubles.
|
||||||
|
let query = r#"[:find ?e .
|
||||||
|
:where
|
||||||
|
[?e :foo/bar ?t]
|
||||||
|
[(< ?t 1234512345)]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT `datoms00`.e AS `?e` FROM `datoms` AS `datoms00` \
|
||||||
|
WHERE `datoms00`.a = 99 \
|
||||||
|
AND `datoms00`.v < 1234512345 \
|
||||||
|
LIMIT 1");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compare_double_to_long() {
|
||||||
|
let schema = prepopulated_typed_schema(ValueType::Long);
|
||||||
|
|
||||||
|
// You can compare doubles to longs.
|
||||||
|
let query = r#"[:find ?e .
|
||||||
|
:where
|
||||||
|
[?e :foo/bar ?t]
|
||||||
|
[(< ?t 1234512345.0)]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT `datoms00`.e AS `?e` FROM `datoms` AS `datoms00` \
|
||||||
|
WHERE `datoms00`.a = 99 \
|
||||||
|
AND `datoms00`.v < 1.234512345e9 \
|
||||||
|
LIMIT 1");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_simple_or_join() {
|
fn test_simple_or_join() {
|
||||||
let mut schema = Schema::default();
|
let mut schema = Schema::default();
|
||||||
|
@ -888,3 +938,20 @@ fn test_fulltext_inputs() {
|
||||||
AND `datoms02`.a = 99");
|
AND `datoms02`.a = 99");
|
||||||
assert_eq!(args, vec![make_arg("$v0", "hello"),]);
|
assert_eq!(args, vec![make_arg("$v0", "hello"),]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_instant_range() {
|
||||||
|
let schema = prepopulated_typed_schema(ValueType::Instant);
|
||||||
|
let query = r#"[:find ?e
|
||||||
|
:where
|
||||||
|
[?e :foo/bar ?t]
|
||||||
|
[(> ?t #inst "2017-06-16T00:56:41.257Z")]]"#;
|
||||||
|
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?e` \
|
||||||
|
FROM \
|
||||||
|
`datoms` AS `datoms00` \
|
||||||
|
WHERE `datoms00`.a = 99 \
|
||||||
|
AND `datoms00`.v > 1497574601257000");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
}
|
||||||
|
|
|
@ -259,6 +259,15 @@ impl FromValue<FnArg> for FnArg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FnArg {
|
||||||
|
pub fn as_variable(&self) -> Option<&Variable> {
|
||||||
|
match self {
|
||||||
|
&FnArg::Variable(ref v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// e, a, tx can't be values -- no strings, no floats -- and so
|
/// e, a, tx can't be values -- no strings, no floats -- and so
|
||||||
/// they can only be variables, entity IDs, ident keywords, or
|
/// they can only be variables, entity IDs, ident keywords, or
|
||||||
/// placeholders.
|
/// placeholders.
|
||||||
|
|
|
@ -356,3 +356,37 @@ fn test_fulltext() {
|
||||||
_ => panic!("Expected query to work."),
|
_ => panic!("Expected query to work."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_instant_range_query() {
|
||||||
|
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/date]
|
||||||
|
[:db/add "a" :db/valueType :db.type/instant]
|
||||||
|
[:db/add "a" :db/cardinality :db.cardinality/one]
|
||||||
|
]"#).unwrap();
|
||||||
|
|
||||||
|
let ids = conn.transact(&mut c, r#"[
|
||||||
|
[:db/add "b" :foo/date #inst "2016-01-01T11:00:00.000Z"]
|
||||||
|
[:db/add "c" :foo/date #inst "2016-06-01T11:00:01.000Z"]
|
||||||
|
[:db/add "d" :foo/date #inst "2017-01-01T11:00:02.000Z"]
|
||||||
|
[:db/add "e" :foo/date #inst "2017-06-01T11:00:03.000Z"]
|
||||||
|
]"#).unwrap().tempids;
|
||||||
|
|
||||||
|
let r = conn.q_once(&mut c,
|
||||||
|
r#"[:find [?x ...]
|
||||||
|
:order (asc ?date)
|
||||||
|
:where
|
||||||
|
[?x :foo/date ?date]
|
||||||
|
[(< ?date #inst "2017-01-01T11:00:02.000Z")]]"#, None);
|
||||||
|
match r {
|
||||||
|
Result::Ok(QueryResults::Coll(vals)) => {
|
||||||
|
assert_eq!(vals,
|
||||||
|
vec![TypedValue::Ref(*ids.get("b").unwrap()),
|
||||||
|
TypedValue::Ref(*ids.get("c").unwrap())]);
|
||||||
|
},
|
||||||
|
_ => panic!("Expected query to work."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue