(#362) Part 4: handle unknown attributes by expanding type codes. r=nalexander
Also, don't run any SQL at all if an algebrized query is known to return no results.
This commit is contained in:
parent
b5867e9131
commit
bf38105fef
9 changed files with 500 additions and 75 deletions
|
@ -74,11 +74,13 @@ impl TypedValue {
|
|||
&TypedValue::Keyword(_) => ValueType::Keyword,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Put this here rather than in `db` simply because it's widely needed.
|
||||
pub trait SQLValueType {
|
||||
fn value_type_tag(&self) -> i32;
|
||||
fn accommodates_integer(&self, int: i64) -> bool;
|
||||
}
|
||||
|
||||
impl SQLValueType for ValueType {
|
||||
|
@ -94,6 +96,27 @@ impl SQLValueType for ValueType {
|
|||
ValueType::Keyword => 13,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the provided integer is in the SQLite value space of this type. For
|
||||
/// example, `1` is how we encode `true`.
|
||||
///
|
||||
/// ```
|
||||
/// use mentat_core::{ValueType, SQLValueType};
|
||||
/// assert!(ValueType::Boolean.accommodates_integer(1));
|
||||
/// assert!(!ValueType::Boolean.accommodates_integer(-1));
|
||||
/// assert!(!ValueType::Boolean.accommodates_integer(10));
|
||||
/// assert!(!ValueType::String.accommodates_integer(10));
|
||||
/// ```
|
||||
fn accommodates_integer(&self, int: i64) -> bool {
|
||||
use ValueType::*;
|
||||
match *self {
|
||||
Instant | Long | Double => true,
|
||||
Ref => int >= 0,
|
||||
Boolean => (int == 0) || (int == 1),
|
||||
ValueType::String => false,
|
||||
Keyword => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -134,23 +134,39 @@ pub fn default_table_aliaser() -> TableAliaser {
|
|||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum ColumnConstraint {
|
||||
EqualsColumn(QualifiedAlias, QualifiedAlias),
|
||||
EqualsEntity(QualifiedAlias, Entid),
|
||||
EqualsValue(QualifiedAlias, TypedValue),
|
||||
EqualsColumn(QualifiedAlias, QualifiedAlias),
|
||||
|
||||
// This is different: a numeric value can only apply to the 'v' column, and it implicitly
|
||||
// constrains the `value_type_tag` column. For instance, a primitive long on `datoms00` of `5`
|
||||
// cannot be a boolean, so `datoms00.value_type_tag` must be in the set `#{0, 4, 5}`.
|
||||
// Note that `5 = 5.0` in SQLite, and we preserve that here.
|
||||
EqualsPrimitiveLong(TableAlias, i64),
|
||||
|
||||
HasType(TableAlias, ValueType),
|
||||
}
|
||||
|
||||
impl Debug for ColumnConstraint {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
use self::ColumnConstraint::*;
|
||||
match self {
|
||||
&EqualsColumn(ref qa1, ref qa2) => {
|
||||
write!(f, "{:?} = {:?}", qa1, qa2)
|
||||
}
|
||||
&EqualsEntity(ref qa, ref entid) => {
|
||||
write!(f, "{:?} = entity({:?})", qa, entid)
|
||||
}
|
||||
&EqualsValue(ref qa, ref typed_value) => {
|
||||
write!(f, "{:?} = value({:?})", qa, typed_value)
|
||||
}
|
||||
&EqualsColumn(ref qa1, ref qa2) => {
|
||||
write!(f, "{:?} = {:?}", qa1, qa2)
|
||||
|
||||
&EqualsPrimitiveLong(ref qa, value) => {
|
||||
write!(f, "{:?}.v = primitive({:?})", qa, value)
|
||||
}
|
||||
|
||||
&HasType(ref qa, value_type) => {
|
||||
write!(f, "{:?}.value_type_tag = {:?}", qa, value_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -181,7 +197,7 @@ impl Debug for ColumnConstraint {
|
|||
///---------------------------------------------------------------------------------------
|
||||
pub struct ConjoiningClauses {
|
||||
/// `true` if this set of clauses cannot yield results in the context of the current schema.
|
||||
is_known_empty: bool,
|
||||
pub is_known_empty: bool,
|
||||
|
||||
/// A function used to generate an alias for a table -- e.g., from "datoms" to "datoms123".
|
||||
aliaser: TableAliaser,
|
||||
|
@ -264,6 +280,10 @@ impl ConjoiningClauses {
|
|||
self.constrain_column_to_entity(table, DatomsColumn::Attribute, attribute)
|
||||
}
|
||||
|
||||
pub fn constrain_value_to_numeric(&mut self, table: TableAlias, value: i64) {
|
||||
self.wheres.push(ColumnConstraint::EqualsPrimitiveLong(table, value))
|
||||
}
|
||||
|
||||
/// Constrains the var if there's no existing type.
|
||||
/// Returns `false` if it's impossible for this type to apply (because there's a conflicting
|
||||
/// type already known).
|
||||
|
@ -601,7 +621,18 @@ impl ConjoiningClauses {
|
|||
if let Some(ValueType::Ref) = value_type {
|
||||
self.constrain_column_to_entity(col.clone(), DatomsColumn::Value, i);
|
||||
} else {
|
||||
unimplemented!();
|
||||
// If we have a pattern like:
|
||||
//
|
||||
// `[123 ?a 1]`
|
||||
//
|
||||
// then `1` could be an entid (ref), a long, a boolean, or an instant.
|
||||
//
|
||||
// We represent these constraints during execution:
|
||||
//
|
||||
// - Constraining the value column to the plain numeric value '1'.
|
||||
// - Constraining its type column to one of a set of types.
|
||||
//
|
||||
self.constrain_value_to_numeric(col.clone(), i);
|
||||
},
|
||||
PatternValuePlace::IdentOrKeyword(ref kw) => {
|
||||
// If we know the valueType, then we can determine whether this is an ident or a
|
||||
|
@ -637,7 +668,26 @@ impl ConjoiningClauses {
|
|||
// TODO: if we don't know the type of the attribute because we don't know the
|
||||
// attribute, we can actually work backwards to the set of appropriate attributes
|
||||
// from the type of the value itself! #292.
|
||||
let typed_value_type = typed_value.value_type();
|
||||
self.constrain_column_to_constant(col.clone(), DatomsColumn::Value, typed_value);
|
||||
|
||||
// If we can't already determine the range of values in the DB from the attribute,
|
||||
// then we must also constrain the type tag.
|
||||
//
|
||||
// Input values might be:
|
||||
//
|
||||
// - A long. This is handled by EntidOrInteger.
|
||||
// - A boolean. This is unambiguous.
|
||||
// - A double. This is currently unambiguous, though note that SQLite will equate 5.0 with 5.
|
||||
// - A string. This is unambiguous.
|
||||
// - A keyword. This is unambiguous.
|
||||
//
|
||||
// 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));
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -789,6 +839,7 @@ mod testing {
|
|||
// TODO: implement expand_type_tags.
|
||||
assert_eq!(cc.wheres, vec![
|
||||
ColumnConstraint::EqualsValue(d0_v, TypedValue::Boolean(true)),
|
||||
ColumnConstraint::HasType("datoms00".to_string(), ValueType::Boolean),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,9 @@ pub struct AlgebraicQuery {
|
|||
}
|
||||
|
||||
impl AlgebraicQuery {
|
||||
/**
|
||||
* Apply a new limit to this query, if one is provided and any existing limit is larger.
|
||||
*/
|
||||
pub fn apply_limit(&mut self, limit: Option<u64>) {
|
||||
match self.limit {
|
||||
None => self.limit = limit,
|
||||
|
@ -47,6 +50,10 @@ impl AlgebraicQuery {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn is_known_empty(&self) -> bool {
|
||||
self.cc.is_known_empty
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
|
@ -100,6 +100,36 @@ impl QueryResults {
|
|||
&Rel(ref v) => v.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
use QueryResults::*;
|
||||
match self {
|
||||
&Scalar(ref o) => o.is_none(),
|
||||
&Tuple(ref o) => o.is_none(),
|
||||
&Coll(ref v) => v.is_empty(),
|
||||
&Rel(ref v) => v.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty(spec: &FindSpec) -> QueryResults {
|
||||
use self::FindSpec::*;
|
||||
match spec {
|
||||
&FindScalar(_) => QueryResults::Scalar(None),
|
||||
&FindTuple(_) => QueryResults::Tuple(None),
|
||||
&FindColl(_) => QueryResults::Coll(vec![]),
|
||||
&FindRel(_) => QueryResults::Rel(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_factory(spec: &FindSpec) -> Box<Fn() -> QueryResults> {
|
||||
use self::FindSpec::*;
|
||||
match spec {
|
||||
&FindScalar(_) => Box::new(|| QueryResults::Scalar(None)),
|
||||
&FindTuple(_) => Box::new(|| QueryResults::Tuple(None)),
|
||||
&FindColl(_) => Box::new(|| QueryResults::Coll(vec![])),
|
||||
&FindRel(_) => Box::new(|| QueryResults::Rel(vec![])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Index = i32; // See rusqlite::RowIndex.
|
||||
|
@ -216,6 +246,24 @@ pub trait Projector {
|
|||
fn project<'stmt>(&self, rows: Rows<'stmt>) -> Result<QueryResults>;
|
||||
}
|
||||
|
||||
/// A projector that produces a `QueryResult` containing fixed data.
|
||||
/// Takes a boxed function that should return an empty result set of the desired type.
|
||||
struct ConstantProjector {
|
||||
results_factory: Box<Fn() -> QueryResults>,
|
||||
}
|
||||
|
||||
impl ConstantProjector {
|
||||
fn new(results_factory: Box<Fn() -> QueryResults>) -> ConstantProjector {
|
||||
ConstantProjector { results_factory: results_factory }
|
||||
}
|
||||
}
|
||||
|
||||
impl Projector for ConstantProjector {
|
||||
fn project<'stmt>(&self, _: Rows<'stmt>) -> Result<QueryResults> {
|
||||
Ok((self.results_factory)())
|
||||
}
|
||||
}
|
||||
|
||||
struct ScalarProjector {
|
||||
template: TypedIndex,
|
||||
}
|
||||
|
@ -396,28 +444,39 @@ pub struct CombinedProjection {
|
|||
pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection {
|
||||
use self::FindSpec::*;
|
||||
|
||||
match query.find_spec {
|
||||
FindColl(ref element) => {
|
||||
let (cols, templates) = project_elements(1, iter::once(element), query);
|
||||
CollProjector::combine(cols, templates)
|
||||
},
|
||||
if query.is_known_empty() {
|
||||
// Do a few gyrations to produce empty results of the right kind for the query.
|
||||
let empty = QueryResults::empty_factory(&query.find_spec);
|
||||
let constant_projector = ConstantProjector::new(empty);
|
||||
CombinedProjection {
|
||||
sql_projection: Projection::One,
|
||||
|
||||
FindScalar(ref element) => {
|
||||
let (cols, templates) = project_elements(1, iter::once(element), query);
|
||||
ScalarProjector::combine(cols, templates)
|
||||
},
|
||||
datalog_projector: Box::new(constant_projector),
|
||||
}
|
||||
} else {
|
||||
match query.find_spec {
|
||||
FindColl(ref element) => {
|
||||
let (cols, templates) = project_elements(1, iter::once(element), query);
|
||||
CollProjector::combine(cols, templates)
|
||||
},
|
||||
|
||||
FindRel(ref elements) => {
|
||||
let column_count = query.find_spec.expected_column_count();
|
||||
let (cols, templates) = project_elements(column_count, elements, query);
|
||||
RelProjector::combine(column_count, cols, templates)
|
||||
},
|
||||
FindScalar(ref element) => {
|
||||
let (cols, templates) = project_elements(1, iter::once(element), query);
|
||||
ScalarProjector::combine(cols, templates)
|
||||
},
|
||||
|
||||
FindTuple(ref elements) => {
|
||||
let column_count = query.find_spec.expected_column_count();
|
||||
let (cols, templates) = project_elements(column_count, elements, query);
|
||||
TupleProjector::combine(column_count, cols, templates)
|
||||
},
|
||||
FindRel(ref elements) => {
|
||||
let column_count = query.find_spec.expected_column_count();
|
||||
let (cols, templates) = project_elements(column_count, elements, query);
|
||||
RelProjector::combine(column_count, cols, templates)
|
||||
},
|
||||
|
||||
FindTuple(ref elements) => {
|
||||
let column_count = query.find_spec.expected_column_count();
|
||||
let (cols, templates) = project_elements(column_count, elements, query);
|
||||
TupleProjector::combine(column_count, cols, templates)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ use mentat_sql::{
|
|||
pub enum ColumnOrExpression {
|
||||
Column(QualifiedAlias),
|
||||
Entid(Entid), // Because it's so common.
|
||||
Integer(i32), // We use these for type codes etc.
|
||||
Value(TypedValue),
|
||||
}
|
||||
|
||||
|
@ -64,11 +65,26 @@ pub enum Constraint {
|
|||
Infix {
|
||||
op: Op,
|
||||
left: ColumnOrExpression,
|
||||
right: ColumnOrExpression
|
||||
right: ColumnOrExpression,
|
||||
},
|
||||
And {
|
||||
constraints: Vec<Constraint>,
|
||||
},
|
||||
In {
|
||||
left: ColumnOrExpression,
|
||||
list: Vec<ColumnOrExpression>,
|
||||
}
|
||||
}
|
||||
|
||||
impl Constraint {
|
||||
pub fn not_equal(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint {
|
||||
Constraint::Infix {
|
||||
op: Op("<>"), // ANSI SQL for future-proofing!
|
||||
left: left,
|
||||
right: right,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equal(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint {
|
||||
Constraint::Infix {
|
||||
op: Op("="),
|
||||
|
@ -86,6 +102,12 @@ enum JoinOp {
|
|||
// Short-hand for a list of tables all inner-joined.
|
||||
pub struct TableList(pub Vec<SourceAlias>);
|
||||
|
||||
impl TableList {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Join {
|
||||
left: TableOrSubquery,
|
||||
op: JoinOp,
|
||||
|
@ -102,6 +124,7 @@ enum TableOrSubquery {
|
|||
pub enum FromClause {
|
||||
TableList(TableList), // Short-hand for a pile of inner joins.
|
||||
Join(Join),
|
||||
Nothing,
|
||||
}
|
||||
|
||||
pub struct SelectQuery {
|
||||
|
@ -119,6 +142,35 @@ fn push_column(qb: &mut QueryBuilder, col: &DatomsColumn) {
|
|||
//---------------------------------------------------------
|
||||
// Turn that representation into SQL.
|
||||
|
||||
|
||||
/// A helper macro to sequentially process an iterable sequence,
|
||||
/// evaluating a block between each pair of items.
|
||||
///
|
||||
/// This is used to simply and efficiently produce output like
|
||||
///
|
||||
/// ```sql
|
||||
/// 1, 2, 3
|
||||
/// ```
|
||||
///
|
||||
/// or
|
||||
///
|
||||
/// ```sql
|
||||
/// x = 1 AND y = 2
|
||||
/// ```
|
||||
///
|
||||
/// without producing an intermediate string sequence.
|
||||
macro_rules! interpose {
|
||||
( $name: ident, $across: expr, $body: block, $inter: block ) => {
|
||||
let mut seq = $across.iter();
|
||||
if let Some($name) = seq.next() {
|
||||
$body;
|
||||
for $name in seq {
|
||||
$inter;
|
||||
$body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl QueryFragment for ColumnOrExpression {
|
||||
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
||||
use self::ColumnOrExpression::*;
|
||||
|
@ -133,6 +185,10 @@ impl QueryFragment for ColumnOrExpression {
|
|||
out.push_sql(entid.to_string().as_str());
|
||||
Ok(())
|
||||
},
|
||||
&Integer(integer) => {
|
||||
out.push_sql(integer.to_string().as_str());
|
||||
Ok(())
|
||||
},
|
||||
&Value(ref v) => {
|
||||
out.push_typed_value(v)
|
||||
},
|
||||
|
@ -172,19 +228,6 @@ impl QueryFragment for Op {
|
|||
}
|
||||
}
|
||||
|
||||
macro_rules! interpose {
|
||||
( $name: ident, $across: expr, $body: block, $inter: block ) => {
|
||||
let mut seq = $across.iter();
|
||||
if let Some($name) = seq.next() {
|
||||
$body;
|
||||
for $name in seq {
|
||||
$inter;
|
||||
$body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryFragment for Constraint {
|
||||
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
||||
use self::Constraint::*;
|
||||
|
@ -195,7 +238,26 @@ impl QueryFragment for Constraint {
|
|||
op.push_sql(out)?;
|
||||
out.push_sql(" ");
|
||||
right.push_sql(out)
|
||||
}
|
||||
},
|
||||
|
||||
&And { ref constraints } => {
|
||||
out.push_sql("(");
|
||||
interpose!(constraint, constraints,
|
||||
{ constraint.push_sql(out)? },
|
||||
{ out.push_sql(" AND ") });
|
||||
out.push_sql(")");
|
||||
Ok(())
|
||||
},
|
||||
|
||||
&In { ref left, ref list } => {
|
||||
left.push_sql(out)?;
|
||||
out.push_sql(" IN (");
|
||||
interpose!(item, list,
|
||||
{ item.push_sql(out)? },
|
||||
{ out.push_sql(", ") });
|
||||
out.push_sql(")");
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -249,8 +311,19 @@ impl QueryFragment for FromClause {
|
|||
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
||||
use self::FromClause::*;
|
||||
match self {
|
||||
&TableList(ref table_list) => table_list.push_sql(out),
|
||||
&Join(ref join) => join.push_sql(out),
|
||||
&TableList(ref table_list) => {
|
||||
if table_list.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
out.push_sql(" FROM ");
|
||||
table_list.push_sql(out)
|
||||
}
|
||||
},
|
||||
&Join(ref join) => {
|
||||
out.push_sql(" FROM ");
|
||||
join.push_sql(out)
|
||||
},
|
||||
&Nothing => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -259,19 +332,15 @@ impl QueryFragment for SelectQuery {
|
|||
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
||||
out.push_sql("SELECT ");
|
||||
self.projection.push_sql(out)?;
|
||||
|
||||
out.push_sql(" FROM ");
|
||||
self.from.push_sql(out)?;
|
||||
|
||||
if self.constraints.is_empty() {
|
||||
return Ok(());
|
||||
if !self.constraints.is_empty() {
|
||||
out.push_sql(" WHERE ");
|
||||
interpose!(constraint, self.constraints,
|
||||
{ constraint.push_sql(out)? },
|
||||
{ out.push_sql(" AND ") });
|
||||
}
|
||||
|
||||
out.push_sql(" WHERE ");
|
||||
interpose!(constraint, self.constraints,
|
||||
{ constraint.push_sql(out)? },
|
||||
{ out.push_sql(" AND ") });
|
||||
|
||||
// Guaranteed to be positive: u64.
|
||||
if let Some(limit) = self.limit {
|
||||
out.push_sql(" LIMIT ");
|
||||
|
@ -294,6 +363,67 @@ mod tests {
|
|||
use super::*;
|
||||
use mentat_query_algebrizer::DatomsTable;
|
||||
|
||||
fn build_constraint(c: Constraint) -> String {
|
||||
let mut builder = SQLiteQueryBuilder::new();
|
||||
c.push_sql(&mut builder)
|
||||
.map(|_| builder.finish())
|
||||
.unwrap().sql
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_constraint() {
|
||||
let none = Constraint::In {
|
||||
left: ColumnOrExpression::Column(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value)),
|
||||
list: vec![],
|
||||
};
|
||||
|
||||
let one = Constraint::In {
|
||||
left: ColumnOrExpression::Column(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value)),
|
||||
list: vec![
|
||||
ColumnOrExpression::Entid(123),
|
||||
],
|
||||
};
|
||||
|
||||
let three = Constraint::In {
|
||||
left: ColumnOrExpression::Column(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value)),
|
||||
list: vec![
|
||||
ColumnOrExpression::Entid(123),
|
||||
ColumnOrExpression::Entid(456),
|
||||
ColumnOrExpression::Entid(789),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!("`datoms01`.v IN ()", build_constraint(none));
|
||||
assert_eq!("`datoms01`.v IN (123)", build_constraint(one));
|
||||
assert_eq!("`datoms01`.v IN (123, 456, 789)", build_constraint(three));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_and_constraint() {
|
||||
let c = Constraint::And {
|
||||
constraints: vec![
|
||||
Constraint::And {
|
||||
constraints: vec![
|
||||
Constraint::Infix {
|
||||
op: Op("="),
|
||||
left: ColumnOrExpression::Entid(123),
|
||||
right: ColumnOrExpression::Entid(456),
|
||||
},
|
||||
Constraint::Infix {
|
||||
op: Op("="),
|
||||
left: ColumnOrExpression::Entid(789),
|
||||
right: ColumnOrExpression::Entid(246),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Two sets of parens: the outermost AND only has one child,
|
||||
// but still contributes parens.
|
||||
assert_eq!("((123 = 456 AND 789 = 246))", build_constraint(c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_end_to_end() {
|
||||
|
||||
|
|
|
@ -23,6 +23,5 @@ pub use mentat_query_sql::{
|
|||
|
||||
pub use translate::{
|
||||
cc_to_exists,
|
||||
cc_to_select,
|
||||
query_to_select,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,12 @@
|
|||
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use mentat_core::{
|
||||
SQLValueType,
|
||||
TypedValue,
|
||||
ValueType,
|
||||
};
|
||||
|
||||
use mentat_query::{
|
||||
Element,
|
||||
FindSpec,
|
||||
|
@ -64,45 +70,106 @@ impl ToConstraint for ColumnConstraint {
|
|||
match self {
|
||||
EqualsEntity(qa, entid) =>
|
||||
Constraint::equal(qa.to_column(), ColumnOrExpression::Entid(entid)),
|
||||
|
||||
EqualsValue(qa, tv) =>
|
||||
Constraint::equal(qa.to_column(), ColumnOrExpression::Value(tv)),
|
||||
|
||||
EqualsColumn(left, right) =>
|
||||
Constraint::equal(left.to_column(), right.to_column()),
|
||||
|
||||
EqualsPrimitiveLong(table, value) => {
|
||||
let value_column = QualifiedAlias(table.clone(), DatomsColumn::Value).to_column();
|
||||
let tag_column = QualifiedAlias(table, DatomsColumn::ValueTypeTag).to_column();
|
||||
|
||||
/// A bare long in a query might match a ref, an instant, a long (obviously), or a
|
||||
/// double. If it's negative, it can't match a ref, but that's OK -- it won't!
|
||||
///
|
||||
/// However, '1' and '0' are used to represent booleans, and some integers are also
|
||||
/// used to represent FTS values. We don't want to accidentally match those.
|
||||
///
|
||||
/// We ask `SQLValueType` whether this value is in range for how booleans are
|
||||
/// represented in the database.
|
||||
///
|
||||
/// We only hit this code path when the attribute is unknown, so we're querying
|
||||
/// `all_datoms`. That means we don't see FTS IDs at all -- they're transparently
|
||||
/// replaced by their strings. If that changes, then you should also exclude the
|
||||
/// string type code (10) here.
|
||||
let must_exclude_boolean = ValueType::Boolean.accommodates_integer(value);
|
||||
if must_exclude_boolean {
|
||||
Constraint::And {
|
||||
constraints: vec![
|
||||
Constraint::equal(value_column,
|
||||
ColumnOrExpression::Value(TypedValue::Long(value))),
|
||||
Constraint::not_equal(tag_column,
|
||||
ColumnOrExpression::Integer(ValueType::Boolean.value_type_tag())),
|
||||
],
|
||||
}
|
||||
} else {
|
||||
Constraint::equal(value_column, ColumnOrExpression::Value(TypedValue::Long(value)))
|
||||
}
|
||||
},
|
||||
|
||||
HasType(table, value_type) => {
|
||||
let column = QualifiedAlias(table, DatomsColumn::ValueTypeTag).to_column();
|
||||
Constraint::equal(column, ColumnOrExpression::Integer(value_type.value_type_tag()))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CombinedSelectQuery {
|
||||
pub struct ProjectedSelect{
|
||||
pub query: SelectQuery,
|
||||
pub projector: Box<Projector>,
|
||||
}
|
||||
|
||||
/// Returns a `SelectQuery` that queries for the provided `cc`. Note that this _always_ returns a
|
||||
/// query that runs SQL. The next level up the call stack can check for known-empty queries if
|
||||
/// needed.
|
||||
fn cc_to_select_query<T: Into<Option<u64>>>(projection: Projection, cc: ConjoiningClauses, limit: T) -> SelectQuery {
|
||||
let from = if cc.from.is_empty() {
|
||||
FromClause::Nothing
|
||||
} else {
|
||||
FromClause::TableList(TableList(cc.from))
|
||||
};
|
||||
SelectQuery {
|
||||
projection: projection,
|
||||
from: FromClause::TableList(TableList(cc.from)),
|
||||
from: from,
|
||||
constraints: cc.wheres
|
||||
.into_iter()
|
||||
.map(|c| c.to_constraint())
|
||||
.collect(),
|
||||
limit: limit.into(),
|
||||
limit: if cc.is_known_empty { Some(0) } else { limit.into() },
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a query that projects `1` if the `cc` matches the store, and returns no results
|
||||
/// if it doesn't.
|
||||
pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery {
|
||||
if cc.is_known_empty {
|
||||
// In this case we can produce a very simple query that returns no results.
|
||||
SelectQuery {
|
||||
projection: Projection::One,
|
||||
from: FromClause::Nothing,
|
||||
constraints: vec![],
|
||||
limit: Some(0),
|
||||
}
|
||||
} else {
|
||||
cc_to_select_query(Projection::One, cc, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume a provided `ConjoiningClauses` to yield a new
|
||||
/// `SelectQuery`. A projection list must also be provided.
|
||||
pub fn cc_to_select(projection: CombinedProjection, cc: ConjoiningClauses, limit: Option<u64>) -> CombinedSelectQuery {
|
||||
/// `ProjectedSelect`. A projection list must also be provided.
|
||||
fn cc_to_select(projection: CombinedProjection, cc: ConjoiningClauses, limit: Option<u64>) -> ProjectedSelect {
|
||||
let CombinedProjection { sql_projection, datalog_projector } = projection;
|
||||
CombinedSelectQuery {
|
||||
ProjectedSelect {
|
||||
query: cc_to_select_query(sql_projection, cc, limit),
|
||||
projector: datalog_projector,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_to_select(query: AlgebraicQuery) -> CombinedSelectQuery {
|
||||
/// Consume a provided `AlgebraicQuery` to yield a new
|
||||
/// `ProjectedSelect`.
|
||||
pub fn query_to_select(query: AlgebraicQuery) -> ProjectedSelect {
|
||||
cc_to_select(query_projection(&query), query.cc, query.limit)
|
||||
}
|
||||
|
||||
pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery {
|
||||
cc_to_select_query(Projection::One, cc, 1)
|
||||
}
|
||||
|
|
|
@ -41,6 +41,14 @@ fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
|
|||
schema.schema_map.insert(e, a);
|
||||
}
|
||||
|
||||
fn translate<T: Into<Option<u64>>>(schema: &Schema, input: &'static str, limit: T) -> SQLQuery {
|
||||
let parsed = parse_find_string(input).expect("parse failed");
|
||||
let mut algebrized = algebrize(schema, parsed);
|
||||
algebrized.apply_limit(limit.into());
|
||||
let select = query_to_select(algebrized);
|
||||
select.query.to_sql_query().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coll() {
|
||||
let mut schema = Schema::default();
|
||||
|
@ -51,10 +59,7 @@ fn test_coll() {
|
|||
});
|
||||
|
||||
let input = r#"[:find [?x ...] :where [?x :foo/bar "yyy"]]"#;
|
||||
let parsed = parse_find_string(input).expect("parse failed");
|
||||
let algebrized = algebrize(&schema, parsed);
|
||||
let select = query_to_select(algebrized);
|
||||
let SQLQuery { sql, args } = select.query.to_sql_query().unwrap();
|
||||
let SQLQuery { sql, args } = translate(&schema, input, None);
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0");
|
||||
assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]);
|
||||
}
|
||||
|
@ -69,10 +74,7 @@ fn test_rel() {
|
|||
});
|
||||
|
||||
let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#;
|
||||
let parsed = parse_find_string(input).expect("parse failed");
|
||||
let algebrized = algebrize(&schema, parsed);
|
||||
let select = query_to_select(algebrized);
|
||||
let SQLQuery { sql, args } = select.query.to_sql_query().unwrap();
|
||||
let SQLQuery { sql, args } = translate(&schema, input, None);
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0");
|
||||
assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]);
|
||||
}
|
||||
|
@ -87,11 +89,92 @@ fn test_limit() {
|
|||
});
|
||||
|
||||
let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#;
|
||||
let parsed = parse_find_string(input).expect("parse failed");
|
||||
let mut algebrized = algebrize(&schema, parsed);
|
||||
algebrized.limit = Some(5);
|
||||
let select = query_to_select(algebrized);
|
||||
let SQLQuery { sql, args } = select.query.to_sql_query().unwrap();
|
||||
let SQLQuery { sql, args } = translate(&schema, input, 5);
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 5");
|
||||
assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_attribute_keyword_value() {
|
||||
let schema = Schema::default();
|
||||
|
||||
let input = r#"[:find ?x :where [?x _ :ab/yyy]]"#;
|
||||
let SQLQuery { sql, args } = translate(&schema, input, None);
|
||||
|
||||
// Only match keywords, not strings: tag = 13.
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = $v0 AND `datoms00`.value_type_tag = 13");
|
||||
assert_eq!(args, vec![("$v0".to_string(), ":ab/yyy".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_attribute_string_value() {
|
||||
let schema = Schema::default();
|
||||
|
||||
let input = r#"[:find ?x :where [?x _ "horses"]]"#;
|
||||
let SQLQuery { sql, args } = translate(&schema, input, None);
|
||||
|
||||
// We expect all_datoms because we're querying for a string. Magic, that.
|
||||
// We don't want keywords etc., so tag = 10.
|
||||
assert_eq!(sql, "SELECT `all_datoms00`.e AS `?x` FROM `all_datoms` AS `all_datoms00` WHERE `all_datoms00`.v = $v0 AND `all_datoms00`.value_type_tag = 10");
|
||||
assert_eq!(args, vec![("$v0".to_string(), "horses".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_attribute_double_value() {
|
||||
let schema = Schema::default();
|
||||
|
||||
let input = r#"[:find ?x :where [?x _ 9.95]]"#;
|
||||
let SQLQuery { sql, args } = translate(&schema, input, None);
|
||||
|
||||
// In general, doubles _could_ be 1.0, which might match a boolean or a ref. Set tag = 5 to
|
||||
// make sure we only match numbers.
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = 9.95 AND `datoms00`.value_type_tag = 5");
|
||||
assert_eq!(args, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_attribute_integer_value() {
|
||||
let schema = Schema::default();
|
||||
|
||||
let negative = r#"[:find ?x :where [?x _ -1]]"#;
|
||||
let zero = r#"[:find ?x :where [?x _ 0]]"#;
|
||||
let one = r#"[:find ?x :where [?x _ 1]]"#;
|
||||
let two = r#"[:find ?x :where [?x _ 2]]"#;
|
||||
|
||||
// Can't match boolean; no need to filter it out.
|
||||
let SQLQuery { sql, args } = translate(&schema, negative, None);
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = -1");
|
||||
assert_eq!(args, vec![]);
|
||||
|
||||
// Excludes booleans.
|
||||
let SQLQuery { sql, args } = translate(&schema, zero, None);
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE (`datoms00`.v = 0 AND `datoms00`.value_type_tag <> 1)");
|
||||
assert_eq!(args, vec![]);
|
||||
|
||||
// Excludes booleans.
|
||||
let SQLQuery { sql, args } = translate(&schema, one, None);
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE (`datoms00`.v = 1 AND `datoms00`.value_type_tag <> 1)");
|
||||
assert_eq!(args, vec![]);
|
||||
|
||||
// Can't match boolean; no need to filter it out.
|
||||
let SQLQuery { sql, args } = translate(&schema, two, None);
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = 2");
|
||||
assert_eq!(args, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_ident() {
|
||||
let schema = Schema::default();
|
||||
|
||||
let impossible = r#"[:find ?x :where [?x :db/ident :no/exist]]"#;
|
||||
let parsed = parse_find_string(impossible).expect("parse failed");
|
||||
let algebrized = algebrize(&schema, parsed);
|
||||
|
||||
// This query cannot return results: the ident doesn't resolve for a ref-typed attribute.
|
||||
assert!(algebrized.is_known_empty());
|
||||
|
||||
// If you insist…
|
||||
let select = query_to_select(algebrized);
|
||||
let sql = select.query.to_sql_query().unwrap().sql;
|
||||
assert_eq!("SELECT 1 LIMIT 0", sql);
|
||||
}
|
||||
|
|
|
@ -66,6 +66,12 @@ pub fn q_once<'sqlite, 'schema, 'query, T, U>
|
|||
|
||||
let parsed = parse_find_string(query)?;
|
||||
let mut algebrized = algebrize(schema, parsed);
|
||||
|
||||
if algebrized.is_known_empty() {
|
||||
// We don't need to do any SQL work at all.
|
||||
return Ok(QueryResults::empty(&algebrized.find_spec));
|
||||
}
|
||||
|
||||
algebrized.apply_limit(limit.into());
|
||||
|
||||
let select = query_to_select(algebrized);
|
||||
|
|
Loading…
Reference in a new issue