From 997df0b776daa956e65f9c4709b8dcc0519d93a5 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Mon, 27 Mar 2017 19:35:39 -0700 Subject: [PATCH] Part 1: introduce ColumnIntersection and ColumnAlternation. This provides a limited form of OR and AND for column constraints, allowing simple 'or-join' queries to be expressed on a single table alias. --- query-algebrizer/src/cc.rs | 37 +++++++------- query-algebrizer/src/lib.rs | 3 ++ query-algebrizer/src/types.rs | 84 +++++++++++++++++++++++++++++++ query-sql/src/lib.rs | 22 ++++++++ query-translator/src/translate.rs | 29 +++++++++++ 5 files changed, 157 insertions(+), 18 deletions(-) diff --git a/query-algebrizer/src/cc.rs b/query-algebrizer/src/cc.rs index 73c6cfcc..d1f34e3c 100644 --- a/query-algebrizer/src/cc.rs +++ b/query-algebrizer/src/cc.rs @@ -51,6 +51,7 @@ use errors::{ use types::{ ColumnConstraint, + ColumnIntersection, DatomsColumn, DatomsTable, EmptyBecause, @@ -129,7 +130,7 @@ pub struct ConjoiningClauses { pub from: Vec, /// A list of fragments that can be joined by `AND`. - pub wheres: Vec, + pub wheres: ColumnIntersection, /// A map from var to qualified columns. Used to project. pub column_bindings: BTreeMap>, @@ -218,7 +219,7 @@ impl Default for ConjoiningClauses { empty_because: None, aliaser: default_table_aliaser(), from: vec![], - wheres: vec![], + wheres: ColumnIntersection::default(), input_variables: BTreeSet::new(), column_bindings: BTreeMap::new(), value_bindings: BTreeMap::new(), @@ -318,11 +319,11 @@ impl ConjoiningClauses { } pub fn constrain_column_to_constant(&mut self, table: TableAlias, column: DatomsColumn, constant: TypedValue) { - self.wheres.push(ColumnConstraint::Equals(QualifiedAlias(table, column), QueryValue::TypedValue(constant))) + self.wheres.also(ColumnConstraint::Equals(QualifiedAlias(table, column), QueryValue::TypedValue(constant))) } pub fn constrain_column_to_entity(&mut self, table: TableAlias, column: DatomsColumn, entity: Entid) { - self.wheres.push(ColumnConstraint::Equals(QualifiedAlias(table, column), QueryValue::Entid(entity))) + self.wheres.also(ColumnConstraint::Equals(QualifiedAlias(table, column), QueryValue::Entid(entity))) } pub fn constrain_attribute(&mut self, table: TableAlias, attribute: Entid) { @@ -330,7 +331,7 @@ impl ConjoiningClauses { } pub fn constrain_value_to_numeric(&mut self, table: TableAlias, value: i64) { - self.wheres.push(ColumnConstraint::Equals( + self.wheres.also(ColumnConstraint::Equals( QualifiedAlias(table, DatomsColumn::Value), QueryValue::PrimitiveLong(value))) } @@ -577,7 +578,7 @@ impl ConjoiningClauses { // TODO: if both primary and secondary are .v, should we make sure // the type tag columns also match? // We don't do so in the ClojureScript version. - self.wheres.push(ColumnConstraint::Equals(primary.clone(), QueryValue::Column(secondary.clone()))); + self.wheres.also(ColumnConstraint::Equals(primary.clone(), QueryValue::Column(secondary.clone()))); } } } @@ -827,7 +828,7 @@ impl ConjoiningClauses { } else { // It must be a keyword. self.constrain_column_to_constant(col.clone(), DatomsColumn::Value, TypedValue::Keyword(kw.clone())); - self.wheres.push(ColumnConstraint::HasType(col.clone(), ValueType::Keyword)); + self.wheres.also(ColumnConstraint::HasType(col.clone(), ValueType::Keyword)); }; }, PatternValuePlace::Constant(ref c) => { @@ -863,7 +864,7 @@ impl ConjoiningClauses { // Because everything we handle here is unambiguous, we generate a single type // restriction from the value type of the typed value. if value_type.is_none() { - self.wheres.push(ColumnConstraint::HasType(col.clone(), typed_value_type)); + self.wheres.also(ColumnConstraint::HasType(col.clone(), typed_value_type)); } }, @@ -941,7 +942,7 @@ impl ConjoiningClauses { left: left, right: right, }; - self.wheres.push(constraint); + self.wheres.also(constraint); Ok(()) } } @@ -1059,7 +1060,7 @@ mod testing { assert_eq!(cc.wheres, vec![ ColumnConstraint::Equals(d0_a, QueryValue::Entid(99)), ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))), - ]); + ].into()); } #[test] @@ -1097,7 +1098,7 @@ mod testing { assert_eq!(cc.wheres, vec![ ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))), ColumnConstraint::HasType("datoms00".to_string(), ValueType::Boolean), - ]); + ].into()); } /// This test ensures that we do less work if we know the attribute thanks to a var lookup. @@ -1140,7 +1141,7 @@ mod testing { assert_eq!(cc.column_bindings.get(&x).unwrap(), &vec![d0_e.clone()]); assert_eq!(cc.wheres, vec![ ColumnConstraint::Equals(d0_a, QueryValue::Entid(99)), - ]); + ].into()); } /// Queries that bind non-entity values to entity places can't return results. @@ -1198,7 +1199,7 @@ mod testing { // ?x is bound to datoms0.e. assert_eq!(cc.column_bindings.get(&x).unwrap(), &vec![d0_e.clone()]); - assert_eq!(cc.wheres, vec![]); + assert_eq!(cc.wheres, vec![].into()); } /// This test ensures that we query all_datoms if we're looking for a string. @@ -1237,7 +1238,7 @@ mod testing { assert_eq!(cc.wheres, vec![ ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::String("hello".to_string()))), ColumnConstraint::HasType("all_datoms00".to_string(), ValueType::String), - ]); + ].into()); } #[test] @@ -1310,7 +1311,7 @@ mod testing { ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::String("idgoeshere".to_string()))), ColumnConstraint::Equals(d1_a, QueryValue::Entid(99)), ColumnConstraint::Equals(d0_e, QueryValue::Column(d1_e)), - ]); + ].into()); } #[test] @@ -1346,7 +1347,7 @@ mod testing { assert_eq!(cc.wheres, vec![ ColumnConstraint::Equals(d0_a, QueryValue::Entid(99)), ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))), - ]); + ].into()); // There is no binding for ?y. assert!(!cc.column_bindings.contains_key(&y)); @@ -1466,11 +1467,11 @@ mod testing { let clauses = cc.wheres; assert_eq!(clauses.len(), 1); - assert_eq!(clauses[0], ColumnConstraint::NumericInequality { + assert_eq!(clauses.0[0], ColumnConstraint::NumericInequality { operator: NumericComparison::LessThan, left: QueryValue::Column(cc.column_bindings.get(&y).unwrap()[0].clone()), right: QueryValue::TypedValue(TypedValue::Long(10)), - }); + }.into()); } #[test] diff --git a/query-algebrizer/src/lib.rs b/query-algebrizer/src/lib.rs index d6d92417..51832d26 100644 --- a/query-algebrizer/src/lib.rs +++ b/query-algebrizer/src/lib.rs @@ -93,7 +93,10 @@ pub use cc::{ }; pub use types::{ + ColumnAlternation, ColumnConstraint, + ColumnConstraintOrAlternation, + ColumnIntersection, DatomsColumn, DatomsTable, QualifiedAlias, diff --git a/query-algebrizer/src/types.rs b/query-algebrizer/src/types.rs index 274c1cac..3d1f2ce6 100644 --- a/query-algebrizer/src/types.rs +++ b/query-algebrizer/src/types.rs @@ -194,6 +194,90 @@ pub enum ColumnConstraint { HasType(TableAlias, ValueType), } +#[derive(PartialEq, Eq, Debug)] +pub enum ColumnConstraintOrAlternation { + Constraint(ColumnConstraint), + Alternation(ColumnAlternation), +} + +impl From for ColumnConstraintOrAlternation { + fn from(thing: ColumnConstraint) -> Self { + ColumnConstraintOrAlternation::Constraint(thing) + } +} + +/// A `ColumnIntersection` constraint is satisfied if all of its inner constraints are satisfied. +/// An empty intersection is always satisfied. +#[derive(PartialEq, Eq)] +pub struct ColumnIntersection(pub Vec); + +impl From> for ColumnIntersection { + fn from(thing: Vec) -> Self { + ColumnIntersection(thing.into_iter().map(|x| x.into()).collect()) + } +} + +impl Default for ColumnIntersection { + fn default() -> Self { + ColumnIntersection(vec![]) + } +} + +impl IntoIterator for ColumnIntersection { + type Item = ColumnConstraintOrAlternation; + type IntoIter = ::std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl ColumnIntersection { + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn also(&mut self, constraint: ColumnConstraint) { + self.0.push(ColumnConstraintOrAlternation::Constraint(constraint)); + } +} + +/// A `ColumnAlternation` constraint is satisfied if at least one of its inner constraints is +/// satisfied. An empty `ColumnAlternation` is never satisfied. +#[derive(PartialEq, Eq, Debug)] +pub struct ColumnAlternation(pub Vec); + +impl Default for ColumnAlternation { + fn default() -> Self { + ColumnAlternation(vec![]) + } +} + +impl IntoIterator for ColumnAlternation { + type Item = ColumnIntersection; + type IntoIter = ::std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl ColumnAlternation { + pub fn instead(&mut self, intersection: ColumnIntersection) { + self.0.push(intersection); + } +} + +impl Debug for ColumnIntersection { + fn fmt(&self, f: &mut Formatter) -> ::std::fmt::Result { + write!(f, "{:?}", self.0) + } +} + impl Debug for ColumnConstraint { fn fmt(&self, f: &mut Formatter) -> ::std::fmt::Result { use self::ColumnConstraint::*; diff --git a/query-sql/src/lib.rs b/query-sql/src/lib.rs index d8b7c343..1d3d7869 100644 --- a/query-sql/src/lib.rs +++ b/query-sql/src/lib.rs @@ -81,6 +81,9 @@ pub enum Constraint { left: ColumnOrExpression, right: ColumnOrExpression, }, + Or { + constraints: Vec, + }, And { constraints: Vec, }, @@ -260,6 +263,11 @@ impl QueryFragment for Constraint { }, &And { ref constraints } => { + // An empty intersection is true. + if constraints.is_empty() { + out.push_sql("1"); + return Ok(()) + } out.push_sql("("); interpose!(constraint, constraints, { constraint.push_sql(out)? }, @@ -268,6 +276,20 @@ impl QueryFragment for Constraint { Ok(()) }, + &Or { ref constraints } => { + // An empty alternation is false. + if constraints.is_empty() { + out.push_sql("0"); + return Ok(()) + } + out.push_sql("("); + interpose!(constraint, constraints, + { constraint.push_sql(out)? }, + { out.push_sql(" OR ") }); + out.push_sql(")"); + Ok(()) + } + &In { ref left, ref list } => { left.push_sql(out)?; out.push_sql(" IN ("); diff --git a/query-translator/src/translate.rs b/query-translator/src/translate.rs index 7831a450..e6b4b375 100644 --- a/query-translator/src/translate.rs +++ b/query-translator/src/translate.rs @@ -25,7 +25,10 @@ use mentat_query::{ use mentat_query_algebrizer::{ AlgebraicQuery, + ColumnAlternation, ColumnConstraint, + ColumnConstraintOrAlternation, + ColumnIntersection, ConjoiningClauses, DatomsColumn, DatomsTable, @@ -66,6 +69,32 @@ impl ToColumn for QualifiedAlias { } } +impl ToConstraint for ColumnIntersection { + fn to_constraint(self) -> Constraint { + Constraint::And { + constraints: self.into_iter().map(|x| x.to_constraint()).collect() + } + } +} + +impl ToConstraint for ColumnAlternation { + fn to_constraint(self) -> Constraint { + Constraint::Or { + constraints: self.into_iter().map(|x| x.to_constraint()).collect() + } + } +} + +impl ToConstraint for ColumnConstraintOrAlternation { + fn to_constraint(self) -> Constraint { + use self::ColumnConstraintOrAlternation::*; + match self { + Alternation(alt) => alt.to_constraint(), + Constraint(c) => c.to_constraint(), + } + } +} + impl ToConstraint for ColumnConstraint { fn to_constraint(self) -> Constraint { use self::ColumnConstraint::*;