2017-02-16 23:39:19 +00:00
// Copyright 2016 Mozilla
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
// this file except in compliance with the License. You may obtain a copy of the
// License at http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
2017-04-19 23:16:19 +00:00
extern crate regex ;
2017-03-06 22:40:10 +00:00
extern crate mentat_core ;
extern crate mentat_query ;
extern crate mentat_query_algebrizer ;
extern crate mentat_sql ;
2017-02-16 23:39:19 +00:00
2017-04-28 09:44:11 +00:00
use std ::boxed ::Box ;
2017-02-22 03:57:00 +00:00
use mentat_core ::{
Entid ,
2018-01-29 22:29:16 +00:00
SQLTypeAffinity ,
2018-03-12 22:18:50 +00:00
TypedValue ,
ValueType ,
2017-02-22 03:57:00 +00:00
} ;
Implement :order. (#415) (#416) r=nalexander
This adds an `:order` keyword to `:find`.
If present, the results of the query will be an ordered set, rather than
an unordered set; rows will appear in an ordered defined by each
`:order` entry.
Each can be one of three things:
- A var, `?x`, meaning "order by ?x ascending".
- A pair, `(asc ?x)`, meaning "order by ?x ascending".
- A pair, `(desc ?x)`, meaning "order by ?x descending".
Values will be ordered in this sequence for asc, and in reverse for desc:
1. Entity IDs, in ascending numerical order.
2. Booleans, false then true.
3. Timestamps, in ascending numerical order.
4. Longs and doubles, intermixed, in ascending numerical order.
5. Strings, in ascending lexicographic order.
6. Keywords, in ascending lexicographic order, considering the entire
ns/name pair as a single string separated by '/'.
Subcommits:
Pre: make bound_value public.
Pre: generalize ErrorKind::UnboundVariable for use in order.
Part 1: parse (direction, var) pairs.
Part 2: parse :order clause into FindQuery.
Part 3: include order variables in algebrized query.
We add order variables to :with, so we can reuse its type tag projection
logic, and so that we can phrase ordering in terms of variables rather
than datoms columns.
Part 4: produce SQL for order clauses.
2017-04-14 23:10:56 +00:00
use mentat_query ::{
Direction ,
2017-04-19 23:16:19 +00:00
Limit ,
Variable ,
Implement :order. (#415) (#416) r=nalexander
This adds an `:order` keyword to `:find`.
If present, the results of the query will be an ordered set, rather than
an unordered set; rows will appear in an ordered defined by each
`:order` entry.
Each can be one of three things:
- A var, `?x`, meaning "order by ?x ascending".
- A pair, `(asc ?x)`, meaning "order by ?x ascending".
- A pair, `(desc ?x)`, meaning "order by ?x descending".
Values will be ordered in this sequence for asc, and in reverse for desc:
1. Entity IDs, in ascending numerical order.
2. Booleans, false then true.
3. Timestamps, in ascending numerical order.
4. Longs and doubles, intermixed, in ascending numerical order.
5. Strings, in ascending lexicographic order.
6. Keywords, in ascending lexicographic order, considering the entire
ns/name pair as a single string separated by '/'.
Subcommits:
Pre: make bound_value public.
Pre: generalize ErrorKind::UnboundVariable for use in order.
Part 1: parse (direction, var) pairs.
Part 2: parse :order clause into FindQuery.
Part 3: include order variables in algebrized query.
We add order variables to :with, so we can reuse its type tag projection
logic, and so that we can phrase ordering in terms of variables rather
than datoms columns.
Part 4: produce SQL for order clauses.
2017-04-14 23:10:56 +00:00
} ;
2017-02-16 23:39:19 +00:00
use mentat_query_algebrizer ::{
2017-04-08 00:23:41 +00:00
Column ,
Implement :order. (#415) (#416) r=nalexander
This adds an `:order` keyword to `:find`.
If present, the results of the query will be an ordered set, rather than
an unordered set; rows will appear in an ordered defined by each
`:order` entry.
Each can be one of three things:
- A var, `?x`, meaning "order by ?x ascending".
- A pair, `(asc ?x)`, meaning "order by ?x ascending".
- A pair, `(desc ?x)`, meaning "order by ?x descending".
Values will be ordered in this sequence for asc, and in reverse for desc:
1. Entity IDs, in ascending numerical order.
2. Booleans, false then true.
3. Timestamps, in ascending numerical order.
4. Longs and doubles, intermixed, in ascending numerical order.
5. Strings, in ascending lexicographic order.
6. Keywords, in ascending lexicographic order, considering the entire
ns/name pair as a single string separated by '/'.
Subcommits:
Pre: make bound_value public.
Pre: generalize ErrorKind::UnboundVariable for use in order.
Part 1: parse (direction, var) pairs.
Part 2: parse :order clause into FindQuery.
Part 3: include order variables in algebrized query.
We add order variables to :with, so we can reuse its type tag projection
logic, and so that we can phrase ordering in terms of variables rather
than datoms columns.
Part 4: produce SQL for order clauses.
2017-04-14 23:10:56 +00:00
OrderBy ,
2017-02-16 23:39:19 +00:00
QualifiedAlias ,
2017-03-16 19:23:48 +00:00
QueryValue ,
2017-02-16 23:39:19 +00:00
SourceAlias ,
2017-04-08 00:23:41 +00:00
TableAlias ,
VariableColumn ,
2017-02-16 23:39:19 +00:00
} ;
use mentat_sql ::{
BuildQueryResult ,
QueryBuilder ,
QueryFragment ,
SQLiteQueryBuilder ,
2017-02-22 03:57:00 +00:00
SQLQuery ,
2017-02-16 23:39:19 +00:00
} ;
//---------------------------------------------------------
// A Mentat-focused representation of a SQL query.
2017-02-22 03:57:00 +00:00
/// One of the things that can appear in a projection or a constraint. Note that we use
/// `TypedValue` here; it's not pure SQL, but it avoids us having to concern ourselves at this
/// point with the translation between a `TypedValue` and the storage-layer representation.
///
/// Eventually we might allow different translations by providing a different `QueryBuilder`
/// implementation for each storage backend. Passing `TypedValue`s here allows for that.
pub enum ColumnOrExpression {
2017-02-16 23:39:19 +00:00
Column ( QualifiedAlias ) ,
2018-03-12 22:18:50 +00:00
ExistingColumn ( Name ) ,
2017-02-22 03:57:00 +00:00
Entid ( Entid ) , // Because it's so common.
2017-03-07 04:18:38 +00:00
Integer ( i32 ) , // We use these for type codes etc.
2017-03-16 19:23:48 +00:00
Long ( i64 ) ,
2017-02-22 03:57:00 +00:00
Value ( TypedValue ) ,
2018-03-12 22:18:50 +00:00
Expression ( Box < Expression > , ValueType ) , // Track the return type.
}
pub enum Expression {
Unary { sql_op : & 'static str , arg : ColumnOrExpression } ,
2017-02-22 03:57:00 +00:00
}
2017-03-16 19:23:48 +00:00
/// `QueryValue` and `ColumnOrExpression` are almost identical… merge somehow?
impl From < QueryValue > for ColumnOrExpression {
fn from ( v : QueryValue ) -> Self {
match v {
QueryValue ::Column ( c ) = > ColumnOrExpression ::Column ( c ) ,
QueryValue ::Entid ( e ) = > ColumnOrExpression ::Entid ( e ) ,
QueryValue ::PrimitiveLong ( v ) = > ColumnOrExpression ::Long ( v ) ,
QueryValue ::TypedValue ( v ) = > ColumnOrExpression ::Value ( v ) ,
}
}
}
2017-02-22 03:57:00 +00:00
pub type Name = String ;
pub struct ProjectedColumn ( pub ColumnOrExpression , pub Name ) ;
pub enum Projection {
Columns ( Vec < ProjectedColumn > ) ,
Star ,
One ,
2017-02-16 23:39:19 +00:00
}
2018-03-12 22:18:50 +00:00
#[ derive(Debug, PartialEq, Eq) ]
pub enum GroupBy {
ProjectedColumn ( Name ) ,
QueryColumn ( QualifiedAlias ) ,
// TODO: non-projected expressions, etc.
}
impl QueryFragment for GroupBy {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
match self {
& GroupBy ::ProjectedColumn ( ref name ) = > {
out . push_identifier ( name . as_str ( ) )
} ,
& GroupBy ::QueryColumn ( ref qa ) = > {
qualified_alias_push_sql ( out , qa )
} ,
}
}
}
2017-02-22 03:57:00 +00:00
#[ derive(Copy, Clone) ]
2017-03-16 19:23:48 +00:00
pub struct Op ( pub & 'static str ) ; // TODO: we can do better than this!
2017-02-16 23:39:19 +00:00
2017-02-22 03:57:00 +00:00
pub enum Constraint {
2017-02-16 23:39:19 +00:00
Infix {
op : Op ,
left : ColumnOrExpression ,
2017-03-07 04:18:38 +00:00
right : ColumnOrExpression ,
} ,
2017-03-28 02:35:39 +00:00
Or {
constraints : Vec < Constraint > ,
} ,
2017-03-07 04:18:38 +00:00
And {
constraints : Vec < Constraint > ,
} ,
In {
left : ColumnOrExpression ,
list : Vec < ColumnOrExpression > ,
2017-04-28 09:44:11 +00:00
} ,
NotExists {
subquery : TableOrSubquery ,
2018-01-29 22:29:16 +00:00
} ,
TypeCheck {
value : ColumnOrExpression ,
affinity : SQLTypeAffinity
2017-02-16 23:39:19 +00:00
}
}
2017-02-22 03:57:00 +00:00
impl Constraint {
2017-03-07 04:18:38 +00:00
pub fn not_equal ( left : ColumnOrExpression , right : ColumnOrExpression ) -> Constraint {
Constraint ::Infix {
op : Op ( " <> " ) , // ANSI SQL for future-proofing!
left : left ,
right : right ,
}
}
2017-02-22 03:57:00 +00:00
pub fn equal ( left : ColumnOrExpression , right : ColumnOrExpression ) -> Constraint {
Constraint ::Infix {
op : Op ( " = " ) ,
left : left ,
right : right ,
}
}
2017-06-12 21:24:56 +00:00
pub fn fulltext_match ( left : ColumnOrExpression , right : ColumnOrExpression ) -> Constraint {
Constraint ::Infix {
op : Op ( " MATCH " ) , // SQLite specific!
left : left ,
right : right ,
}
}
2017-02-22 03:57:00 +00:00
}
2017-03-06 22:40:10 +00:00
#[ allow(dead_code) ]
2017-02-16 23:39:19 +00:00
enum JoinOp {
Inner ,
}
// Short-hand for a list of tables all inner-joined.
2017-04-08 00:23:41 +00:00
pub struct TableList ( pub Vec < TableOrSubquery > ) ;
2017-02-16 23:39:19 +00:00
2017-03-07 04:18:38 +00:00
impl TableList {
fn is_empty ( & self ) -> bool {
self . 0. is_empty ( )
}
}
2017-02-22 03:57:00 +00:00
pub struct Join {
2017-02-16 23:39:19 +00:00
left : TableOrSubquery ,
op : JoinOp ,
right : TableOrSubquery ,
// TODO: constraints (ON, USING).
}
2017-03-06 22:40:10 +00:00
#[ allow(dead_code) ]
2017-04-08 00:23:41 +00:00
pub enum TableOrSubquery {
2017-02-16 23:39:19 +00:00
Table ( SourceAlias ) ,
2017-04-08 00:23:41 +00:00
Union ( Vec < SelectQuery > , TableAlias ) ,
2017-04-28 09:44:11 +00:00
Subquery ( Box < SelectQuery > ) ,
2017-04-19 20:00:14 +00:00
Values ( Values , TableAlias ) ,
}
pub enum Values {
/// Like "VALUES (0, 1), (2, 3), ...".
2017-06-02 20:03:16 +00:00
/// The vector must be of a length that is a multiple of the given size.
Unnamed ( usize , Vec < TypedValue > ) ,
2017-04-19 20:00:14 +00:00
/// Like "SELECT 0 AS x, SELECT 0 AS y WHERE 0 UNION ALL VALUES (0, 1), (2, 3), ...".
2017-06-02 20:03:16 +00:00
/// The vector of values must be of a length that is a multiple of the length
/// of the vector of names.
Named ( Vec < Variable > , Vec < TypedValue > ) ,
2017-02-16 23:39:19 +00:00
}
2017-02-22 03:57:00 +00:00
pub enum FromClause {
2017-02-16 23:39:19 +00:00
TableList ( TableList ) , // Short-hand for a pile of inner joins.
Join ( Join ) ,
2017-03-07 04:18:38 +00:00
Nothing ,
2017-02-16 23:39:19 +00:00
}
2017-02-22 03:57:00 +00:00
pub struct SelectQuery {
2017-03-22 21:02:00 +00:00
pub distinct : bool ,
2017-02-22 03:57:00 +00:00
pub projection : Projection ,
pub from : FromClause ,
pub constraints : Vec < Constraint > ,
2018-03-12 22:18:50 +00:00
pub group_by : Vec < GroupBy > ,
Implement :order. (#415) (#416) r=nalexander
This adds an `:order` keyword to `:find`.
If present, the results of the query will be an ordered set, rather than
an unordered set; rows will appear in an ordered defined by each
`:order` entry.
Each can be one of three things:
- A var, `?x`, meaning "order by ?x ascending".
- A pair, `(asc ?x)`, meaning "order by ?x ascending".
- A pair, `(desc ?x)`, meaning "order by ?x descending".
Values will be ordered in this sequence for asc, and in reverse for desc:
1. Entity IDs, in ascending numerical order.
2. Booleans, false then true.
3. Timestamps, in ascending numerical order.
4. Longs and doubles, intermixed, in ascending numerical order.
5. Strings, in ascending lexicographic order.
6. Keywords, in ascending lexicographic order, considering the entire
ns/name pair as a single string separated by '/'.
Subcommits:
Pre: make bound_value public.
Pre: generalize ErrorKind::UnboundVariable for use in order.
Part 1: parse (direction, var) pairs.
Part 2: parse :order clause into FindQuery.
Part 3: include order variables in algebrized query.
We add order variables to :with, so we can reuse its type tag projection
logic, and so that we can phrase ordering in terms of variables rather
than datoms columns.
Part 4: produce SQL for order clauses.
2017-04-14 23:10:56 +00:00
pub order : Vec < OrderBy > ,
2017-04-19 23:16:19 +00:00
pub limit : Limit ,
2017-02-22 03:57:00 +00:00
}
Implement :order. (#415) (#416) r=nalexander
This adds an `:order` keyword to `:find`.
If present, the results of the query will be an ordered set, rather than
an unordered set; rows will appear in an ordered defined by each
`:order` entry.
Each can be one of three things:
- A var, `?x`, meaning "order by ?x ascending".
- A pair, `(asc ?x)`, meaning "order by ?x ascending".
- A pair, `(desc ?x)`, meaning "order by ?x descending".
Values will be ordered in this sequence for asc, and in reverse for desc:
1. Entity IDs, in ascending numerical order.
2. Booleans, false then true.
3. Timestamps, in ascending numerical order.
4. Longs and doubles, intermixed, in ascending numerical order.
5. Strings, in ascending lexicographic order.
6. Keywords, in ascending lexicographic order, considering the entire
ns/name pair as a single string separated by '/'.
Subcommits:
Pre: make bound_value public.
Pre: generalize ErrorKind::UnboundVariable for use in order.
Part 1: parse (direction, var) pairs.
Part 2: parse :order clause into FindQuery.
Part 3: include order variables in algebrized query.
We add order variables to :with, so we can reuse its type tag projection
logic, and so that we can phrase ordering in terms of variables rather
than datoms columns.
Part 4: produce SQL for order clauses.
2017-04-14 23:10:56 +00:00
fn push_variable_column ( qb : & mut QueryBuilder , vc : & VariableColumn ) -> BuildQueryResult {
match vc {
& VariableColumn ::Variable ( ref v ) = > {
qb . push_identifier ( v . as_str ( ) )
} ,
& VariableColumn ::VariableTypeTag ( ref v ) = > {
qb . push_identifier ( format! ( " {} _value_type_tag " , v . name ( ) ) . as_str ( ) )
} ,
}
}
2017-04-08 00:23:41 +00:00
fn push_column ( qb : & mut QueryBuilder , col : & Column ) -> BuildQueryResult {
match col {
& Column ::Fixed ( ref d ) = > {
qb . push_sql ( d . as_str ( ) ) ;
Ok ( ( ) )
} ,
2017-06-12 21:24:56 +00:00
& Column ::Fulltext ( ref d ) = > {
qb . push_sql ( d . as_str ( ) ) ;
Ok ( ( ) )
} ,
Implement :order. (#415) (#416) r=nalexander
This adds an `:order` keyword to `:find`.
If present, the results of the query will be an ordered set, rather than
an unordered set; rows will appear in an ordered defined by each
`:order` entry.
Each can be one of three things:
- A var, `?x`, meaning "order by ?x ascending".
- A pair, `(asc ?x)`, meaning "order by ?x ascending".
- A pair, `(desc ?x)`, meaning "order by ?x descending".
Values will be ordered in this sequence for asc, and in reverse for desc:
1. Entity IDs, in ascending numerical order.
2. Booleans, false then true.
3. Timestamps, in ascending numerical order.
4. Longs and doubles, intermixed, in ascending numerical order.
5. Strings, in ascending lexicographic order.
6. Keywords, in ascending lexicographic order, considering the entire
ns/name pair as a single string separated by '/'.
Subcommits:
Pre: make bound_value public.
Pre: generalize ErrorKind::UnboundVariable for use in order.
Part 1: parse (direction, var) pairs.
Part 2: parse :order clause into FindQuery.
Part 3: include order variables in algebrized query.
We add order variables to :with, so we can reuse its type tag projection
logic, and so that we can phrase ordering in terms of variables rather
than datoms columns.
Part 4: produce SQL for order clauses.
2017-04-14 23:10:56 +00:00
& Column ::Variable ( ref vc ) = > push_variable_column ( qb , vc ) ,
2018-04-16 21:08:00 +00:00
& Column ::Transactions ( ref d ) = > {
qb . push_sql ( d . as_str ( ) ) ;
Ok ( ( ) )
} ,
2017-04-08 00:23:41 +00:00
}
2017-02-16 23:39:19 +00:00
}
//---------------------------------------------------------
// Turn that representation into SQL.
2017-03-07 04:18:38 +00:00
/// 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 {
Implement :order. (#415) (#416) r=nalexander
This adds an `:order` keyword to `:find`.
If present, the results of the query will be an ordered set, rather than
an unordered set; rows will appear in an ordered defined by each
`:order` entry.
Each can be one of three things:
- A var, `?x`, meaning "order by ?x ascending".
- A pair, `(asc ?x)`, meaning "order by ?x ascending".
- A pair, `(desc ?x)`, meaning "order by ?x descending".
Values will be ordered in this sequence for asc, and in reverse for desc:
1. Entity IDs, in ascending numerical order.
2. Booleans, false then true.
3. Timestamps, in ascending numerical order.
4. Longs and doubles, intermixed, in ascending numerical order.
5. Strings, in ascending lexicographic order.
6. Keywords, in ascending lexicographic order, considering the entire
ns/name pair as a single string separated by '/'.
Subcommits:
Pre: make bound_value public.
Pre: generalize ErrorKind::UnboundVariable for use in order.
Part 1: parse (direction, var) pairs.
Part 2: parse :order clause into FindQuery.
Part 3: include order variables in algebrized query.
We add order variables to :with, so we can reuse its type tag projection
logic, and so that we can phrase ordering in terms of variables rather
than datoms columns.
Part 4: produce SQL for order clauses.
2017-04-14 23:10:56 +00:00
( $name : pat , $across : expr , $body : block , $inter : block ) = > {
2017-06-02 20:03:16 +00:00
interpose_iter! ( $name , $across . iter ( ) , $body , $inter )
}
}
macro_rules ! interpose_iter {
( $name : pat , $across : expr , $body : block , $inter : block ) = > {
let mut seq = $across ;
2017-03-07 04:18:38 +00:00
if let Some ( $name ) = seq . next ( ) {
$body ;
for $name in seq {
$inter ;
$body ;
}
}
}
}
2017-04-19 20:00:14 +00:00
2017-02-16 23:39:19 +00:00
impl QueryFragment for ColumnOrExpression {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
use self ::ColumnOrExpression ::* ;
match self {
2018-03-12 22:18:50 +00:00
& Column ( ref qa ) = > {
qualified_alias_push_sql ( out , qa )
} ,
& ExistingColumn ( ref alias ) = > {
out . push_identifier ( alias . as_str ( ) )
2017-02-16 23:39:19 +00:00
} ,
2017-02-22 03:57:00 +00:00
& Entid ( entid ) = > {
out . push_sql ( entid . to_string ( ) . as_str ( ) ) ;
2017-02-16 23:39:19 +00:00
Ok ( ( ) )
2017-02-22 03:57:00 +00:00
} ,
2017-03-07 04:18:38 +00:00
& Integer ( integer ) = > {
out . push_sql ( integer . to_string ( ) . as_str ( ) ) ;
Ok ( ( ) )
} ,
2017-03-16 19:23:48 +00:00
& Long ( long ) = > {
out . push_sql ( long . to_string ( ) . as_str ( ) ) ;
Ok ( ( ) )
} ,
2017-02-22 03:57:00 +00:00
& Value ( ref v ) = > {
out . push_typed_value ( v )
} ,
2018-03-12 22:18:50 +00:00
& Expression ( ref e , _ ) = > {
e . push_sql ( out )
} ,
}
}
}
impl QueryFragment for Expression {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
match self {
& Expression ::Unary { ref sql_op , ref arg } = > {
out . push_sql ( sql_op ) ; // No need to escape built-ins.
out . push_sql ( " ( " ) ;
arg . push_sql ( out ) ? ;
out . push_sql ( " ) " ) ;
Ok ( ( ) )
} ,
2017-02-16 23:39:19 +00:00
}
}
}
impl QueryFragment for Projection {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
2017-02-22 03:57:00 +00:00
use self ::Projection ::* ;
match self {
& One = > out . push_sql ( " 1 " ) ,
& Star = > out . push_sql ( " * " ) ,
& Columns ( ref cols ) = > {
let & ProjectedColumn ( ref col , ref alias ) = & cols [ 0 ] ;
col . push_sql ( out ) ? ;
out . push_sql ( " AS " ) ;
out . push_identifier ( alias . as_str ( ) ) ? ;
for & ProjectedColumn ( ref col , ref alias ) in & cols [ 1 .. ] {
out . push_sql ( " , " ) ;
col . push_sql ( out ) ? ;
out . push_sql ( " AS " ) ;
out . push_identifier ( alias . as_str ( ) ) ? ;
}
} ,
} ;
Ok ( ( ) )
2017-02-16 23:39:19 +00:00
}
}
impl QueryFragment for Op {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
// No escaping needed.
2017-02-22 03:57:00 +00:00
out . push_sql ( self . 0 ) ;
2017-02-16 23:39:19 +00:00
Ok ( ( ) )
}
}
impl QueryFragment for Constraint {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
use self ::Constraint ::* ;
match self {
& Infix { ref op , ref left , ref right } = > {
left . push_sql ( out ) ? ;
out . push_sql ( " " ) ;
op . push_sql ( out ) ? ;
out . push_sql ( " " ) ;
right . push_sql ( out )
2017-03-07 04:18:38 +00:00
} ,
& And { ref constraints } = > {
2017-03-28 02:35:39 +00:00
// An empty intersection is true.
if constraints . is_empty ( ) {
out . push_sql ( " 1 " ) ;
return Ok ( ( ) )
}
2017-03-07 04:18:38 +00:00
out . push_sql ( " ( " ) ;
interpose! ( constraint , constraints ,
{ constraint . push_sql ( out ) ? } ,
{ out . push_sql ( " AND " ) } ) ;
out . push_sql ( " ) " ) ;
Ok ( ( ) )
} ,
2017-03-28 02:35:39 +00:00
& Or { ref constraints } = > {
// An empty alternation is false.
if constraints . is_empty ( ) {
out . push_sql ( " 0 " ) ;
return Ok ( ( ) )
}
out . push_sql ( " ( " ) ;
interpose! ( constraint , constraints ,
{ constraint . push_sql ( out ) ? } ,
{ out . push_sql ( " OR " ) } ) ;
out . push_sql ( " ) " ) ;
Ok ( ( ) )
}
2017-03-07 04:18:38 +00:00
& 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 ( ( ) )
} ,
2017-04-28 09:44:11 +00:00
& NotExists { ref subquery } = > {
2018-03-12 22:18:50 +00:00
out . push_sql ( " NOT EXISTS " ) ;
subquery . push_sql ( out )
2018-01-29 22:29:16 +00:00
} ,
& TypeCheck { ref value , ref affinity } = > {
out . push_sql ( " typeof( " ) ;
value . push_sql ( out ) ? ;
out . push_sql ( " ) = " ) ;
out . push_sql ( match * affinity {
SQLTypeAffinity ::Null = > " 'null' " ,
SQLTypeAffinity ::Integer = > " 'integer' " ,
SQLTypeAffinity ::Real = > " 'real' " ,
SQLTypeAffinity ::Text = > " 'text' " ,
SQLTypeAffinity ::Blob = > " 'blob' " ,
} ) ;
Ok ( ( ) )
} ,
2017-02-16 23:39:19 +00:00
}
}
}
impl QueryFragment for JoinOp {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
out . push_sql ( " JOIN " ) ;
Ok ( ( ) )
}
}
2018-03-12 22:18:50 +00:00
// We don't own QualifiedAlias or QueryFragment, so we can't implement the trait.
fn qualified_alias_push_sql ( out : & mut QueryBuilder , qa : & QualifiedAlias ) -> BuildQueryResult {
out . push_identifier ( qa . 0. as_str ( ) ) ? ;
out . push_sql ( " . " ) ;
push_column ( out , & qa . 1 )
}
2017-02-16 23:39:19 +00:00
// We don't own SourceAlias or QueryFragment, so we can't implement the trait.
fn source_alias_push_sql ( out : & mut QueryBuilder , sa : & SourceAlias ) -> BuildQueryResult {
let & SourceAlias ( ref table , ref alias ) = sa ;
out . push_identifier ( table . name ( ) ) ? ;
out . push_sql ( " AS " ) ;
out . push_identifier ( alias . as_str ( ) )
}
impl QueryFragment for TableList {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
if self . 0. is_empty ( ) {
return Ok ( ( ) ) ;
}
2017-04-08 00:23:41 +00:00
interpose! ( t , self . 0 ,
{ t . push_sql ( out ) ? } ,
2017-03-07 18:31:28 +00:00
{ out . push_sql ( " , " ) } ) ;
2017-02-16 23:39:19 +00:00
Ok ( ( ) )
}
}
impl QueryFragment for Join {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
self . left . push_sql ( out ) ? ;
self . op . push_sql ( out ) ? ;
self . right . push_sql ( out )
}
}
impl QueryFragment for TableOrSubquery {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
use self ::TableOrSubquery ::* ;
match self {
2017-04-08 00:23:41 +00:00
& Table ( ref sa ) = > source_alias_push_sql ( out , sa ) ,
& Union ( ref subqueries , ref table_alias ) = > {
out . push_sql ( " ( " ) ;
interpose! ( subquery , subqueries ,
{ subquery . push_sql ( out ) ? } ,
{ out . push_sql ( " UNION " ) } ) ;
out . push_sql ( " ) AS " ) ;
out . push_identifier ( table_alias . as_str ( ) )
} ,
2017-04-28 09:44:11 +00:00
& Subquery ( ref subquery ) = > {
2018-03-12 22:18:50 +00:00
out . push_sql ( " ( " ) ;
subquery . push_sql ( out ) ? ;
out . push_sql ( " ) " ) ;
Ok ( ( ) )
2017-04-19 20:00:14 +00:00
} ,
& Values ( ref values , ref table_alias ) = > {
// XXX: does this work for Values::Unnamed?
out . push_sql ( " ( " ) ;
values . push_sql ( out ) ? ;
out . push_sql ( " ) AS " ) ;
out . push_identifier ( table_alias . as_str ( ) )
2017-04-28 09:44:11 +00:00
} ,
2017-02-16 23:39:19 +00:00
}
}
}
2017-04-19 20:00:14 +00:00
impl QueryFragment for Values {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
// There are at least 3 ways to name the columns of a VALUES table:
// 1) the columns are named "", ":1", ":2", ... -- but this is undocumented. See
// http://stackoverflow.com/a/40921724.
// 2) A CTE ("WITH" statement) can declare the shape of the table, like "WITH
// table_name(column_name, ...) AS (VALUES ...)".
// 3) We can "UNION ALL" a dummy "SELECT" statement in place.
//
// We don't want to use an undocumented SQLite quirk, and we're a little concerned that some
// SQL systems will not optimize WITH statements well. It's also convenient to have an in
2017-06-02 20:34:02 +00:00
// place table to query, so for now we implement option 3.
2017-04-19 20:00:14 +00:00
if let & Values ::Named ( ref names , _ ) = self {
2017-06-02 20:34:02 +00:00
out . push_sql ( " SELECT " ) ;
interpose! ( alias , names ,
{ out . push_sql ( " 0 AS " ) ;
out . push_identifier ( alias . as_str ( ) ) ? } ,
{ out . push_sql ( " , " ) } ) ;
2017-04-19 20:00:14 +00:00
out . push_sql ( " WHERE 0 UNION ALL " ) ;
}
let values = match self {
2017-06-02 20:03:16 +00:00
& Values ::Named ( ref names , ref values ) = > values . chunks ( names . len ( ) ) ,
& Values ::Unnamed ( ref size , ref values ) = > values . chunks ( * size ) ,
2017-04-19 20:00:14 +00:00
} ;
out . push_sql ( " VALUES " ) ;
2017-06-02 20:03:16 +00:00
interpose_iter! ( outer , values ,
{ out . push_sql ( " ( " ) ;
interpose! ( inner , outer ,
{ out . push_typed_value ( inner ) ? } ,
{ out . push_sql ( " , " ) } ) ;
out . push_sql ( " ) " ) ;
} ,
{ out . push_sql ( " , " ) } ) ;
2017-04-19 20:00:14 +00:00
Ok ( ( ) )
}
}
2017-02-16 23:39:19 +00:00
impl QueryFragment for FromClause {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
use self ::FromClause ::* ;
match self {
2017-03-07 04:18:38 +00:00
& 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 ( ( ) ) ,
2017-02-16 23:39:19 +00:00
}
}
}
2017-04-19 23:16:19 +00:00
impl SelectQuery {
fn push_variable_param ( & self , var : & Variable , out : & mut QueryBuilder ) -> BuildQueryResult {
// `var` is something like `?foo99-people`.
// Trim the `?` and escape the rest. Prepend `i` to distinguish from
// the inline value space `v`.
let re = regex ::Regex ::new ( " [^a-zA-Z_0-9] " ) . unwrap ( ) ;
let without_question = var . as_str ( ) . split_at ( 1 ) . 1 ;
let replaced = re . replace_all ( without_question , " _ " ) ;
let bind_param = format! ( " i {} " , replaced ) ; // We _could_ avoid this copying.
out . push_bind_param ( bind_param . as_str ( ) )
}
}
2017-02-16 23:39:19 +00:00
impl QueryFragment for SelectQuery {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
2017-03-22 21:02:00 +00:00
if self . distinct {
out . push_sql ( " SELECT DISTINCT " ) ;
} else {
out . push_sql ( " SELECT " ) ;
}
2017-02-22 03:57:00 +00:00
self . projection . push_sql ( out ) ? ;
2017-02-16 23:39:19 +00:00
self . from . push_sql ( out ) ? ;
2017-03-07 04:18:38 +00:00
if ! self . constraints . is_empty ( ) {
out . push_sql ( " WHERE " ) ;
interpose! ( constraint , self . constraints ,
{ constraint . push_sql ( out ) ? } ,
{ out . push_sql ( " AND " ) } ) ;
2017-02-16 23:39:19 +00:00
}
2018-03-12 22:18:50 +00:00
match & self . group_by {
group_by if ! group_by . is_empty ( ) = > {
out . push_sql ( " GROUP BY " ) ;
interpose! ( group , group_by ,
{ group . push_sql ( out ) ? } ,
{ out . push_sql ( " , " ) } ) ;
} ,
_ = > { } ,
}
Implement :order. (#415) (#416) r=nalexander
This adds an `:order` keyword to `:find`.
If present, the results of the query will be an ordered set, rather than
an unordered set; rows will appear in an ordered defined by each
`:order` entry.
Each can be one of three things:
- A var, `?x`, meaning "order by ?x ascending".
- A pair, `(asc ?x)`, meaning "order by ?x ascending".
- A pair, `(desc ?x)`, meaning "order by ?x descending".
Values will be ordered in this sequence for asc, and in reverse for desc:
1. Entity IDs, in ascending numerical order.
2. Booleans, false then true.
3. Timestamps, in ascending numerical order.
4. Longs and doubles, intermixed, in ascending numerical order.
5. Strings, in ascending lexicographic order.
6. Keywords, in ascending lexicographic order, considering the entire
ns/name pair as a single string separated by '/'.
Subcommits:
Pre: make bound_value public.
Pre: generalize ErrorKind::UnboundVariable for use in order.
Part 1: parse (direction, var) pairs.
Part 2: parse :order clause into FindQuery.
Part 3: include order variables in algebrized query.
We add order variables to :with, so we can reuse its type tag projection
logic, and so that we can phrase ordering in terms of variables rather
than datoms columns.
Part 4: produce SQL for order clauses.
2017-04-14 23:10:56 +00:00
if ! self . order . is_empty ( ) {
out . push_sql ( " ORDER BY " ) ;
interpose! ( & OrderBy ( ref dir , ref var ) , self . order ,
{ push_variable_column ( out , var ) ? ;
match dir {
& Direction ::Ascending = > { out . push_sql ( " ASC " ) ; } ,
& Direction ::Descending = > { out . push_sql ( " DESC " ) ; } ,
} ;
} ,
{ out . push_sql ( " , " ) } ) ;
}
2017-04-19 23:16:19 +00:00
match & self . limit {
& Limit ::None = > ( ) ,
& Limit ::Fixed ( limit ) = > {
// Guaranteed to be non-negative: u64.
out . push_sql ( " LIMIT " ) ;
out . push_sql ( limit . to_string ( ) . as_str ( ) ) ;
} ,
& Limit ::Variable ( ref var ) = > {
// Guess this wasn't bound yet. Produce an argument.
out . push_sql ( " LIMIT " ) ;
self . push_variable_param ( var , out ) ? ;
} ,
2017-03-07 00:27:13 +00:00
}
2017-02-16 23:39:19 +00:00
Ok ( ( ) )
}
}
impl SelectQuery {
2017-02-24 03:52:17 +00:00
pub fn to_sql_query ( & self ) -> mentat_sql ::Result < SQLQuery > {
2017-02-16 23:39:19 +00:00
let mut builder = SQLiteQueryBuilder ::new ( ) ;
self . push_sql ( & mut builder ) . map ( | _ | builder . finish ( ) )
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
2017-06-12 21:24:56 +00:00
use std ::rc ::Rc ;
2017-04-08 00:23:41 +00:00
use mentat_query_algebrizer ::{
2017-06-12 21:24:56 +00:00
Column ,
2017-04-08 00:23:41 +00:00
DatomsColumn ,
DatomsTable ,
2017-06-12 21:24:56 +00:00
FulltextColumn ,
2017-04-08 00:23:41 +00:00
} ;
2017-02-16 23:39:19 +00:00
2017-06-12 21:24:56 +00:00
fn build_query ( c : & QueryFragment ) -> SQLQuery {
2017-03-07 04:18:38 +00:00
let mut builder = SQLiteQueryBuilder ::new ( ) ;
c . push_sql ( & mut builder )
. map ( | _ | builder . finish ( ) )
2017-06-12 21:24:56 +00:00
. expect ( " to produce a query for the given constraint " )
}
fn build ( c : & QueryFragment ) -> String {
build_query ( c ) . sql
2017-03-07 04:18:38 +00:00
}
#[ test ]
fn test_in_constraint ( ) {
let none = Constraint ::In {
2017-06-12 21:24:56 +00:00
left : ColumnOrExpression ::Column ( QualifiedAlias ::new ( " datoms01 " . to_string ( ) , Column ::Fixed ( DatomsColumn ::Value ) ) ) ,
2017-03-07 04:18:38 +00:00
list : vec ! [ ] ,
} ;
let one = Constraint ::In {
2017-04-08 00:23:41 +00:00
left : ColumnOrExpression ::Column ( QualifiedAlias ::new ( " datoms01 " . to_string ( ) , DatomsColumn ::Value ) ) ,
2017-03-07 04:18:38 +00:00
list : vec ! [
ColumnOrExpression ::Entid ( 123 ) ,
] ,
} ;
let three = Constraint ::In {
2017-04-08 00:23:41 +00:00
left : ColumnOrExpression ::Column ( QualifiedAlias ::new ( " datoms01 " . to_string ( ) , DatomsColumn ::Value ) ) ,
2017-03-07 04:18:38 +00:00
list : vec ! [
ColumnOrExpression ::Entid ( 123 ) ,
ColumnOrExpression ::Entid ( 456 ) ,
ColumnOrExpression ::Entid ( 789 ) ,
] ,
} ;
2017-04-19 20:00:14 +00:00
assert_eq! ( " `datoms01`.v IN () " , build ( & none ) ) ;
assert_eq! ( " `datoms01`.v IN (123) " , build ( & one ) ) ;
assert_eq! ( " `datoms01`.v IN (123, 456, 789) " , build ( & three ) ) ;
2017-03-07 04:18:38 +00:00
}
#[ 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.
2017-04-19 20:00:14 +00:00
assert_eq! ( " ((123 = 456 AND 789 = 246)) " , build ( & c ) ) ;
}
#[ test ]
fn test_unnamed_values ( ) {
2017-06-02 20:34:02 +00:00
let build = | len , values | build ( & Values ::Unnamed ( len , values ) ) ;
assert_eq! ( build ( 1 , vec! [ TypedValue ::Long ( 1 ) ] ) ,
2017-04-19 20:00:14 +00:00
" VALUES (1) " ) ;
2017-06-02 20:34:02 +00:00
assert_eq! ( build ( 2 , vec! [ TypedValue ::Boolean ( false ) , TypedValue ::Long ( 1 ) ] ) ,
2017-04-19 20:00:14 +00:00
" VALUES (0, 1) " ) ;
2017-06-02 20:34:02 +00:00
assert_eq! ( build ( 2 , vec! [ TypedValue ::Boolean ( false ) , TypedValue ::Long ( 1 ) ,
TypedValue ::Boolean ( true ) , TypedValue ::Long ( 2 ) ] ) ,
2017-04-19 20:00:14 +00:00
" VALUES (0, 1), (1, 2) " ) ;
}
#[ test ]
fn test_named_values ( ) {
let build = | names : Vec < _ > , values | build ( & Values ::Named ( names . into_iter ( ) . map ( Variable ::from_valid_name ) . collect ( ) , values ) ) ;
2017-06-02 20:34:02 +00:00
assert_eq! ( build ( vec! [ " ?a " ] , vec! [ TypedValue ::Long ( 1 ) ] ) ,
2017-04-19 20:00:14 +00:00
" SELECT 0 AS `?a` WHERE 0 UNION ALL VALUES (1) " ) ;
2017-06-02 20:34:02 +00:00
assert_eq! ( build ( vec! [ " ?a " , " ?b " ] , vec! [ TypedValue ::Boolean ( false ) , TypedValue ::Long ( 1 ) ] ) ,
2017-04-19 20:00:14 +00:00
" SELECT 0 AS `?a`, 0 AS `?b` WHERE 0 UNION ALL VALUES (0, 1) " ) ;
assert_eq! ( build ( vec! [ " ?a " , " ?b " ] ,
2017-06-02 20:34:02 +00:00
vec! [ TypedValue ::Boolean ( false ) , TypedValue ::Long ( 1 ) ,
TypedValue ::Boolean ( true ) , TypedValue ::Long ( 2 ) ] ) ,
2017-04-19 20:00:14 +00:00
" SELECT 0 AS `?a`, 0 AS `?b` WHERE 0 UNION ALL VALUES (0, 1), (1, 2) " ) ;
2017-03-07 04:18:38 +00:00
}
2017-06-12 21:24:56 +00:00
#[ test ]
fn test_matches_constraint ( ) {
let c = Constraint ::Infix {
op : Op ( " MATCHES " ) ,
left : ColumnOrExpression ::Column ( QualifiedAlias ( " fulltext01 " . to_string ( ) , Column ::Fulltext ( FulltextColumn ::Text ) ) ) ,
right : ColumnOrExpression ::Value ( TypedValue ::String ( Rc ::new ( " needle " . to_string ( ) ) ) ) ,
} ;
let q = build_query ( & c ) ;
assert_eq! ( " `fulltext01`.text MATCHES $v0 " , q . sql ) ;
assert_eq! ( vec! [ ( " $v0 " . to_string ( ) , Rc ::new ( mentat_sql ::Value ::Text ( " needle " . to_string ( ) ) ) ) ] , q . args ) ;
let c = Constraint ::Infix {
op : Op ( " = " ) ,
left : ColumnOrExpression ::Column ( QualifiedAlias ( " fulltext01 " . to_string ( ) , Column ::Fulltext ( FulltextColumn ::Rowid ) ) ) ,
right : ColumnOrExpression ::Column ( QualifiedAlias ( " datoms02 " . to_string ( ) , Column ::Fixed ( DatomsColumn ::Value ) ) ) ,
} ;
assert_eq! ( " `fulltext01`.rowid = `datoms02`.v " , build ( & c ) ) ;
}
2017-02-16 23:39:19 +00:00
#[ test ]
fn test_end_to_end ( ) {
// [:find ?x :where [?x 65537 ?v] [?x 65536 ?v]]
let datoms00 = " datoms00 " . to_string ( ) ;
let datoms01 = " datoms01 " . to_string ( ) ;
2017-02-22 03:57:00 +00:00
let eq = Op ( " = " ) ;
2017-02-16 23:39:19 +00:00
let source_aliases = vec! [
2017-04-08 00:23:41 +00:00
TableOrSubquery ::Table ( SourceAlias ( DatomsTable ::Datoms , datoms00 . clone ( ) ) ) ,
TableOrSubquery ::Table ( SourceAlias ( DatomsTable ::Datoms , datoms01 . clone ( ) ) ) ,
2017-02-16 23:39:19 +00:00
] ;
2017-03-22 21:02:00 +00:00
let mut query = SelectQuery {
distinct : true ,
2017-02-22 03:57:00 +00:00
projection : Projection ::Columns (
vec! [
ProjectedColumn (
2017-04-08 00:23:41 +00:00
ColumnOrExpression ::Column ( QualifiedAlias ::new ( datoms00 . clone ( ) , DatomsColumn ::Entity ) ) ,
2017-02-22 03:57:00 +00:00
" x " . to_string ( ) ) ,
] ) ,
2017-02-16 23:39:19 +00:00
from : FromClause ::TableList ( TableList ( source_aliases ) ) ,
constraints : vec ! [
Constraint ::Infix {
op : eq . clone ( ) ,
2017-04-08 00:23:41 +00:00
left : ColumnOrExpression ::Column ( QualifiedAlias ::new ( datoms01 . clone ( ) , DatomsColumn ::Value ) ) ,
right : ColumnOrExpression ::Column ( QualifiedAlias ::new ( datoms00 . clone ( ) , DatomsColumn ::Value ) ) ,
2017-02-16 23:39:19 +00:00
} ,
Constraint ::Infix {
op : eq . clone ( ) ,
2017-04-08 00:23:41 +00:00
left : ColumnOrExpression ::Column ( QualifiedAlias ::new ( datoms00 . clone ( ) , DatomsColumn ::Attribute ) ) ,
2017-02-22 03:57:00 +00:00
right : ColumnOrExpression ::Entid ( 65537 ) ,
2017-02-16 23:39:19 +00:00
} ,
Constraint ::Infix {
op : eq . clone ( ) ,
2017-04-08 00:23:41 +00:00
left : ColumnOrExpression ::Column ( QualifiedAlias ::new ( datoms01 . clone ( ) , DatomsColumn ::Attribute ) ) ,
2017-02-22 03:57:00 +00:00
right : ColumnOrExpression ::Entid ( 65536 ) ,
2017-02-16 23:39:19 +00:00
} ,
] ,
2018-03-12 22:18:50 +00:00
group_by : vec ! [ ] ,
Implement :order. (#415) (#416) r=nalexander
This adds an `:order` keyword to `:find`.
If present, the results of the query will be an ordered set, rather than
an unordered set; rows will appear in an ordered defined by each
`:order` entry.
Each can be one of three things:
- A var, `?x`, meaning "order by ?x ascending".
- A pair, `(asc ?x)`, meaning "order by ?x ascending".
- A pair, `(desc ?x)`, meaning "order by ?x descending".
Values will be ordered in this sequence for asc, and in reverse for desc:
1. Entity IDs, in ascending numerical order.
2. Booleans, false then true.
3. Timestamps, in ascending numerical order.
4. Longs and doubles, intermixed, in ascending numerical order.
5. Strings, in ascending lexicographic order.
6. Keywords, in ascending lexicographic order, considering the entire
ns/name pair as a single string separated by '/'.
Subcommits:
Pre: make bound_value public.
Pre: generalize ErrorKind::UnboundVariable for use in order.
Part 1: parse (direction, var) pairs.
Part 2: parse :order clause into FindQuery.
Part 3: include order variables in algebrized query.
We add order variables to :with, so we can reuse its type tag projection
logic, and so that we can phrase ordering in terms of variables rather
than datoms columns.
Part 4: produce SQL for order clauses.
2017-04-14 23:10:56 +00:00
order : vec ! [ ] ,
2017-04-19 23:16:19 +00:00
limit : Limit ::None ,
2017-02-16 23:39:19 +00:00
} ;
2017-03-22 21:02:00 +00:00
let SQLQuery { sql , args } = query . to_sql_query ( ) . unwrap ( ) ;
println! ( " {} " , sql ) ;
assert_eq! ( " SELECT DISTINCT `datoms00`.e AS `x` FROM `datoms` AS `datoms00`, `datoms` AS `datoms01` WHERE `datoms01`.v = `datoms00`.v AND `datoms00`.a = 65537 AND `datoms01`.a = 65536 " , sql ) ;
assert! ( args . is_empty ( ) ) ;
// And without distinct…
query . distinct = false ;
2017-02-22 03:57:00 +00:00
let SQLQuery { sql , args } = query . to_sql_query ( ) . unwrap ( ) ;
2017-02-16 23:39:19 +00:00
println! ( " {} " , sql ) ;
2017-02-22 03:57:00 +00:00
assert_eq! ( " SELECT `datoms00`.e AS `x` FROM `datoms` AS `datoms00`, `datoms` AS `datoms01` WHERE `datoms01`.v = `datoms00`.v AND `datoms00`.a = 65537 AND `datoms01`.a = 65536 " , sql ) ;
assert! ( args . is_empty ( ) ) ;
2017-03-22 21:02:00 +00:00
2017-02-16 23:39:19 +00:00
}
}