(#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,
|
&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]
|
||||||
|
|
|
@ -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),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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,28 +444,39 @@ pub struct CombinedProjection {
|
||||||
pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection {
|
pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection {
|
||||||
use self::FindSpec::*;
|
use self::FindSpec::*;
|
||||||
|
|
||||||
match query.find_spec {
|
if query.is_known_empty() {
|
||||||
FindColl(ref element) => {
|
// Do a few gyrations to produce empty results of the right kind for the query.
|
||||||
let (cols, templates) = project_elements(1, iter::once(element), query);
|
let empty = QueryResults::empty_factory(&query.find_spec);
|
||||||
CollProjector::combine(cols, templates)
|
let constant_projector = ConstantProjector::new(empty);
|
||||||
},
|
CombinedProjection {
|
||||||
|
sql_projection: Projection::One,
|
||||||
|
|
||||||
FindScalar(ref element) => {
|
datalog_projector: Box::new(constant_projector),
|
||||||
let (cols, templates) = project_elements(1, iter::once(element), query);
|
}
|
||||||
ScalarProjector::combine(cols, templates)
|
} 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) => {
|
FindScalar(ref element) => {
|
||||||
let column_count = query.find_spec.expected_column_count();
|
let (cols, templates) = project_elements(1, iter::once(element), query);
|
||||||
let (cols, templates) = project_elements(column_count, elements, query);
|
ScalarProjector::combine(cols, templates)
|
||||||
RelProjector::combine(column_count, cols, templates)
|
},
|
||||||
},
|
|
||||||
|
|
||||||
FindTuple(ref elements) => {
|
FindRel(ref elements) => {
|
||||||
let column_count = query.find_spec.expected_column_count();
|
let column_count = query.find_spec.expected_column_count();
|
||||||
let (cols, templates) = project_elements(column_count, elements, query);
|
let (cols, templates) = project_elements(column_count, elements, query);
|
||||||
TupleProjector::combine(column_count, cols, templates)
|
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 {
|
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,19 +332,15 @@ 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 ");
|
||||||
|
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.
|
// Guaranteed to be positive: u64.
|
||||||
if let Some(limit) = self.limit {
|
if let Some(limit) = self.limit {
|
||||||
out.push_sql(" LIMIT ");
|
out.push_sql(" 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() {
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue