Implement projection and querying. (#353) r=nalexander
* Add a failing test for EDN parsing '…'. * Expose a SQLValueType trait to get value_type_tag values out of a ValueType. * Add accessors to FindSpec. * Implement querying. * Implement rudimentary projection. * Export mentat_db::new_connection. * Export symbols from mentat. * Add rudimentary end-to-end query tests.
This commit is contained in:
parent
f86b24001f
commit
70b112801c
19 changed files with 821 additions and 74 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -48,11 +48,17 @@ path = "db"
|
|||
[dependencies.mentat_query]
|
||||
path = "query"
|
||||
|
||||
[dependencies.mentat_query_algebrizer]
|
||||
path = "query-algebrizer"
|
||||
|
||||
[dependencies.mentat_query_parser]
|
||||
path = "query-parser"
|
||||
|
||||
[dependencies.mentat_query_algebrizer]
|
||||
path = "query-algebrizer"
|
||||
[dependencies.mentat_query_projector]
|
||||
path = "query-projector"
|
||||
|
||||
[dependencies.mentat_query_sql]
|
||||
path = "query-sql"
|
||||
|
||||
[dependencies.mentat_query_translator]
|
||||
path = "query-translator"
|
||||
|
|
19
db/src/db.rs
19
db/src/db.rs
|
@ -331,6 +331,25 @@ pub fn ensure_current_version(conn: &mut rusqlite::Connection) -> Result<DB> {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait SQLValueType {
|
||||
fn value_type_tag(&self) -> i32;
|
||||
}
|
||||
|
||||
impl SQLValueType for ValueType {
|
||||
fn value_type_tag(&self) -> i32 {
|
||||
match *self {
|
||||
ValueType::Ref => 0,
|
||||
ValueType::Boolean => 1,
|
||||
ValueType::Instant => 4,
|
||||
// SQLite distinguishes integral from decimal types, allowing long and double to share a tag.
|
||||
ValueType::Long => 5,
|
||||
ValueType::Double => 5,
|
||||
ValueType::String => 10,
|
||||
ValueType::Keyword => 13,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TypedSQLValue {
|
||||
fn from_sql_value_pair(value: rusqlite::types::Value, value_type_tag: i32) -> Result<TypedValue>;
|
||||
fn to_sql_value_pair<'a>(&'a self) -> (ToSqlOutput<'a>, i32);
|
||||
|
|
|
@ -40,6 +40,12 @@ mod upsert_resolution;
|
|||
mod values;
|
||||
mod tx;
|
||||
|
||||
pub use db::{
|
||||
SQLValueType,
|
||||
TypedSQLValue,
|
||||
new_connection,
|
||||
};
|
||||
|
||||
pub use tx::transact;
|
||||
pub use types::{
|
||||
DB,
|
||||
|
|
|
@ -193,12 +193,12 @@ pub struct ConjoiningClauses {
|
|||
pub wheres: Vec<ColumnConstraint>,
|
||||
|
||||
/// A map from var to qualified columns. Used to project.
|
||||
bindings: BTreeMap<Variable, Vec<QualifiedAlias>>,
|
||||
pub bindings: BTreeMap<Variable, Vec<QualifiedAlias>>,
|
||||
|
||||
/// A map from var to type. Whenever a var maps unambiguously to two different types, it cannot
|
||||
/// yield results, so we don't represent that case here. If a var isn't present in the map, it
|
||||
/// means that its type is not known in advance.
|
||||
known_types: BTreeMap<Variable, ValueType>,
|
||||
pub known_types: BTreeMap<Variable, ValueType>,
|
||||
|
||||
/// A mapping, similar to `bindings`, but used to pull type tags out of the store at runtime.
|
||||
/// If a var isn't present in `known_types`, it should be present here.
|
||||
|
|
|
@ -27,9 +27,9 @@ use mentat_query::{
|
|||
#[allow(dead_code)]
|
||||
pub struct AlgebraicQuery {
|
||||
default_source: SrcVar,
|
||||
find_spec: FindSpec,
|
||||
pub find_spec: FindSpec,
|
||||
has_aggregates: bool,
|
||||
limit: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
pub cc: cc::ConjoiningClauses,
|
||||
}
|
||||
|
||||
|
|
35
query-projector/Cargo.toml
Normal file
35
query-projector/Cargo.toml
Normal file
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "mentat_query_projector"
|
||||
version = "0.0.1"
|
||||
workspace = ".."
|
||||
|
||||
[dependencies]
|
||||
error-chain = "0.9.0"
|
||||
|
||||
[dependencies.rusqlite]
|
||||
version = "0.9.5"
|
||||
# System sqlite might be very old.
|
||||
features = ["bundled"]
|
||||
|
||||
[dependencies.mentat_core]
|
||||
path = "../core"
|
||||
|
||||
[dependencies.mentat_db]
|
||||
path = "../db"
|
||||
|
||||
[dependencies.mentat_sql]
|
||||
path = "../sql"
|
||||
|
||||
[dependencies.mentat_query]
|
||||
path = "../query"
|
||||
|
||||
[dependencies.mentat_query_algebrizer]
|
||||
path = "../query-algebrizer"
|
||||
|
||||
# Only for tests.
|
||||
[dev-dependencies.mentat_query_parser]
|
||||
path = "../query-parser"
|
||||
|
||||
[dependencies.mentat_query_sql]
|
||||
path = "../query-sql"
|
||||
|
6
query-projector/README.md
Normal file
6
query-projector/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
This module handles the derivation from an algebrized query of two things:
|
||||
|
||||
- A SQL projection: a mapping from columns mentioned in the body of the query to columns in the output.
|
||||
- A Datalog projection: a function that consumes rows of the appropriate shape (as defined by the SQL projection) to yield one of the four kinds of Datalog query result.
|
||||
|
||||
These two must naturally coordinate, and so they are both produced here.
|
423
query-projector/src/lib.rs
Normal file
423
query-projector/src/lib.rs
Normal file
|
@ -0,0 +1,423 @@
|
|||
// 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.
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
extern crate rusqlite;
|
||||
|
||||
extern crate mentat_core;
|
||||
extern crate mentat_db; // For value conversion.
|
||||
extern crate mentat_query;
|
||||
extern crate mentat_query_algebrizer;
|
||||
extern crate mentat_query_sql;
|
||||
extern crate mentat_sql;
|
||||
|
||||
use std::iter;
|
||||
|
||||
use rusqlite::{
|
||||
Row,
|
||||
Rows,
|
||||
};
|
||||
|
||||
use mentat_core::{
|
||||
TypedValue,
|
||||
};
|
||||
|
||||
use mentat_db::{
|
||||
SQLValueType,
|
||||
TypedSQLValue,
|
||||
};
|
||||
|
||||
use mentat_query::{
|
||||
Element,
|
||||
FindSpec,
|
||||
PlainSymbol,
|
||||
Variable,
|
||||
};
|
||||
|
||||
use mentat_query_algebrizer::{
|
||||
AlgebraicQuery,
|
||||
DatomsColumn,
|
||||
QualifiedAlias,
|
||||
/*
|
||||
ConjoiningClauses,
|
||||
DatomsTable,
|
||||
SourceAlias,
|
||||
*/
|
||||
};
|
||||
|
||||
use mentat_query_sql::{
|
||||
ColumnOrExpression,
|
||||
/*
|
||||
Constraint,
|
||||
FromClause,
|
||||
*/
|
||||
Name,
|
||||
Projection,
|
||||
ProjectedColumn,
|
||||
/*
|
||||
SelectQuery,
|
||||
TableList,
|
||||
*/
|
||||
};
|
||||
|
||||
error_chain! {
|
||||
types {
|
||||
Error, ErrorKind, ResultExt, Result;
|
||||
}
|
||||
|
||||
foreign_links {
|
||||
Rusqlite(rusqlite::Error);
|
||||
}
|
||||
|
||||
links {
|
||||
DbError(mentat_db::Error, mentat_db::ErrorKind);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum QueryResults {
|
||||
Scalar(Option<TypedValue>),
|
||||
Tuple(Option<Vec<TypedValue>>),
|
||||
Coll(Vec<TypedValue>),
|
||||
Rel(Vec<Vec<TypedValue>>),
|
||||
}
|
||||
|
||||
impl QueryResults {
|
||||
pub fn len(&self) -> usize {
|
||||
use QueryResults::*;
|
||||
match self {
|
||||
&Scalar(ref o) => if o.is_some() { 1 } else { 0 },
|
||||
&Tuple(ref o) => if o.is_some() { 1 } else { 0 },
|
||||
&Coll(ref v) => v.len(),
|
||||
&Rel(ref v) => v.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Index = i32; // See rusqlite::RowIndex.
|
||||
type ValueTypeTag = i32;
|
||||
enum TypedIndex {
|
||||
Known(Index, ValueTypeTag),
|
||||
Unknown(Index, Index),
|
||||
}
|
||||
|
||||
impl TypedIndex {
|
||||
/// Look up this index and type(index) pair in the provided row.
|
||||
/// This function will panic if:
|
||||
///
|
||||
/// - This is an `Unknown` and the retrieved type code isn't an i32.
|
||||
/// - If the retrieved value can't be coerced to a rusqlite `Value`.
|
||||
/// - Either index is out of bounds.
|
||||
///
|
||||
/// Because we construct our SQL projection list, the code that stored the data, and this
|
||||
/// consumer, a panic here implies that we have a bad bug — we put data of a very wrong type in
|
||||
/// a row, and thus can't coerce to Value, we're retrieving from the wrong place, or our
|
||||
/// generated SQL is junk.
|
||||
///
|
||||
/// This function will return a runtime error if the type code is unknown, or the value is
|
||||
/// otherwise not convertible by the DB layer.
|
||||
fn lookup<'a, 'stmt>(&self, row: &Row<'a, 'stmt>) -> Result<TypedValue> {
|
||||
use TypedIndex::*;
|
||||
|
||||
match self {
|
||||
&Known(value_index, value_type) => {
|
||||
let v: rusqlite::types::Value = row.get(value_index);
|
||||
TypedValue::from_sql_value_pair(v, value_type).map_err(|e| e.into())
|
||||
},
|
||||
&Unknown(value_index, type_index) => {
|
||||
let v: rusqlite::types::Value = row.get(value_index);
|
||||
let value_type_tag: i32 = row.get(type_index);
|
||||
TypedValue::from_sql_value_pair(v, value_type_tag).map_err(|e| e.into())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn column_name(var: &Variable) -> Name {
|
||||
let &Variable(PlainSymbol(ref s)) = var;
|
||||
s.clone()
|
||||
}
|
||||
|
||||
fn value_type_tag_name(var: &Variable) -> Name {
|
||||
let &Variable(PlainSymbol(ref s)) = var;
|
||||
format!("{}_value_type_tag", s)
|
||||
}
|
||||
|
||||
/// Walk an iterator of `Element`s, collecting projector templates and columns.
|
||||
///
|
||||
/// Returns a pair: the SQL projection (which should always be a `Projection::Columns`)
|
||||
/// and a `Vec` of `TypedIndex` 'keys' to use when looking up values.
|
||||
///
|
||||
/// Callers must ensure that every `Element` is distinct -- a query like
|
||||
///
|
||||
/// ```edn
|
||||
/// [:find ?x ?x :where [?x _ _]]
|
||||
/// ```
|
||||
///
|
||||
/// should fail to parse. See #358.
|
||||
fn project_elements<'a, I: IntoIterator<Item = &'a Element>>(
|
||||
count: usize,
|
||||
elements: I,
|
||||
query: &AlgebraicQuery) -> (Projection, Vec<TypedIndex>) {
|
||||
|
||||
let mut cols = Vec::with_capacity(count);
|
||||
let mut i: i32 = 0;
|
||||
let mut templates = vec![];
|
||||
|
||||
for e in elements {
|
||||
match e {
|
||||
// Each time we come across a variable, we push a SQL column
|
||||
// into the SQL projection, aliased to the name of the variable,
|
||||
// and we push an annotated index into the projector.
|
||||
&Element::Variable(ref var) => {
|
||||
// Every variable should be bound by the top-level CC to at least
|
||||
// one column in the query. If that constraint is violated it's a
|
||||
// bug in our code, so it's appropriate to panic here.
|
||||
let columns = query.cc
|
||||
.bindings
|
||||
.get(var)
|
||||
.expect("Every variable has a binding");
|
||||
|
||||
let qa = columns[0].clone();
|
||||
let name = column_name(var);
|
||||
|
||||
if let Some(t) = query.cc.known_types.get(var) {
|
||||
cols.push(ProjectedColumn(ColumnOrExpression::Column(qa), name));
|
||||
let tag = t.value_type_tag();
|
||||
templates.push(TypedIndex::Known(i, tag));
|
||||
i += 1; // We used one SQL column.
|
||||
} else {
|
||||
let table = qa.0.clone();
|
||||
cols.push(ProjectedColumn(ColumnOrExpression::Column(qa), name));
|
||||
templates.push(TypedIndex::Unknown(i, i + 1));
|
||||
i += 2; // We used two SQL columns.
|
||||
|
||||
// Also project the type from the SQL query.
|
||||
let type_name = value_type_tag_name(var);
|
||||
let type_qa = QualifiedAlias(table, DatomsColumn::ValueTypeTag);
|
||||
cols.push(ProjectedColumn(ColumnOrExpression::Column(type_qa), type_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(Projection::Columns(cols), templates)
|
||||
}
|
||||
|
||||
pub trait Projector {
|
||||
fn project<'stmt>(&self, rows: Rows<'stmt>) -> Result<QueryResults>;
|
||||
}
|
||||
|
||||
struct ScalarProjector {
|
||||
template: TypedIndex,
|
||||
}
|
||||
|
||||
impl ScalarProjector {
|
||||
fn with_template(template: TypedIndex) -> ScalarProjector {
|
||||
ScalarProjector {
|
||||
template: template,
|
||||
}
|
||||
}
|
||||
|
||||
fn combine(sql: Projection, mut templates: Vec<TypedIndex>) -> CombinedProjection {
|
||||
let template = templates.pop().expect("Expected a single template");
|
||||
CombinedProjection {
|
||||
sql_projection: sql,
|
||||
datalog_projector: Box::new(ScalarProjector::with_template(template)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Projector for ScalarProjector {
|
||||
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryResults> {
|
||||
if let Some(r) = rows.next() {
|
||||
let row = r?;
|
||||
let binding = self.template.lookup(&row)?;
|
||||
Ok(QueryResults::Scalar(Some(binding)))
|
||||
} else {
|
||||
Ok(QueryResults::Scalar(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A tuple projector produces a single vector. It's the single-result version of rel.
|
||||
struct TupleProjector {
|
||||
len: usize,
|
||||
templates: Vec<TypedIndex>,
|
||||
}
|
||||
|
||||
impl TupleProjector {
|
||||
fn with_templates(len: usize, templates: Vec<TypedIndex>) -> TupleProjector {
|
||||
TupleProjector {
|
||||
len: len,
|
||||
templates: templates,
|
||||
}
|
||||
}
|
||||
|
||||
// This is exactly the same as for rel.
|
||||
fn collect_bindings<'a, 'stmt>(&self, row: Row<'a, 'stmt>) -> Result<Vec<TypedValue>> {
|
||||
assert_eq!(row.column_count(), self.len as i32);
|
||||
self.templates
|
||||
.iter()
|
||||
.map(|ti| ti.lookup(&row))
|
||||
.collect::<Result<Vec<TypedValue>>>()
|
||||
}
|
||||
|
||||
fn combine(column_count: usize, sql: Projection, templates: Vec<TypedIndex>) -> CombinedProjection {
|
||||
let p = TupleProjector::with_templates(column_count, templates);
|
||||
CombinedProjection {
|
||||
sql_projection: sql,
|
||||
datalog_projector: Box::new(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Projector for TupleProjector {
|
||||
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryResults> {
|
||||
if let Some(r) = rows.next() {
|
||||
let row = r?;
|
||||
let bindings = self.collect_bindings(row)?;
|
||||
Ok(QueryResults::Tuple(Some(bindings)))
|
||||
} else {
|
||||
Ok(QueryResults::Tuple(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A rel projector produces a vector of vectors.
|
||||
/// Each inner vector is the same size, and sourced from the same columns.
|
||||
/// One inner vector is produced per `Row`.
|
||||
/// Each column in the inner vector is the result of taking one or two columns from
|
||||
/// the `Row`: one for the value and optionally one for the type tag.
|
||||
struct RelProjector {
|
||||
len: usize,
|
||||
templates: Vec<TypedIndex>,
|
||||
}
|
||||
|
||||
impl RelProjector {
|
||||
fn with_templates(len: usize, templates: Vec<TypedIndex>) -> RelProjector {
|
||||
RelProjector {
|
||||
len: len,
|
||||
templates: templates,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_bindings<'a, 'stmt>(&self, row: Row<'a, 'stmt>) -> Result<Vec<TypedValue>> {
|
||||
assert_eq!(row.column_count(), self.len as i32);
|
||||
self.templates
|
||||
.iter()
|
||||
.map(|ti| ti.lookup(&row))
|
||||
.collect::<Result<Vec<TypedValue>>>()
|
||||
}
|
||||
|
||||
fn combine(column_count: usize, sql: Projection, templates: Vec<TypedIndex>) -> CombinedProjection {
|
||||
let p = RelProjector::with_templates(column_count, templates);
|
||||
CombinedProjection {
|
||||
sql_projection: sql,
|
||||
datalog_projector: Box::new(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Projector for RelProjector {
|
||||
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryResults> {
|
||||
let mut out: Vec<Vec<TypedValue>> = vec![];
|
||||
while let Some(r) = rows.next() {
|
||||
let row = r?;
|
||||
let bindings = self.collect_bindings(row)?;
|
||||
out.push(bindings);
|
||||
}
|
||||
Ok(QueryResults::Rel(out))
|
||||
}
|
||||
}
|
||||
|
||||
/// A coll projector produces a vector of values.
|
||||
/// Each value is sourced from the same column.
|
||||
struct CollProjector {
|
||||
template: TypedIndex,
|
||||
}
|
||||
|
||||
impl CollProjector {
|
||||
fn with_template(template: TypedIndex) -> CollProjector {
|
||||
CollProjector {
|
||||
template: template,
|
||||
}
|
||||
}
|
||||
|
||||
fn combine(sql: Projection, mut templates: Vec<TypedIndex>) -> CombinedProjection {
|
||||
let template = templates.pop().expect("Expected a single template");
|
||||
CombinedProjection {
|
||||
sql_projection: sql,
|
||||
datalog_projector: Box::new(CollProjector::with_template(template)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Projector for CollProjector {
|
||||
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryResults> {
|
||||
let mut out: Vec<TypedValue> = vec![];
|
||||
while let Some(r) = rows.next() {
|
||||
let row = r?;
|
||||
let binding = self.template.lookup(&row)?;
|
||||
out.push(binding);
|
||||
}
|
||||
Ok(QueryResults::Coll(out))
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines the two things you need to turn a query into SQL and turn its results into
|
||||
/// `QueryResults`.
|
||||
pub struct CombinedProjection {
|
||||
/// A SQL projection, mapping columns mentioned in the body of the query to columns in the
|
||||
/// output.
|
||||
pub sql_projection: Projection,
|
||||
|
||||
/// A Datalog projection. This consumes rows of the appropriate shape (as defined by
|
||||
/// the SQL projection) to yield one of the four kinds of Datalog query result.
|
||||
pub datalog_projector: Box<Projector>,
|
||||
}
|
||||
|
||||
/// Compute a suitable SQL projection for an algebrized query.
|
||||
/// This takes into account a number of things:
|
||||
/// - The variable list in the find spec.
|
||||
/// - The presence of any aggregate operations in the find spec. TODO: for now we only handle
|
||||
/// simple variables
|
||||
/// - The bindings established by the topmost CC.
|
||||
/// - The types known at algebrizing time.
|
||||
/// - The types extracted from the store for unknown attributes.
|
||||
pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection {
|
||||
use self::FindSpec::*;
|
||||
|
||||
match query.find_spec {
|
||||
FindColl(ref element) => {
|
||||
let (cols, templates) = project_elements(1, iter::once(element), query);
|
||||
CollProjector::combine(cols, templates)
|
||||
},
|
||||
|
||||
FindScalar(ref element) => {
|
||||
let (cols, templates) = project_elements(1, iter::once(element), query);
|
||||
ScalarProjector::combine(cols, templates)
|
||||
},
|
||||
|
||||
FindRel(ref elements) => {
|
||||
let column_count = query.find_spec.expected_column_count();
|
||||
let (cols, templates) = project_elements(column_count, elements, query);
|
||||
RelProjector::combine(column_count, cols, templates)
|
||||
},
|
||||
|
||||
FindTuple(ref elements) => {
|
||||
let column_count = query.find_spec.expected_column_count();
|
||||
let (cols, templates) = project_elements(column_count, elements, query);
|
||||
TupleProjector::combine(column_count, cols, templates)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
21
query-sql/Cargo.toml
Normal file
21
query-sql/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "mentat_query_sql"
|
||||
version = "0.0.1"
|
||||
workspace = ".."
|
||||
|
||||
[dependencies]
|
||||
[dependencies.mentat_core]
|
||||
path = "../core"
|
||||
|
||||
[dependencies.mentat_sql]
|
||||
path = "../sql"
|
||||
|
||||
[dependencies.mentat_query]
|
||||
path = "../query"
|
||||
|
||||
[dependencies.mentat_query_algebrizer]
|
||||
path = "../query-algebrizer"
|
||||
|
||||
# Only for tests.
|
||||
[dev-dependencies.mentat_query_parser]
|
||||
path = "../query-parser"
|
|
@ -8,7 +8,10 @@
|
|||
// 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)]
|
||||
extern crate mentat_core;
|
||||
extern crate mentat_query;
|
||||
extern crate mentat_query_algebrizer;
|
||||
extern crate mentat_sql;
|
||||
|
||||
use mentat_core::{
|
||||
Entid,
|
||||
|
@ -16,16 +19,11 @@ use mentat_core::{
|
|||
};
|
||||
|
||||
use mentat_query_algebrizer::{
|
||||
AlgebraicQuery,
|
||||
ConjoiningClauses,
|
||||
DatomsColumn,
|
||||
DatomsTable,
|
||||
QualifiedAlias,
|
||||
SourceAlias,
|
||||
};
|
||||
|
||||
use mentat_sql;
|
||||
|
||||
use mentat_sql::{
|
||||
BuildQueryResult,
|
||||
QueryBuilder,
|
||||
|
@ -80,6 +78,7 @@ impl Constraint {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
enum JoinOp {
|
||||
Inner,
|
||||
}
|
||||
|
@ -94,6 +93,7 @@ pub struct Join {
|
|||
// TODO: constraints (ON, USING).
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
enum TableOrSubquery {
|
||||
Table(SourceAlias),
|
||||
// TODO: Subquery.
|
||||
|
@ -278,6 +278,7 @@ impl SelectQuery {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mentat_query_algebrizer::DatomsTable;
|
||||
|
||||
#[test]
|
||||
fn test_end_to_end() {
|
|
@ -19,3 +19,9 @@ path = "../query-algebrizer"
|
|||
# Only for tests.
|
||||
[dev-dependencies.mentat_query_parser]
|
||||
path = "../query-parser"
|
||||
|
||||
[dependencies.mentat_query_projector]
|
||||
path = "../query-projector"
|
||||
|
||||
[dependencies.mentat_query_sql]
|
||||
path = "../query-sql"
|
||||
|
|
|
@ -11,16 +11,18 @@
|
|||
extern crate mentat_core;
|
||||
extern crate mentat_query;
|
||||
extern crate mentat_query_algebrizer;
|
||||
extern crate mentat_query_projector;
|
||||
extern crate mentat_query_sql;
|
||||
extern crate mentat_sql;
|
||||
|
||||
mod translate;
|
||||
mod types;
|
||||
|
||||
pub use types::{
|
||||
pub use mentat_query_sql::{
|
||||
Projection,
|
||||
};
|
||||
|
||||
pub use translate::{
|
||||
cc_to_exists,
|
||||
cc_to_select,
|
||||
query_to_select,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,13 @@
|
|||
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use mentat_query::{
|
||||
Element,
|
||||
FindSpec,
|
||||
PlainSymbol,
|
||||
Variable,
|
||||
};
|
||||
|
||||
use mentat_query_algebrizer::{
|
||||
AlgebraicQuery,
|
||||
ColumnConstraint,
|
||||
|
@ -20,11 +27,19 @@ use mentat_query_algebrizer::{
|
|||
SourceAlias,
|
||||
};
|
||||
|
||||
use types::{
|
||||
use mentat_query_projector::{
|
||||
CombinedProjection,
|
||||
Projector,
|
||||
query_projection,
|
||||
};
|
||||
|
||||
use mentat_query_sql::{
|
||||
ColumnOrExpression,
|
||||
Constraint,
|
||||
FromClause,
|
||||
Name,
|
||||
Projection,
|
||||
ProjectedColumn,
|
||||
SelectQuery,
|
||||
TableList,
|
||||
};
|
||||
|
@ -57,10 +72,12 @@ impl ToConstraint for ColumnConstraint {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct CombinedSelectQuery {
|
||||
pub query: SelectQuery,
|
||||
pub projector: Box<Projector>,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
fn cc_to_select_query(projection: Projection, cc: ConjoiningClauses) -> SelectQuery {
|
||||
SelectQuery {
|
||||
projection: projection,
|
||||
from: FromClause::TableList(TableList(cc.from)),
|
||||
|
@ -71,6 +88,20 @@ pub fn cc_to_select(projection: Projection, cc: ConjoiningClauses) -> SelectQuer
|
|||
}
|
||||
}
|
||||
|
||||
pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery {
|
||||
cc_to_select(Projection::One, cc)
|
||||
/// Consume a provided `ConjoiningClauses` to yield a new
|
||||
/// `SelectQuery`. A projection list must also be provided.
|
||||
pub fn cc_to_select(projection: CombinedProjection, cc: ConjoiningClauses) -> CombinedSelectQuery {
|
||||
let CombinedProjection { sql_projection, datalog_projector } = projection;
|
||||
CombinedSelectQuery {
|
||||
query: cc_to_select_query(sql_projection, cc),
|
||||
projector: datalog_projector,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_to_select(query: AlgebraicQuery) -> CombinedSelectQuery {
|
||||
cc_to_select(query_projection(&query), query.cc)
|
||||
}
|
||||
|
||||
pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery {
|
||||
cc_to_select_query(Projection::One, cc)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ use mentat_core::{
|
|||
use mentat_query_parser::parse_find_string;
|
||||
use mentat_query_algebrizer::algebrize;
|
||||
use mentat_query_translator::{
|
||||
cc_to_exists,
|
||||
query_to_select,
|
||||
};
|
||||
|
||||
use mentat_sql::SQLQuery;
|
||||
|
@ -42,7 +42,26 @@ fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_exists() {
|
||||
#[should_panic(expected = "parse failed")]
|
||||
fn test_coll() {
|
||||
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).expect("parse failed");
|
||||
let algebrized = algebrize(&schema, parsed);
|
||||
let select = query_to_select(algebrized);
|
||||
let SQLQuery { sql, args } = select.query.to_sql_query().unwrap();
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0");
|
||||
assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rel() {
|
||||
let mut schema = Schema::default();
|
||||
associate_ident(&mut schema, NamespacedKeyword::new("foo", "bar"), 99);
|
||||
add_attribute(&mut schema, 99, Attribute {
|
||||
|
@ -51,10 +70,10 @@ fn test_exists() {
|
|||
});
|
||||
|
||||
let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#;
|
||||
let parsed = parse_find_string(input).unwrap();
|
||||
let parsed = parse_find_string(input).expect("parse failed");
|
||||
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");
|
||||
let select = query_to_select(algebrized);
|
||||
let SQLQuery { sql, args } = select.query.to_sql_query().unwrap();
|
||||
assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0");
|
||||
assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]);
|
||||
}
|
||||
|
|
|
@ -340,35 +340,48 @@ pub enum FindSpec {
|
|||
}
|
||||
|
||||
/// Returns true if the provided `FindSpec` returns at most one result.
|
||||
pub fn is_unit_limited(spec: &FindSpec) -> bool {
|
||||
match spec {
|
||||
&FindSpec::FindScalar(..) => true,
|
||||
&FindSpec::FindTuple(..) => true,
|
||||
&FindSpec::FindRel(..) => false,
|
||||
&FindSpec::FindColl(..) => false,
|
||||
impl FindSpec {
|
||||
pub fn is_unit_limited(&self) -> bool {
|
||||
use FindSpec::*;
|
||||
match self {
|
||||
&FindScalar(..) => true,
|
||||
&FindTuple(..) => true,
|
||||
&FindRel(..) => false,
|
||||
&FindColl(..) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the provided `FindSpec` cares about distinct results.
|
||||
///
|
||||
/// I use the words "cares about" because find is generally defined in terms of producing distinct
|
||||
/// results at the Datalog level.
|
||||
///
|
||||
/// Two of the find specs (scalar and tuple) produce only a single result. Those don't need to be
|
||||
/// run with `SELECT DISTINCT`, because we're only consuming a single result. Those queries will be
|
||||
/// run with `LIMIT 1`.
|
||||
///
|
||||
/// Additionally, some projections cannot produce duplicate results: `[:find (max ?x) …]`, for
|
||||
/// example.
|
||||
///
|
||||
/// This function gives us the hook to add that logic when we're ready.
|
||||
///
|
||||
/// Beyond this, `DISTINCT` is not always needed. For example, in some kinds of accumulation or
|
||||
/// sampling projections we might not need to do it at the SQL level because we're consuming into
|
||||
/// a dupe-eliminating data structure like a Set, or we know that a particular query cannot produce
|
||||
/// duplicate results.
|
||||
pub fn requires_distinct(spec: &FindSpec) -> bool {
|
||||
return !is_unit_limited(spec);
|
||||
pub fn expected_column_count(&self) -> usize {
|
||||
use FindSpec::*;
|
||||
match self {
|
||||
&FindScalar(..) => 1,
|
||||
&FindColl(..) => 1,
|
||||
&FindTuple(ref elems) | &FindRel(ref elems) => elems.len(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns true if the provided `FindSpec` cares about distinct results.
|
||||
///
|
||||
/// I use the words "cares about" because find is generally defined in terms of producing distinct
|
||||
/// results at the Datalog level.
|
||||
///
|
||||
/// Two of the find specs (scalar and tuple) produce only a single result. Those don't need to be
|
||||
/// run with `SELECT DISTINCT`, because we're only consuming a single result. Those queries will be
|
||||
/// run with `LIMIT 1`.
|
||||
///
|
||||
/// Additionally, some projections cannot produce duplicate results: `[:find (max ?x) …]`, for
|
||||
/// example.
|
||||
///
|
||||
/// This function gives us the hook to add that logic when we're ready.
|
||||
///
|
||||
/// Beyond this, `DISTINCT` is not always needed. For example, in some kinds of accumulation or
|
||||
/// sampling projections we might not need to do it at the SQL level because we're consuming into
|
||||
/// a dupe-eliminating data structure like a Set, or we know that a particular query cannot produce
|
||||
/// duplicate results.
|
||||
pub fn requires_distinct(&self) -> bool {
|
||||
!self.is_unit_limited()
|
||||
}
|
||||
}
|
||||
|
||||
// Note that the "implicit blank" rule applies.
|
||||
|
|
|
@ -15,6 +15,7 @@ use rusqlite;
|
|||
use edn;
|
||||
use mentat_db;
|
||||
use mentat_query_parser;
|
||||
use mentat_query_projector;
|
||||
use mentat_sql;
|
||||
use mentat_tx_parser;
|
||||
|
||||
|
@ -31,6 +32,7 @@ error_chain! {
|
|||
links {
|
||||
DbError(mentat_db::Error, mentat_db::ErrorKind);
|
||||
QueryParseError(mentat_query_parser::Error, mentat_query_parser::ErrorKind);
|
||||
ProjectorError(mentat_query_projector::Error, mentat_query_projector::ErrorKind);
|
||||
SqlError(mentat_sql::Error, mentat_sql::ErrorKind);
|
||||
TxParseError(mentat_tx_parser::Error, mentat_tx_parser::ErrorKind);
|
||||
}
|
||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -25,6 +25,7 @@ extern crate mentat_db;
|
|||
extern crate mentat_query;
|
||||
extern crate mentat_query_algebrizer;
|
||||
extern crate mentat_query_parser;
|
||||
extern crate mentat_query_projector;
|
||||
extern crate mentat_query_translator;
|
||||
extern crate mentat_sql;
|
||||
extern crate mentat_tx_parser;
|
||||
|
@ -46,6 +47,17 @@ pub fn get_connection() -> Connection {
|
|||
return Connection::open_in_memory().unwrap();
|
||||
}
|
||||
|
||||
pub use mentat_db::{
|
||||
new_connection,
|
||||
};
|
||||
|
||||
pub use query::{
|
||||
NamespacedKeyword,
|
||||
PlainSymbol,
|
||||
QueryResults,
|
||||
q_once,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use edn::symbols::Keyword;
|
||||
|
|
45
src/query.rs
45
src/query.rs
|
@ -10,6 +10,9 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rusqlite;
|
||||
use rusqlite::types::ToSql;
|
||||
|
||||
use mentat_core::{
|
||||
Schema,
|
||||
TypedValue,
|
||||
|
@ -17,6 +20,11 @@ use mentat_core::{
|
|||
|
||||
use mentat_query_algebrizer::algebrize;
|
||||
|
||||
pub use mentat_query::{
|
||||
NamespacedKeyword,
|
||||
PlainSymbol,
|
||||
};
|
||||
|
||||
use mentat_query_parser::{
|
||||
parse_find_string,
|
||||
};
|
||||
|
@ -26,21 +34,15 @@ use mentat_sql::{
|
|||
};
|
||||
|
||||
use mentat_query_translator::{
|
||||
cc_to_select,
|
||||
Projection,
|
||||
query_to_select,
|
||||
};
|
||||
|
||||
pub use mentat_query_projector::{
|
||||
QueryResults,
|
||||
};
|
||||
|
||||
use errors::Result;
|
||||
|
||||
use rusqlite;
|
||||
|
||||
pub enum QueryResults {
|
||||
Scalar(Option<TypedValue>),
|
||||
Tuple(Vec<TypedValue>),
|
||||
Coll(Vec<TypedValue>),
|
||||
Rel(Vec<Vec<TypedValue>>),
|
||||
}
|
||||
|
||||
pub type QueryExecutionResult = Result<QueryResults>;
|
||||
|
||||
/// Take an EDN query string, a reference to a open SQLite connection, a Mentat DB, and an optional
|
||||
|
@ -59,21 +61,22 @@ pub fn q_once<'sqlite, 'schema, 'query>
|
|||
// TODO: validate inputs.
|
||||
let parsed = parse_find_string(query)?;
|
||||
let algebrized = algebrize(schema, parsed);
|
||||
let projection = Projection::Star;
|
||||
let select = cc_to_select(projection, algebrized.cc);
|
||||
let SQLQuery { sql, args } = select.to_sql_query()?;
|
||||
let select = query_to_select(algebrized);
|
||||
let SQLQuery { sql, args } = select.query.to_sql_query()?;
|
||||
|
||||
/*
|
||||
let mut statement = sqlite.prepare(sql.as_str())?;
|
||||
|
||||
let mut rows = if args.is_empty() {
|
||||
let rows = if args.is_empty() {
|
||||
statement.query(&[])?
|
||||
} else {
|
||||
statement.query_named(args.map(|(k, v)| (k.as_str(), &v)))?
|
||||
let refs: Vec<(&str, &ToSql)> =
|
||||
args.iter()
|
||||
.map(|&(ref k, ref v)| (k.as_str(), v as &ToSql))
|
||||
.collect();
|
||||
statement.query_named(refs.as_slice())?
|
||||
};
|
||||
*/
|
||||
|
||||
|
||||
|
||||
Ok(QueryResults::Scalar(Some(TypedValue::Boolean(true))))
|
||||
select.projector
|
||||
.project(rows)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
|
142
tests/query.rs
Normal file
142
tests/query.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
// 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 time;
|
||||
|
||||
extern crate mentat;
|
||||
extern crate mentat_core;
|
||||
extern crate mentat_db;
|
||||
|
||||
use mentat_core::{
|
||||
TypedValue,
|
||||
ValueType,
|
||||
};
|
||||
|
||||
use mentat::{
|
||||
NamespacedKeyword,
|
||||
QueryResults,
|
||||
new_connection,
|
||||
q_once,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_rel() {
|
||||
let mut c = new_connection("").expect("Couldn't open conn.");
|
||||
let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB.");
|
||||
|
||||
// Rel.
|
||||
let start = time::PreciseTime::now();
|
||||
let results = q_once(&c, &db.schema,
|
||||
"[:find ?x ?ident :where [?x :db/ident ?ident]]", None)
|
||||
.expect("Query failed");
|
||||
let end = time::PreciseTime::now();
|
||||
|
||||
// This will need to change each time we add a default ident.
|
||||
assert_eq!(37, results.len());
|
||||
|
||||
// Every row is a pair of a Ref and a Keyword.
|
||||
if let QueryResults::Rel(ref rel) = results {
|
||||
for r in rel {
|
||||
assert_eq!(r.len(), 2);
|
||||
assert!(r[0].matches_type(ValueType::Ref));
|
||||
assert!(r[1].matches_type(ValueType::Keyword));
|
||||
}
|
||||
} else {
|
||||
panic!("Expected rel.");
|
||||
}
|
||||
|
||||
println!("{:?}", results);
|
||||
println!("Rel took {}µs", start.to(end).num_microseconds().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_failing_scalar() {
|
||||
let mut c = new_connection("").expect("Couldn't open conn.");
|
||||
let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB.");
|
||||
|
||||
// Scalar that fails.
|
||||
let start = time::PreciseTime::now();
|
||||
let results = q_once(&c, &db.schema,
|
||||
"[:find ?x . :where [?x :db/fulltext true]]", None)
|
||||
.expect("Query failed");
|
||||
let end = time::PreciseTime::now();
|
||||
|
||||
assert_eq!(0, results.len());
|
||||
|
||||
if let QueryResults::Scalar(None) = results {
|
||||
} else {
|
||||
panic!("Expected failed scalar.");
|
||||
}
|
||||
|
||||
println!("Failing scalar took {}µs", start.to(end).num_microseconds().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scalar() {
|
||||
let mut c = new_connection("").expect("Couldn't open conn.");
|
||||
let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB.");
|
||||
|
||||
// Scalar that succeeds.
|
||||
let start = time::PreciseTime::now();
|
||||
let results = q_once(&c, &db.schema,
|
||||
"[:find ?ident . :where [24 :db/ident ?ident]]", None)
|
||||
.expect("Query failed");
|
||||
let end = time::PreciseTime::now();
|
||||
|
||||
assert_eq!(1, results.len());
|
||||
|
||||
if let QueryResults::Scalar(Some(TypedValue::Keyword(ref kw))) = results {
|
||||
// Should be '24'.
|
||||
assert_eq!(&NamespacedKeyword::new("db.type", "keyword"), kw);
|
||||
assert_eq!(24,
|
||||
db.schema.get_entid(kw).unwrap());
|
||||
} else {
|
||||
panic!("Expected scalar.");
|
||||
}
|
||||
|
||||
println!("{:?}", results);
|
||||
println!("Scalar took {}µs", start.to(end).num_microseconds().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple() {
|
||||
let mut c = new_connection("").expect("Couldn't open conn.");
|
||||
let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB.");
|
||||
|
||||
// Tuple.
|
||||
let start = time::PreciseTime::now();
|
||||
let results = q_once(&c, &db.schema,
|
||||
"[:find [?index ?cardinality]
|
||||
:where [:db/txInstant :db/index ?index]
|
||||
[:db/txInstant :db/cardinality ?cardinality]]",
|
||||
None)
|
||||
.expect("Query failed");
|
||||
let end = time::PreciseTime::now();
|
||||
|
||||
assert_eq!(1, results.len());
|
||||
|
||||
if let QueryResults::Tuple(Some(ref tuple)) = results {
|
||||
let cardinality_one = NamespacedKeyword::new("db.cardinality", "one");
|
||||
assert_eq!(tuple.len(), 2);
|
||||
assert_eq!(tuple[0], TypedValue::Boolean(true));
|
||||
assert_eq!(tuple[1], TypedValue::Ref(db.schema.get_entid(&cardinality_one).unwrap()));
|
||||
} else {
|
||||
panic!("Expected tuple.");
|
||||
}
|
||||
|
||||
println!("{:?}", results);
|
||||
println!("Tuple took {}µs", start.to(end).num_microseconds().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coll() {
|
||||
// We can't test Coll yet, because the EDN parser is incomplete.
|
||||
}
|
||||
|
Loading…
Reference in a new issue