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-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-02-22 03:57:00 +00:00
use mentat_core ::{
Entid ,
TypedValue ,
} ;
2017-02-16 23:39:19 +00:00
use mentat_query_algebrizer ::{
DatomsColumn ,
QualifiedAlias ,
SourceAlias ,
} ;
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 ) ,
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-02-22 03:57:00 +00:00
Value ( TypedValue ) ,
}
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
}
2017-02-22 03:57:00 +00:00
#[ derive(Copy, Clone) ]
pub struct Op ( & '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 ,
} ,
And {
constraints : Vec < Constraint > ,
} ,
In {
left : ColumnOrExpression ,
list : Vec < ColumnOrExpression > ,
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-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-02-22 03:57:00 +00:00
pub struct TableList ( pub Vec < SourceAlias > ) ;
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-02-16 23:39:19 +00:00
enum TableOrSubquery {
Table ( SourceAlias ) ,
// TODO: Subquery.
}
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 {
pub projection : Projection ,
pub from : FromClause ,
pub constraints : Vec < Constraint > ,
2017-03-07 00:27:13 +00:00
pub limit : Option < u64 > ,
2017-02-22 03:57:00 +00:00
}
// We know that DatomsColumns are safe to serialize.
fn push_column ( qb : & mut QueryBuilder , col : & DatomsColumn ) {
qb . push_sql ( col . as_str ( ) ) ;
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 {
( $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 ;
}
}
}
}
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 {
& Column ( QualifiedAlias ( ref table , ref column ) ) = > {
out . push_identifier ( table . as_str ( ) ) ? ;
out . push_sql ( " . " ) ;
2017-02-22 03:57:00 +00:00
push_column ( out , column ) ;
Ok ( ( ) )
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-02-22 03:57:00 +00:00
& Value ( ref v ) = > {
out . push_typed_value ( v )
} ,
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 } = > {
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 ( ( ) )
} ,
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 ( ( ) )
}
}
// 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-03-07 18:31:28 +00:00
interpose! ( sa , self . 0 ,
{ source_alias_push_sql ( out , sa ) ? } ,
{ 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 {
& Table ( ref sa ) = > source_alias_push_sql ( out , sa )
}
}
}
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
}
}
}
impl QueryFragment for SelectQuery {
fn push_sql ( & self , out : & mut QueryBuilder ) -> BuildQueryResult {
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
}
2017-03-07 00:27:13 +00:00
// Guaranteed to be positive: u64.
if let Some ( limit ) = self . limit {
out . push_sql ( " LIMIT " ) ;
out . push_sql ( limit . to_string ( ) . as_str ( ) ) ;
}
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-03-06 22:40:10 +00:00
use mentat_query_algebrizer ::DatomsTable ;
2017-02-16 23:39:19 +00:00
2017-03-07 04:18:38 +00:00
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 ) ) ;
}
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! [
SourceAlias ( DatomsTable ::Datoms , datoms00 . clone ( ) ) ,
SourceAlias ( DatomsTable ::Datoms , datoms01 . clone ( ) ) ,
] ;
let query = SelectQuery {
2017-02-22 03:57:00 +00:00
projection : Projection ::Columns (
vec! [
ProjectedColumn (
ColumnOrExpression ::Column ( QualifiedAlias ( datoms00 . clone ( ) , DatomsColumn ::Entity ) ) ,
" x " . to_string ( ) ) ,
] ) ,
2017-02-16 23:39:19 +00:00
from : FromClause ::TableList ( TableList ( source_aliases ) ) ,
constraints : vec ! [
Constraint ::Infix {
op : eq . clone ( ) ,
left : ColumnOrExpression ::Column ( QualifiedAlias ( datoms01 . clone ( ) , DatomsColumn ::Value ) ) ,
right : ColumnOrExpression ::Column ( QualifiedAlias ( datoms00 . clone ( ) , DatomsColumn ::Value ) ) ,
} ,
Constraint ::Infix {
op : eq . clone ( ) ,
left : ColumnOrExpression ::Column ( QualifiedAlias ( 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 ( ) ,
left : ColumnOrExpression ::Column ( QualifiedAlias ( datoms01 . clone ( ) , DatomsColumn ::Attribute ) ) ,
2017-02-22 03:57:00 +00:00
right : ColumnOrExpression ::Entid ( 65536 ) ,
2017-02-16 23:39:19 +00:00
} ,
] ,
2017-03-07 00:27:13 +00:00
limit : None ,
2017-02-16 23:39:19 +00:00
} ;
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-02-16 23:39:19 +00:00
}
}