From 5e3cdd1fc2a52396837a0018522c75a144f0776c Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Tue, 21 Feb 2017 19:57:00 -0800 Subject: [PATCH] Implement query-translator. (#301) r=nalexander --- query-translator/Cargo.toml | 7 ++ query-translator/src/lib.rs | 7 ++ query-translator/src/translate.rs | 76 ++++++++++++++++ query-translator/src/types.rs | 135 +++++++++++++++++++--------- query-translator/tests/translate.rs | 60 +++++++++++++ 5 files changed, 243 insertions(+), 42 deletions(-) create mode 100644 query-translator/src/translate.rs create mode 100644 query-translator/tests/translate.rs diff --git a/query-translator/Cargo.toml b/query-translator/Cargo.toml index e6ef4b16..51f1d2f2 100644 --- a/query-translator/Cargo.toml +++ b/query-translator/Cargo.toml @@ -4,6 +4,9 @@ version = "0.0.1" workspace = ".." [dependencies] +[dependencies.mentat_core] +path = "../core" + [dependencies.mentat_sql] path = "../sql" @@ -12,3 +15,7 @@ path = "../query" [dependencies.mentat_query_algebrizer] path = "../query-algebrizer" + +# Only for tests. +[dev-dependencies.mentat_query_parser] +path = "../query-parser" diff --git a/query-translator/src/lib.rs b/query-translator/src/lib.rs index 12331ad9..7c18b962 100644 --- a/query-translator/src/lib.rs +++ b/query-translator/src/lib.rs @@ -8,8 +8,15 @@ // 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; +mod translate; mod types; + +pub use translate::{ + cc_to_exists, + cc_to_select, +}; diff --git a/query-translator/src/translate.rs b/query-translator/src/translate.rs new file mode 100644 index 00000000..20c61048 --- /dev/null +++ b/query-translator/src/translate.rs @@ -0,0 +1,76 @@ +// 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. + +#![allow(dead_code, unused_imports)] + +use mentat_query_algebrizer::{ + AlgebraicQuery, + ColumnConstraint, + ConjoiningClauses, + DatomsColumn, + DatomsTable, + QualifiedAlias, + SourceAlias, +}; + +use types::{ + ColumnOrExpression, + Constraint, + FromClause, + Projection, + SelectQuery, + TableList, +}; + +trait ToConstraint { + fn to_constraint(self) -> Constraint; +} + +trait ToColumn { + fn to_column(self) -> ColumnOrExpression; +} + +impl ToColumn for QualifiedAlias { + fn to_column(self) -> ColumnOrExpression { + ColumnOrExpression::Column(self) + } +} + +impl ToConstraint for ColumnConstraint { + fn to_constraint(self) -> Constraint { + use self::ColumnConstraint::*; + match self { + EqualsEntity(qa, entid) => + Constraint::equal(qa.to_column(), ColumnOrExpression::Entid(entid)), + EqualsValue(qa, tv) => + Constraint::equal(qa.to_column(), ColumnOrExpression::Value(tv)), + EqualsColumn(left, right) => + Constraint::equal(left.to_column(), right.to_column()), + } + } +} + + +/// Consume a provided `ConjoiningClauses` to yield a new +/// `SelectQuery`. A projection list must also be provided. +pub fn cc_to_select(projection: Projection, cc: ConjoiningClauses) -> SelectQuery { + SelectQuery { + projection: projection, + from: FromClause::TableList(TableList(cc.from)), + constraints: cc.wheres + .into_iter() + .map(|c| c.to_constraint()) + .collect(), + } +} + +pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery { + cc_to_select(Projection::One, cc) +} diff --git a/query-translator/src/types.rs b/query-translator/src/types.rs index d0f63b20..d149455a 100644 --- a/query-translator/src/types.rs +++ b/query-translator/src/types.rs @@ -10,6 +10,11 @@ #![allow(dead_code, unused_imports)] +use mentat_core::{ + Entid, + TypedValue, +}; + use mentat_query_algebrizer::{ AlgebraicQuery, ConjoiningClauses, @@ -25,22 +30,38 @@ use mentat_sql::{ QueryBuilder, QueryFragment, SQLiteQueryBuilder, + SQLQuery, }; //--------------------------------------------------------- // A Mentat-focused representation of a SQL query. -enum ColumnOrExpression { +/// 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), - Integer(i64), // Because it's so common. + Entid(Entid), // Because it's so common. + Value(TypedValue), } -type Name = String; -struct Projection (ColumnOrExpression, Name); +pub type Name = String; -#[derive(Clone)] -struct Op(String); // TODO -enum Constraint { +pub struct ProjectedColumn(pub ColumnOrExpression, pub Name); + +pub enum Projection { + Columns(Vec), + Star, + One, +} + +#[derive(Copy, Clone)] +pub struct Op(&'static str); // TODO: we can do better than this! + +pub enum Constraint { Infix { op: Op, left: ColumnOrExpression, @@ -48,14 +69,24 @@ enum Constraint { } } +impl Constraint { + pub fn equal(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint { + Constraint::Infix { + op: Op("="), + left: left, + right: right, + } + } +} + enum JoinOp { Inner, } // Short-hand for a list of tables all inner-joined. -struct TableList(Vec); +pub struct TableList(pub Vec); -struct Join { +pub struct Join { left: TableOrSubquery, op: JoinOp, right: TableOrSubquery, @@ -67,15 +98,20 @@ enum TableOrSubquery { // TODO: Subquery. } -enum FromClause { +pub enum FromClause { TableList(TableList), // Short-hand for a pile of inner joins. Join(Join), } -struct SelectQuery { - projection: Vec, - from: FromClause, - constraints: Vec, +pub struct SelectQuery { + pub projection: Projection, + pub from: FromClause, + pub constraints: Vec, +} + +// We know that DatomsColumns are safe to serialize. +fn push_column(qb: &mut QueryBuilder, col: &DatomsColumn) { + qb.push_sql(col.as_str()); } //--------------------------------------------------------- @@ -88,28 +124,48 @@ impl QueryFragment for ColumnOrExpression { &Column(QualifiedAlias(ref table, ref column)) => { out.push_identifier(table.as_str())?; out.push_sql("."); - out.push_identifier(column.as_str()) - }, - &Integer(i) => { - out.push_sql(i.to_string().as_str()); + push_column(out, column); Ok(()) - } + }, + &Entid(entid) => { + out.push_sql(entid.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 { - self.0.push_sql(out)?; - out.push_sql(" AS "); - out.push_identifier(self.1.as_str()) + 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.as_str()); + out.push_sql(self.0); Ok(()) } } @@ -190,12 +246,7 @@ impl QueryFragment for FromClause { impl QueryFragment for SelectQuery { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { out.push_sql("SELECT "); - self.projection[0].push_sql(out)?; - - for projection in self.projection[1..].iter() { - out.push_sql(", "); - projection.push_sql(out)?; - } + self.projection.push_sql(out)?; out.push_sql(" FROM "); self.from.push_sql(out)?; @@ -217,7 +268,7 @@ impl QueryFragment for SelectQuery { } impl SelectQuery { - fn to_sql_string(&self) -> Result { + pub fn to_sql_query(&self) -> Result { let mut builder = SQLiteQueryBuilder::new(); self.push_sql(&mut builder).map(|_| builder.finish()) } @@ -233,21 +284,20 @@ mod tests { // [:find ?x :where [?x 65537 ?v] [?x 65536 ?v]] let datoms00 = "datoms00".to_string(); let datoms01 = "datoms01".to_string(); - let eq = Op("=".to_string()); + let eq = Op("="); let source_aliases = vec![ SourceAlias(DatomsTable::Datoms, datoms00.clone()), SourceAlias(DatomsTable::Datoms, datoms01.clone()), ]; let query = SelectQuery { - projection: vec![ - Projection( - ColumnOrExpression::Column(QualifiedAlias(datoms00.clone(), DatomsColumn::Entity)), - "x".to_string(), - ), - ], + projection: Projection::Columns( + vec![ + ProjectedColumn( + ColumnOrExpression::Column(QualifiedAlias(datoms00.clone(), DatomsColumn::Entity)), + "x".to_string()), + ]), from: FromClause::TableList(TableList(source_aliases)), constraints: vec![ - //ColumnOrExpression::Expression(TypedValue::Integer(15)), Constraint::Infix { op: eq.clone(), left: ColumnOrExpression::Column(QualifiedAlias(datoms01.clone(), DatomsColumn::Value)), @@ -256,18 +306,19 @@ mod tests { Constraint::Infix { op: eq.clone(), left: ColumnOrExpression::Column(QualifiedAlias(datoms00.clone(), DatomsColumn::Attribute)), - right: ColumnOrExpression::Integer(65537), + right: ColumnOrExpression::Entid(65537), }, Constraint::Infix { op: eq.clone(), left: ColumnOrExpression::Column(QualifiedAlias(datoms01.clone(), DatomsColumn::Attribute)), - right: ColumnOrExpression::Integer(65536), + right: ColumnOrExpression::Entid(65536), }, ], }; - let sql = query.to_sql_string().unwrap(); + 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_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()); } } diff --git a/query-translator/tests/translate.rs b/query-translator/tests/translate.rs new file mode 100644 index 00000000..08720c73 --- /dev/null +++ b/query-translator/tests/translate.rs @@ -0,0 +1,60 @@ +// 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_query_parser; +extern crate mentat_query_translator; +extern crate mentat_sql; + +use mentat_query::NamespacedKeyword; + +use mentat_core::{ + Attribute, + Entid, + Schema, + ValueType, +}; + +use mentat_query_parser::parse_find_string; +use mentat_query_algebrizer::algebrize; +use mentat_query_translator::{ + cc_to_exists, +}; + +use mentat_sql::SQLQuery; + +fn associate_ident(schema: &mut Schema, i: NamespacedKeyword, e: Entid) { + schema.entid_map.insert(e, i.clone()); + schema.ident_map.insert(i.clone(), e); +} + +fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) { + schema.schema_map.insert(e, a); +} + +#[test] +fn test_exists() { + let mut schema = Schema::default(); + associate_ident(&mut schema, NamespacedKeyword::new("foo", "bar"), 99); + add_attribute(&mut schema, 99, Attribute { + value_type: ValueType::String, + ..Default::default() + }); + + let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#; + let parsed = parse_find_string(input).unwrap(); + let algebrized = algebrize(&schema, parsed); + let select = cc_to_exists(algebrized.cc); + let SQLQuery { sql, args } = select.to_sql_query().unwrap(); + assert_eq!(sql, "SELECT 1 FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0"); + assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]); +}