(#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:
Richard Newman 2017-03-06 20:18:38 -08:00
parent b5867e9131
commit bf38105fef
9 changed files with 500 additions and 75 deletions

View file

@ -74,11 +74,13 @@ impl TypedValue {
&TypedValue::Keyword(_) => ValueType::Keyword, &TypedValue::Keyword(_) => ValueType::Keyword,
} }
} }
} }
// Put this here rather than in `db` simply because it's widely needed. // Put this here rather than in `db` simply because it's widely needed.
pub trait SQLValueType { pub trait SQLValueType {
fn value_type_tag(&self) -> i32; fn value_type_tag(&self) -> i32;
fn accommodates_integer(&self, int: i64) -> bool;
} }
impl SQLValueType for ValueType { impl SQLValueType for ValueType {
@ -94,6 +96,27 @@ impl SQLValueType for ValueType {
ValueType::Keyword => 13, 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] #[test]

View file

@ -134,23 +134,39 @@ pub fn default_table_aliaser() -> TableAliaser {
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub enum ColumnConstraint { pub enum ColumnConstraint {
EqualsColumn(QualifiedAlias, QualifiedAlias),
EqualsEntity(QualifiedAlias, Entid), EqualsEntity(QualifiedAlias, Entid),
EqualsValue(QualifiedAlias, TypedValue), 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 { impl Debug for ColumnConstraint {
fn fmt(&self, f: &mut Formatter) -> Result { fn fmt(&self, f: &mut Formatter) -> Result {
use self::ColumnConstraint::*; use self::ColumnConstraint::*;
match self { match self {
&EqualsColumn(ref qa1, ref qa2) => {
write!(f, "{:?} = {:?}", qa1, qa2)
}
&EqualsEntity(ref qa, ref entid) => { &EqualsEntity(ref qa, ref entid) => {
write!(f, "{:?} = entity({:?})", qa, entid) write!(f, "{:?} = entity({:?})", qa, entid)
} }
&EqualsValue(ref qa, ref typed_value) => { &EqualsValue(ref qa, ref typed_value) => {
write!(f, "{:?} = value({:?})", qa, 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 { pub struct ConjoiningClauses {
/// `true` if this set of clauses cannot yield results in the context of the current schema. /// `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". /// A function used to generate an alias for a table -- e.g., from "datoms" to "datoms123".
aliaser: TableAliaser, aliaser: TableAliaser,
@ -264,6 +280,10 @@ impl ConjoiningClauses {
self.constrain_column_to_entity(table, DatomsColumn::Attribute, attribute) 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. /// 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 /// Returns `false` if it's impossible for this type to apply (because there's a conflicting
/// type already known). /// type already known).
@ -601,7 +621,18 @@ impl ConjoiningClauses {
if let Some(ValueType::Ref) = value_type { if let Some(ValueType::Ref) = value_type {
self.constrain_column_to_entity(col.clone(), DatomsColumn::Value, i); self.constrain_column_to_entity(col.clone(), DatomsColumn::Value, i);
} else { } 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) => { PatternValuePlace::IdentOrKeyword(ref kw) => {
// If we know the valueType, then we can determine whether this is an ident or a // 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 // 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 // attribute, we can actually work backwards to the set of appropriate attributes
// from the type of the value itself! #292. // 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); 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. // TODO: implement expand_type_tags.
assert_eq!(cc.wheres, vec![ assert_eq!(cc.wheres, vec![
ColumnConstraint::EqualsValue(d0_v, TypedValue::Boolean(true)), ColumnConstraint::EqualsValue(d0_v, TypedValue::Boolean(true)),
ColumnConstraint::HasType("datoms00".to_string(), ValueType::Boolean),
]); ]);
} }

View file

@ -34,6 +34,9 @@ pub struct AlgebraicQuery {
} }
impl 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>) { pub fn apply_limit(&mut self, limit: Option<u64>) {
match self.limit { match self.limit {
None => self.limit = 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)] #[allow(dead_code)]

View file

@ -100,6 +100,36 @@ impl QueryResults {
&Rel(ref v) => v.len(), &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. type Index = i32; // See rusqlite::RowIndex.
@ -216,6 +246,24 @@ pub trait Projector {
fn project<'stmt>(&self, rows: Rows<'stmt>) -> Result<QueryResults>; 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 { struct ScalarProjector {
template: TypedIndex, template: TypedIndex,
} }
@ -396,6 +444,16 @@ pub struct CombinedProjection {
pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection { pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection {
use self::FindSpec::*; use self::FindSpec::*;
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,
datalog_projector: Box::new(constant_projector),
}
} else {
match query.find_spec { match query.find_spec {
FindColl(ref element) => { FindColl(ref element) => {
let (cols, templates) = project_elements(1, iter::once(element), query); let (cols, templates) = project_elements(1, iter::once(element), query);
@ -419,5 +477,6 @@ pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection {
TupleProjector::combine(column_count, cols, templates) TupleProjector::combine(column_count, cols, templates)
}, },
} }
}
} }

View file

@ -44,6 +44,7 @@ use mentat_sql::{
pub enum ColumnOrExpression { pub enum ColumnOrExpression {
Column(QualifiedAlias), Column(QualifiedAlias),
Entid(Entid), // Because it's so common. Entid(Entid), // Because it's so common.
Integer(i32), // We use these for type codes etc.
Value(TypedValue), Value(TypedValue),
} }
@ -64,11 +65,26 @@ pub enum Constraint {
Infix { Infix {
op: Op, op: Op,
left: ColumnOrExpression, left: ColumnOrExpression,
right: ColumnOrExpression right: ColumnOrExpression,
},
And {
constraints: Vec<Constraint>,
},
In {
left: ColumnOrExpression,
list: Vec<ColumnOrExpression>,
} }
} }
impl Constraint { 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 { pub fn equal(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint {
Constraint::Infix { Constraint::Infix {
op: Op("="), op: Op("="),
@ -86,6 +102,12 @@ enum JoinOp {
// Short-hand for a list of tables all inner-joined. // Short-hand for a list of tables all inner-joined.
pub struct TableList(pub Vec<SourceAlias>); pub struct TableList(pub Vec<SourceAlias>);
impl TableList {
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
pub struct Join { pub struct Join {
left: TableOrSubquery, left: TableOrSubquery,
op: JoinOp, op: JoinOp,
@ -102,6 +124,7 @@ enum TableOrSubquery {
pub enum FromClause { pub enum FromClause {
TableList(TableList), // Short-hand for a pile of inner joins. TableList(TableList), // Short-hand for a pile of inner joins.
Join(Join), Join(Join),
Nothing,
} }
pub struct SelectQuery { pub struct SelectQuery {
@ -119,6 +142,35 @@ fn push_column(qb: &mut QueryBuilder, col: &DatomsColumn) {
//--------------------------------------------------------- //---------------------------------------------------------
// Turn that representation into SQL. // 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 { impl QueryFragment for ColumnOrExpression {
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
use self::ColumnOrExpression::*; use self::ColumnOrExpression::*;
@ -133,6 +185,10 @@ impl QueryFragment for ColumnOrExpression {
out.push_sql(entid.to_string().as_str()); out.push_sql(entid.to_string().as_str());
Ok(()) Ok(())
}, },
&Integer(integer) => {
out.push_sql(integer.to_string().as_str());
Ok(())
},
&Value(ref v) => { &Value(ref v) => {
out.push_typed_value(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 { impl QueryFragment for Constraint {
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
use self::Constraint::*; use self::Constraint::*;
@ -195,7 +238,26 @@ impl QueryFragment for Constraint {
op.push_sql(out)?; op.push_sql(out)?;
out.push_sql(" "); out.push_sql(" ");
right.push_sql(out) 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 { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
use self::FromClause::*; use self::FromClause::*;
match self { match self {
&TableList(ref table_list) => table_list.push_sql(out), &TableList(ref table_list) => {
&Join(ref join) => join.push_sql(out), 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,18 +332,14 @@ impl QueryFragment for SelectQuery {
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
out.push_sql("SELECT "); out.push_sql("SELECT ");
self.projection.push_sql(out)?; self.projection.push_sql(out)?;
out.push_sql(" FROM ");
self.from.push_sql(out)?; self.from.push_sql(out)?;
if self.constraints.is_empty() { if !self.constraints.is_empty() {
return Ok(());
}
out.push_sql(" WHERE "); out.push_sql(" WHERE ");
interpose!(constraint, self.constraints, interpose!(constraint, self.constraints,
{ constraint.push_sql(out)? }, { constraint.push_sql(out)? },
{ out.push_sql(" AND ") }); { out.push_sql(" AND ") });
}
// Guaranteed to be positive: u64. // Guaranteed to be positive: u64.
if let Some(limit) = self.limit { if let Some(limit) = self.limit {
@ -294,6 +363,67 @@ mod tests {
use super::*; use super::*;
use mentat_query_algebrizer::DatomsTable; 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] #[test]
fn test_end_to_end() { fn test_end_to_end() {

View file

@ -23,6 +23,5 @@ pub use mentat_query_sql::{
pub use translate::{ pub use translate::{
cc_to_exists, cc_to_exists,
cc_to_select,
query_to_select, query_to_select,
}; };

View file

@ -10,6 +10,12 @@
#![allow(dead_code, unused_imports)] #![allow(dead_code, unused_imports)]
use mentat_core::{
SQLValueType,
TypedValue,
ValueType,
};
use mentat_query::{ use mentat_query::{
Element, Element,
FindSpec, FindSpec,
@ -64,45 +70,106 @@ impl ToConstraint for ColumnConstraint {
match self { match self {
EqualsEntity(qa, entid) => EqualsEntity(qa, entid) =>
Constraint::equal(qa.to_column(), ColumnOrExpression::Entid(entid)), Constraint::equal(qa.to_column(), ColumnOrExpression::Entid(entid)),
EqualsValue(qa, tv) => EqualsValue(qa, tv) =>
Constraint::equal(qa.to_column(), ColumnOrExpression::Value(tv)), Constraint::equal(qa.to_column(), ColumnOrExpression::Value(tv)),
EqualsColumn(left, right) => EqualsColumn(left, right) =>
Constraint::equal(left.to_column(), right.to_column()), 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 query: SelectQuery,
pub projector: Box<Projector>, 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 { 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 { SelectQuery {
projection: projection, projection: projection,
from: FromClause::TableList(TableList(cc.from)), from: from,
constraints: cc.wheres constraints: cc.wheres
.into_iter() .into_iter()
.map(|c| c.to_constraint()) .map(|c| c.to_constraint())
.collect(), .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 /// Consume a provided `ConjoiningClauses` to yield a new
/// `SelectQuery`. A projection list must also be provided. /// `ProjectedSelect`. A projection list must also be provided.
pub fn cc_to_select(projection: CombinedProjection, cc: ConjoiningClauses, limit: Option<u64>) -> CombinedSelectQuery { fn cc_to_select(projection: CombinedProjection, cc: ConjoiningClauses, limit: Option<u64>) -> ProjectedSelect {
let CombinedProjection { sql_projection, datalog_projector } = projection; let CombinedProjection { sql_projection, datalog_projector } = projection;
CombinedSelectQuery { ProjectedSelect {
query: cc_to_select_query(sql_projection, cc, limit), query: cc_to_select_query(sql_projection, cc, limit),
projector: datalog_projector, 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) 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)
}

View file

@ -41,6 +41,14 @@ fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
schema.schema_map.insert(e, a); 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] #[test]
fn test_coll() { fn test_coll() {
let mut schema = Schema::default(); let mut schema = Schema::default();
@ -51,10 +59,7 @@ fn test_coll() {
}); });
let input = r#"[:find [?x ...] :where [?x :foo/bar "yyy"]]"#; let input = r#"[:find [?x ...] :where [?x :foo/bar "yyy"]]"#;
let parsed = parse_find_string(input).expect("parse failed"); let SQLQuery { sql, args } = translate(&schema, input, None);
let algebrized = algebrize(&schema, parsed);
let select = query_to_select(algebrized);
let SQLQuery { sql, args } = select.query.to_sql_query().unwrap();
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0"); 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())]); 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 input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#;
let parsed = parse_find_string(input).expect("parse failed"); let SQLQuery { sql, args } = translate(&schema, input, None);
let algebrized = algebrize(&schema, parsed);
let select = query_to_select(algebrized);
let SQLQuery { sql, args } = select.query.to_sql_query().unwrap();
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0"); 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())]); 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 input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#;
let parsed = parse_find_string(input).expect("parse failed"); let SQLQuery { sql, args } = translate(&schema, input, 5);
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();
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 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())]); 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);
}

View file

@ -66,6 +66,12 @@ pub fn q_once<'sqlite, 'schema, 'query, T, U>
let parsed = parse_find_string(query)?; let parsed = parse_find_string(query)?;
let mut algebrized = algebrize(schema, parsed); 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()); algebrized.apply_limit(limit.into());
let select = query_to_select(algebrized); let select = query_to_select(algebrized);