// 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. extern crate mentat_core; extern crate mentat_query; extern crate mentat_query_algebrizer; extern crate mentat_sql; use mentat_core::{ Entid, TypedValue, }; use mentat_query_algebrizer::{ DatomsColumn, QualifiedAlias, QueryValue, SourceAlias, }; use mentat_sql::{ BuildQueryResult, QueryBuilder, QueryFragment, SQLiteQueryBuilder, SQLQuery, }; //--------------------------------------------------------- // A Mentat-focused representation of a SQL query. /// 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 { Column(QualifiedAlias), Entid(Entid), // Because it's so common. Integer(i32), // We use these for type codes etc. Long(i64), Value(TypedValue), } /// `QueryValue` and `ColumnOrExpression` are almost identical… merge somehow? impl From 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), } } } pub type Name = String; pub struct ProjectedColumn(pub ColumnOrExpression, pub Name); pub enum Projection { Columns(Vec), Star, One, } #[derive(Copy, Clone)] pub struct Op(pub &'static str); // TODO: we can do better than this! pub enum Constraint { Infix { op: Op, left: ColumnOrExpression, right: ColumnOrExpression, }, And { constraints: Vec, }, In { left: ColumnOrExpression, list: Vec, } } impl Constraint { pub fn not_equal(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint { Constraint::Infix { op: Op("<>"), // ANSI SQL for future-proofing! left: left, right: right, } } pub fn equal(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint { Constraint::Infix { op: Op("="), left: left, right: right, } } } #[allow(dead_code)] enum JoinOp { Inner, } // Short-hand for a list of tables all inner-joined. pub struct TableList(pub Vec); impl TableList { fn is_empty(&self) -> bool { self.0.is_empty() } } pub struct Join { left: TableOrSubquery, op: JoinOp, right: TableOrSubquery, // TODO: constraints (ON, USING). } #[allow(dead_code)] enum TableOrSubquery { Table(SourceAlias), // TODO: Subquery. } pub enum FromClause { TableList(TableList), // Short-hand for a pile of inner joins. Join(Join), Nothing, } pub struct SelectQuery { pub distinct: bool, pub projection: Projection, pub from: FromClause, pub constraints: Vec, pub limit: Option, } // We know that DatomsColumns are safe to serialize. fn push_column(qb: &mut QueryBuilder, col: &DatomsColumn) { qb.push_sql(col.as_str()); } //--------------------------------------------------------- // Turn that representation into SQL. /// A helper macro to sequentially process an iterable sequence, /// evaluating a block between each pair of items. /// /// This is used to simply and efficiently produce output like /// /// ```sql /// 1, 2, 3 /// ``` /// /// or /// /// ```sql /// x = 1 AND y = 2 /// ``` /// /// without producing an intermediate string sequence. macro_rules! interpose { ( $name: ident, $across: expr, $body: block, $inter: block ) => { let mut seq = $across.iter(); if let Some($name) = seq.next() { $body; for $name in seq { $inter; $body; } } } } impl QueryFragment for ColumnOrExpression { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { use self::ColumnOrExpression::*; match self { &Column(QualifiedAlias(ref table, ref column)) => { out.push_identifier(table.as_str())?; out.push_sql("."); push_column(out, column); Ok(()) }, &Entid(entid) => { out.push_sql(entid.to_string().as_str()); Ok(()) }, &Integer(integer) => { out.push_sql(integer.to_string().as_str()); Ok(()) }, &Long(long) => { out.push_sql(long.to_string().as_str()); Ok(()) }, &Value(ref v) => { out.push_typed_value(v) }, } } } impl QueryFragment for Projection { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { 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(()) } } impl QueryFragment for Op { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { // No escaping needed. out.push_sql(self.0); 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) }, &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(()) }, } } } 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(()); } interpose!(sa, self.0, { source_alias_push_sql(out, sa)? }, { out.push_sql(", ") }); 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 { &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(()), } } } impl QueryFragment for SelectQuery { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { if self.distinct { out.push_sql("SELECT DISTINCT "); } else { out.push_sql("SELECT "); } self.projection.push_sql(out)?; self.from.push_sql(out)?; if !self.constraints.is_empty() { out.push_sql(" WHERE "); interpose!(constraint, self.constraints, { constraint.push_sql(out)? }, { out.push_sql(" AND ") }); } // Guaranteed to be positive: u64. if let Some(limit) = self.limit { out.push_sql(" LIMIT "); out.push_sql(limit.to_string().as_str()); } Ok(()) } } impl SelectQuery { pub fn to_sql_query(&self) -> mentat_sql::Result { let mut builder = SQLiteQueryBuilder::new(); self.push_sql(&mut builder).map(|_| builder.finish()) } } #[cfg(test)] mod tests { use super::*; use mentat_query_algebrizer::DatomsTable; fn build_constraint(c: Constraint) -> String { let mut builder = SQLiteQueryBuilder::new(); c.push_sql(&mut builder) .map(|_| builder.finish()) .unwrap().sql } #[test] fn test_in_constraint() { let none = Constraint::In { left: ColumnOrExpression::Column(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value)), list: vec![], }; let one = Constraint::In { left: ColumnOrExpression::Column(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value)), list: vec![ ColumnOrExpression::Entid(123), ], }; let three = Constraint::In { left: ColumnOrExpression::Column(QualifiedAlias("datoms01".to_string(), DatomsColumn::Value)), list: vec![ ColumnOrExpression::Entid(123), ColumnOrExpression::Entid(456), ColumnOrExpression::Entid(789), ], }; assert_eq!("`datoms01`.v IN ()", build_constraint(none)); assert_eq!("`datoms01`.v IN (123)", build_constraint(one)); assert_eq!("`datoms01`.v IN (123, 456, 789)", build_constraint(three)); } #[test] fn test_and_constraint() { let c = Constraint::And { constraints: vec![ Constraint::And { constraints: vec![ Constraint::Infix { op: Op("="), left: ColumnOrExpression::Entid(123), right: ColumnOrExpression::Entid(456), }, Constraint::Infix { op: Op("="), left: ColumnOrExpression::Entid(789), right: ColumnOrExpression::Entid(246), }, ], }, ], }; // Two sets of parens: the outermost AND only has one child, // but still contributes parens. assert_eq!("((123 = 456 AND 789 = 246))", build_constraint(c)); } #[test] fn test_end_to_end() { // [:find ?x :where [?x 65537 ?v] [?x 65536 ?v]] let datoms00 = "datoms00".to_string(); let datoms01 = "datoms01".to_string(); let eq = Op("="); let source_aliases = vec![ SourceAlias(DatomsTable::Datoms, datoms00.clone()), SourceAlias(DatomsTable::Datoms, datoms01.clone()), ]; let mut query = SelectQuery { distinct: true, projection: Projection::Columns( vec![ ProjectedColumn( ColumnOrExpression::Column(QualifiedAlias(datoms00.clone(), DatomsColumn::Entity)), "x".to_string()), ]), 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)), right: ColumnOrExpression::Entid(65537), }, Constraint::Infix { op: eq.clone(), left: ColumnOrExpression::Column(QualifiedAlias(datoms01.clone(), DatomsColumn::Attribute)), right: ColumnOrExpression::Entid(65536), }, ], limit: None, }; 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; let SQLQuery { sql, args } = query.to_sql_query().unwrap(); println!("{}", sql); 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()); } }