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.
This commit is contained in:
Richard Newman 2017-03-27 19:35:39 -07:00
parent 460fdac252
commit 997df0b776
5 changed files with 157 additions and 18 deletions

View file

@ -51,6 +51,7 @@ use errors::{
use types::{ use types::{
ColumnConstraint, ColumnConstraint,
ColumnIntersection,
DatomsColumn, DatomsColumn,
DatomsTable, DatomsTable,
EmptyBecause, EmptyBecause,
@ -129,7 +130,7 @@ pub struct ConjoiningClauses {
pub from: Vec<SourceAlias>, pub from: Vec<SourceAlias>,
/// A list of fragments that can be joined by `AND`. /// A list of fragments that can be joined by `AND`.
pub wheres: Vec<ColumnConstraint>, pub wheres: ColumnIntersection,
/// A map from var to qualified columns. Used to project. /// A map from var to qualified columns. Used to project.
pub column_bindings: BTreeMap<Variable, Vec<QualifiedAlias>>, pub column_bindings: BTreeMap<Variable, Vec<QualifiedAlias>>,
@ -218,7 +219,7 @@ impl Default for ConjoiningClauses {
empty_because: None, empty_because: None,
aliaser: default_table_aliaser(), aliaser: default_table_aliaser(),
from: vec![], from: vec![],
wheres: vec![], wheres: ColumnIntersection::default(),
input_variables: BTreeSet::new(), input_variables: BTreeSet::new(),
column_bindings: BTreeMap::new(), column_bindings: BTreeMap::new(),
value_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) { 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) { 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) { 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) { 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), QualifiedAlias(table, DatomsColumn::Value),
QueryValue::PrimitiveLong(value))) QueryValue::PrimitiveLong(value)))
} }
@ -577,7 +578,7 @@ impl ConjoiningClauses {
// TODO: if both primary and secondary are .v, should we make sure // TODO: if both primary and secondary are .v, should we make sure
// the type tag columns also match? // the type tag columns also match?
// We don't do so in the ClojureScript version. // 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 { } else {
// It must be a keyword. // It must be a keyword.
self.constrain_column_to_constant(col.clone(), DatomsColumn::Value, TypedValue::Keyword(kw.clone())); 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) => { PatternValuePlace::Constant(ref c) => {
@ -863,7 +864,7 @@ impl ConjoiningClauses {
// Because everything we handle here is unambiguous, we generate a single type // Because everything we handle here is unambiguous, we generate a single type
// restriction from the value type of the typed value. // restriction from the value type of the typed value.
if value_type.is_none() { 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, left: left,
right: right, right: right,
}; };
self.wheres.push(constraint); self.wheres.also(constraint);
Ok(()) Ok(())
} }
} }
@ -1059,7 +1060,7 @@ mod testing {
assert_eq!(cc.wheres, vec![ assert_eq!(cc.wheres, vec![
ColumnConstraint::Equals(d0_a, QueryValue::Entid(99)), ColumnConstraint::Equals(d0_a, QueryValue::Entid(99)),
ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))), ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))),
]); ].into());
} }
#[test] #[test]
@ -1097,7 +1098,7 @@ mod testing {
assert_eq!(cc.wheres, vec![ assert_eq!(cc.wheres, vec![
ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))), ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))),
ColumnConstraint::HasType("datoms00".to_string(), ValueType::Boolean), 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. /// 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.column_bindings.get(&x).unwrap(), &vec![d0_e.clone()]);
assert_eq!(cc.wheres, vec![ assert_eq!(cc.wheres, vec![
ColumnConstraint::Equals(d0_a, QueryValue::Entid(99)), ColumnConstraint::Equals(d0_a, QueryValue::Entid(99)),
]); ].into());
} }
/// Queries that bind non-entity values to entity places can't return results. /// 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. // ?x is bound to datoms0.e.
assert_eq!(cc.column_bindings.get(&x).unwrap(), &vec![d0_e.clone()]); 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. /// 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![ assert_eq!(cc.wheres, vec![
ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::String("hello".to_string()))), ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::String("hello".to_string()))),
ColumnConstraint::HasType("all_datoms00".to_string(), ValueType::String), ColumnConstraint::HasType("all_datoms00".to_string(), ValueType::String),
]); ].into());
} }
#[test] #[test]
@ -1310,7 +1311,7 @@ mod testing {
ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::String("idgoeshere".to_string()))), ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::String("idgoeshere".to_string()))),
ColumnConstraint::Equals(d1_a, QueryValue::Entid(99)), ColumnConstraint::Equals(d1_a, QueryValue::Entid(99)),
ColumnConstraint::Equals(d0_e, QueryValue::Column(d1_e)), ColumnConstraint::Equals(d0_e, QueryValue::Column(d1_e)),
]); ].into());
} }
#[test] #[test]
@ -1346,7 +1347,7 @@ mod testing {
assert_eq!(cc.wheres, vec![ assert_eq!(cc.wheres, vec![
ColumnConstraint::Equals(d0_a, QueryValue::Entid(99)), ColumnConstraint::Equals(d0_a, QueryValue::Entid(99)),
ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))), ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))),
]); ].into());
// There is no binding for ?y. // There is no binding for ?y.
assert!(!cc.column_bindings.contains_key(&y)); assert!(!cc.column_bindings.contains_key(&y));
@ -1466,11 +1467,11 @@ mod testing {
let clauses = cc.wheres; let clauses = cc.wheres;
assert_eq!(clauses.len(), 1); assert_eq!(clauses.len(), 1);
assert_eq!(clauses[0], ColumnConstraint::NumericInequality { assert_eq!(clauses.0[0], ColumnConstraint::NumericInequality {
operator: NumericComparison::LessThan, operator: NumericComparison::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());
} }
#[test] #[test]

View file

@ -93,7 +93,10 @@ pub use cc::{
}; };
pub use types::{ pub use types::{
ColumnAlternation,
ColumnConstraint, ColumnConstraint,
ColumnConstraintOrAlternation,
ColumnIntersection,
DatomsColumn, DatomsColumn,
DatomsTable, DatomsTable,
QualifiedAlias, QualifiedAlias,

View file

@ -194,6 +194,90 @@ pub enum ColumnConstraint {
HasType(TableAlias, ValueType), HasType(TableAlias, ValueType),
} }
#[derive(PartialEq, Eq, Debug)]
pub enum ColumnConstraintOrAlternation {
Constraint(ColumnConstraint),
Alternation(ColumnAlternation),
}
impl From<ColumnConstraint> 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<ColumnConstraintOrAlternation>);
impl From<Vec<ColumnConstraint>> for ColumnIntersection {
fn from(thing: Vec<ColumnConstraint>) -> 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<ColumnConstraintOrAlternation>;
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<ColumnIntersection>);
impl Default for ColumnAlternation {
fn default() -> Self {
ColumnAlternation(vec![])
}
}
impl IntoIterator for ColumnAlternation {
type Item = ColumnIntersection;
type IntoIter = ::std::vec::IntoIter<ColumnIntersection>;
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 { impl Debug for ColumnConstraint {
fn fmt(&self, f: &mut Formatter) -> ::std::fmt::Result { fn fmt(&self, f: &mut Formatter) -> ::std::fmt::Result {
use self::ColumnConstraint::*; use self::ColumnConstraint::*;

View file

@ -81,6 +81,9 @@ pub enum Constraint {
left: ColumnOrExpression, left: ColumnOrExpression,
right: ColumnOrExpression, right: ColumnOrExpression,
}, },
Or {
constraints: Vec<Constraint>,
},
And { And {
constraints: Vec<Constraint>, constraints: Vec<Constraint>,
}, },
@ -260,6 +263,11 @@ impl QueryFragment for Constraint {
}, },
&And { ref constraints } => { &And { ref constraints } => {
// An empty intersection is true.
if constraints.is_empty() {
out.push_sql("1");
return Ok(())
}
out.push_sql("("); out.push_sql("(");
interpose!(constraint, constraints, interpose!(constraint, constraints,
{ constraint.push_sql(out)? }, { constraint.push_sql(out)? },
@ -268,6 +276,20 @@ impl QueryFragment for Constraint {
Ok(()) 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 } => { &In { ref left, ref list } => {
left.push_sql(out)?; left.push_sql(out)?;
out.push_sql(" IN ("); out.push_sql(" IN (");

View file

@ -25,7 +25,10 @@ use mentat_query::{
use mentat_query_algebrizer::{ use mentat_query_algebrizer::{
AlgebraicQuery, AlgebraicQuery,
ColumnAlternation,
ColumnConstraint, ColumnConstraint,
ColumnConstraintOrAlternation,
ColumnIntersection,
ConjoiningClauses, ConjoiningClauses,
DatomsColumn, DatomsColumn,
DatomsTable, 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 { impl ToConstraint for ColumnConstraint {
fn to_constraint(self) -> Constraint { fn to_constraint(self) -> Constraint {
use self::ColumnConstraint::*; use self::ColumnConstraint::*;