(#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,
}
}
}
// 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]

View file

@ -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),
]);
}

View file

@ -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)]

View file

@ -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)
},
}
}
}

View file

@ -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() {

View file

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

View file

@ -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)
}

View file

@ -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);
}

View file

@ -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);