Implement query-translator. (#301) r=nalexander
This commit is contained in:
parent
91c75f26c8
commit
5e3cdd1fc2
5 changed files with 243 additions and 42 deletions
|
@ -4,6 +4,9 @@ version = "0.0.1"
|
||||||
workspace = ".."
|
workspace = ".."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
[dependencies.mentat_core]
|
||||||
|
path = "../core"
|
||||||
|
|
||||||
[dependencies.mentat_sql]
|
[dependencies.mentat_sql]
|
||||||
path = "../sql"
|
path = "../sql"
|
||||||
|
|
||||||
|
@ -12,3 +15,7 @@ path = "../query"
|
||||||
|
|
||||||
[dependencies.mentat_query_algebrizer]
|
[dependencies.mentat_query_algebrizer]
|
||||||
path = "../query-algebrizer"
|
path = "../query-algebrizer"
|
||||||
|
|
||||||
|
# Only for tests.
|
||||||
|
[dev-dependencies.mentat_query_parser]
|
||||||
|
path = "../query-parser"
|
||||||
|
|
|
@ -8,8 +8,15 @@
|
||||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||||
// specific language governing permissions and limitations under the License.
|
// specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
extern crate mentat_core;
|
||||||
extern crate mentat_query;
|
extern crate mentat_query;
|
||||||
extern crate mentat_query_algebrizer;
|
extern crate mentat_query_algebrizer;
|
||||||
extern crate mentat_sql;
|
extern crate mentat_sql;
|
||||||
|
|
||||||
|
mod translate;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
|
pub use translate::{
|
||||||
|
cc_to_exists,
|
||||||
|
cc_to_select,
|
||||||
|
};
|
||||||
|
|
76
query-translator/src/translate.rs
Normal file
76
query-translator/src/translate.rs
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -10,6 +10,11 @@
|
||||||
|
|
||||||
#![allow(dead_code, unused_imports)]
|
#![allow(dead_code, unused_imports)]
|
||||||
|
|
||||||
|
use mentat_core::{
|
||||||
|
Entid,
|
||||||
|
TypedValue,
|
||||||
|
};
|
||||||
|
|
||||||
use mentat_query_algebrizer::{
|
use mentat_query_algebrizer::{
|
||||||
AlgebraicQuery,
|
AlgebraicQuery,
|
||||||
ConjoiningClauses,
|
ConjoiningClauses,
|
||||||
|
@ -25,22 +30,38 @@ use mentat_sql::{
|
||||||
QueryBuilder,
|
QueryBuilder,
|
||||||
QueryFragment,
|
QueryFragment,
|
||||||
SQLiteQueryBuilder,
|
SQLiteQueryBuilder,
|
||||||
|
SQLQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
//---------------------------------------------------------
|
//---------------------------------------------------------
|
||||||
// A Mentat-focused representation of a SQL query.
|
// 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),
|
Column(QualifiedAlias),
|
||||||
Integer(i64), // Because it's so common.
|
Entid(Entid), // Because it's so common.
|
||||||
|
Value(TypedValue),
|
||||||
}
|
}
|
||||||
|
|
||||||
type Name = String;
|
pub type Name = String;
|
||||||
struct Projection (ColumnOrExpression, Name);
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub struct ProjectedColumn(pub ColumnOrExpression, pub Name);
|
||||||
struct Op(String); // TODO
|
|
||||||
enum Constraint {
|
pub enum Projection {
|
||||||
|
Columns(Vec<ProjectedColumn>),
|
||||||
|
Star,
|
||||||
|
One,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct Op(&'static str); // TODO: we can do better than this!
|
||||||
|
|
||||||
|
pub enum Constraint {
|
||||||
Infix {
|
Infix {
|
||||||
op: Op,
|
op: Op,
|
||||||
left: ColumnOrExpression,
|
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 {
|
enum JoinOp {
|
||||||
Inner,
|
Inner,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short-hand for a list of tables all inner-joined.
|
// Short-hand for a list of tables all inner-joined.
|
||||||
struct TableList(Vec<SourceAlias>);
|
pub struct TableList(pub Vec<SourceAlias>);
|
||||||
|
|
||||||
struct Join {
|
pub struct Join {
|
||||||
left: TableOrSubquery,
|
left: TableOrSubquery,
|
||||||
op: JoinOp,
|
op: JoinOp,
|
||||||
right: TableOrSubquery,
|
right: TableOrSubquery,
|
||||||
|
@ -67,15 +98,20 @@ enum TableOrSubquery {
|
||||||
// TODO: Subquery.
|
// TODO: Subquery.
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SelectQuery {
|
pub struct SelectQuery {
|
||||||
projection: Vec<Projection>,
|
pub projection: Projection,
|
||||||
from: FromClause,
|
pub from: FromClause,
|
||||||
constraints: Vec<Constraint>,
|
pub constraints: Vec<Constraint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)) => {
|
&Column(QualifiedAlias(ref table, ref column)) => {
|
||||||
out.push_identifier(table.as_str())?;
|
out.push_identifier(table.as_str())?;
|
||||||
out.push_sql(".");
|
out.push_sql(".");
|
||||||
out.push_identifier(column.as_str())
|
push_column(out, column);
|
||||||
},
|
|
||||||
&Integer(i) => {
|
|
||||||
out.push_sql(i.to_string().as_str());
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
},
|
||||||
|
&Entid(entid) => {
|
||||||
|
out.push_sql(entid.to_string().as_str());
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
&Value(ref v) => {
|
||||||
|
out.push_typed_value(v)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueryFragment for Projection {
|
impl QueryFragment for Projection {
|
||||||
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
||||||
self.0.push_sql(out)?;
|
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_sql(" AS ");
|
||||||
out.push_identifier(self.1.as_str())
|
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 {
|
impl QueryFragment for Op {
|
||||||
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
||||||
// No escaping needed.
|
// No escaping needed.
|
||||||
out.push_sql(self.0.as_str());
|
out.push_sql(self.0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,12 +246,7 @@ impl QueryFragment for FromClause {
|
||||||
impl QueryFragment for SelectQuery {
|
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[0].push_sql(out)?;
|
self.projection.push_sql(out)?;
|
||||||
|
|
||||||
for projection in self.projection[1..].iter() {
|
|
||||||
out.push_sql(", ");
|
|
||||||
projection.push_sql(out)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
out.push_sql(" FROM ");
|
out.push_sql(" FROM ");
|
||||||
self.from.push_sql(out)?;
|
self.from.push_sql(out)?;
|
||||||
|
@ -217,7 +268,7 @@ impl QueryFragment for SelectQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SelectQuery {
|
impl SelectQuery {
|
||||||
fn to_sql_string(&self) -> Result<String, BuildQueryError> {
|
pub fn to_sql_query(&self) -> Result<SQLQuery, BuildQueryError> {
|
||||||
let mut builder = SQLiteQueryBuilder::new();
|
let mut builder = SQLiteQueryBuilder::new();
|
||||||
self.push_sql(&mut builder).map(|_| builder.finish())
|
self.push_sql(&mut builder).map(|_| builder.finish())
|
||||||
}
|
}
|
||||||
|
@ -233,21 +284,20 @@ mod tests {
|
||||||
// [:find ?x :where [?x 65537 ?v] [?x 65536 ?v]]
|
// [:find ?x :where [?x 65537 ?v] [?x 65536 ?v]]
|
||||||
let datoms00 = "datoms00".to_string();
|
let datoms00 = "datoms00".to_string();
|
||||||
let datoms01 = "datoms01".to_string();
|
let datoms01 = "datoms01".to_string();
|
||||||
let eq = Op("=".to_string());
|
let eq = Op("=");
|
||||||
let source_aliases = vec![
|
let source_aliases = vec![
|
||||||
SourceAlias(DatomsTable::Datoms, datoms00.clone()),
|
SourceAlias(DatomsTable::Datoms, datoms00.clone()),
|
||||||
SourceAlias(DatomsTable::Datoms, datoms01.clone()),
|
SourceAlias(DatomsTable::Datoms, datoms01.clone()),
|
||||||
];
|
];
|
||||||
let query = SelectQuery {
|
let query = SelectQuery {
|
||||||
projection: vec![
|
projection: Projection::Columns(
|
||||||
Projection(
|
vec![
|
||||||
|
ProjectedColumn(
|
||||||
ColumnOrExpression::Column(QualifiedAlias(datoms00.clone(), DatomsColumn::Entity)),
|
ColumnOrExpression::Column(QualifiedAlias(datoms00.clone(), DatomsColumn::Entity)),
|
||||||
"x".to_string(),
|
"x".to_string()),
|
||||||
),
|
]),
|
||||||
],
|
|
||||||
from: FromClause::TableList(TableList(source_aliases)),
|
from: FromClause::TableList(TableList(source_aliases)),
|
||||||
constraints: vec![
|
constraints: vec![
|
||||||
//ColumnOrExpression::Expression(TypedValue::Integer(15)),
|
|
||||||
Constraint::Infix {
|
Constraint::Infix {
|
||||||
op: eq.clone(),
|
op: eq.clone(),
|
||||||
left: ColumnOrExpression::Column(QualifiedAlias(datoms01.clone(), DatomsColumn::Value)),
|
left: ColumnOrExpression::Column(QualifiedAlias(datoms01.clone(), DatomsColumn::Value)),
|
||||||
|
@ -256,18 +306,19 @@ mod tests {
|
||||||
Constraint::Infix {
|
Constraint::Infix {
|
||||||
op: eq.clone(),
|
op: eq.clone(),
|
||||||
left: ColumnOrExpression::Column(QualifiedAlias(datoms00.clone(), DatomsColumn::Attribute)),
|
left: ColumnOrExpression::Column(QualifiedAlias(datoms00.clone(), DatomsColumn::Attribute)),
|
||||||
right: ColumnOrExpression::Integer(65537),
|
right: ColumnOrExpression::Entid(65537),
|
||||||
},
|
},
|
||||||
Constraint::Infix {
|
Constraint::Infix {
|
||||||
op: eq.clone(),
|
op: eq.clone(),
|
||||||
left: ColumnOrExpression::Column(QualifiedAlias(datoms01.clone(), DatomsColumn::Attribute)),
|
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);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
60
query-translator/tests/translate.rs
Normal file
60
query-translator/tests/translate.rs
Normal file
|
@ -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())]);
|
||||||
|
}
|
Loading…
Reference in a new issue