Parse :in, pass inputs through to querying. (#418) r=nalexander

This commit downgrades error_chain to 0.8.1 in order to fix trait bounds
on errors.
This commit is contained in:
Richard Newman 2017-04-18 13:20:00 -07:00
commit aa14a71019
22 changed files with 361 additions and 78 deletions

View file

@ -18,7 +18,7 @@ rustc_version = "0.1.7"
[dependencies] [dependencies]
clap = "2.19.3" clap = "2.19.3"
error-chain = "0.9.0" error-chain = "0.8.1"
nickel = "0.9.0" nickel = "0.9.0"
slog = "1.4.0" slog = "1.4.0"
slog-scope = "0.2.2" slog-scope = "0.2.2"

View file

@ -17,6 +17,7 @@ extern crate edn;
pub mod values; pub mod values;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt;
use std::rc::Rc; use std::rc::Rc;
use self::ordered_float::OrderedFloat; use self::ordered_float::OrderedFloat;
use self::edn::NamespacedKeyword; use self::edn::NamespacedKeyword;
@ -44,19 +45,33 @@ pub enum ValueType {
} }
impl ValueType { impl ValueType {
pub fn to_edn_value(&self) -> edn::Value { pub fn to_edn_value(self) -> edn::Value {
match self { match self {
&ValueType::Ref => values::DB_TYPE_REF.clone(), ValueType::Ref => values::DB_TYPE_REF.clone(),
&ValueType::Boolean => values::DB_TYPE_BOOLEAN.clone(), ValueType::Boolean => values::DB_TYPE_BOOLEAN.clone(),
&ValueType::Instant => values::DB_TYPE_INSTANT.clone(), ValueType::Instant => values::DB_TYPE_INSTANT.clone(),
&ValueType::Long => values::DB_TYPE_LONG.clone(), ValueType::Long => values::DB_TYPE_LONG.clone(),
&ValueType::Double => values::DB_TYPE_DOUBLE.clone(), ValueType::Double => values::DB_TYPE_DOUBLE.clone(),
&ValueType::String => values::DB_TYPE_STRING.clone(), ValueType::String => values::DB_TYPE_STRING.clone(),
&ValueType::Keyword => values::DB_TYPE_KEYWORD.clone(), ValueType::Keyword => values::DB_TYPE_KEYWORD.clone(),
} }
} }
} }
impl fmt::Display for ValueType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", match *self {
ValueType::Ref => "db.type/ref",
ValueType::Boolean => "db.type/boolean",
ValueType::Instant => "db.type/instant",
ValueType::Long => "db.type/long",
ValueType::Double => "db.type/double",
ValueType::String => "db.type/string",
ValueType::Keyword => "db.type/keyword",
})
}
}
/// Represents a Mentat value in a particular value set. /// Represents a Mentat value in a particular value set.
// TODO: expand to include :db.type/{instant,url,uuid}. // TODO: expand to include :db.type/{instant,url,uuid}.
// TODO: BigInt? // TODO: BigInt?

View file

@ -4,7 +4,7 @@ version = "0.0.1"
workspace = ".." workspace = ".."
[dependencies] [dependencies]
error-chain = "0.9.0" error-chain = "0.8.1"
itertools = "0.5.9" itertools = "0.5.9"
lazy_static = "0.2.2" lazy_static = "0.2.2"
ordered-float = "0.4.0" ordered-float = "0.4.0"

View file

@ -4,7 +4,7 @@ version = "0.0.1"
workspace = ".." workspace = ".."
[dependencies] [dependencies]
error-chain = "0.9.0" error-chain = "0.8.1"
[dependencies.mentat_core] [dependencies.mentat_core]
path = "../core" path = "../core"

View file

@ -0,0 +1,72 @@
// 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.
use std::collections::BTreeMap;
use mentat_core::{
TypedValue,
ValueType,
};
use mentat_query::{
Variable,
};
pub use errors::{
Error,
ErrorKind,
Result,
};
/// Define the inputs to a query. This is in two parts: a set of values known now, and a set of
/// types known now.
/// The separate map of types is to allow queries to be algebrized without full knowledge of
/// the bindings that will be used at execution time.
/// When built correctly, `types` is guaranteed to contain the types of `values` -- use
/// `QueryInputs::new` or `QueryInputs::with_values` to construct an instance.
pub struct QueryInputs {
pub types: BTreeMap<Variable, ValueType>,
pub values: BTreeMap<Variable, TypedValue>,
}
impl Default for QueryInputs {
fn default() -> Self {
QueryInputs { types: BTreeMap::default(), values: BTreeMap::default() }
}
}
impl QueryInputs {
pub fn with_value_sequence(vals: Vec<(Variable, TypedValue)>) -> QueryInputs {
let values: BTreeMap<Variable, TypedValue> = vals.into_iter().collect();
QueryInputs::with_values(values)
}
pub fn with_values(values: BTreeMap<Variable, TypedValue>) -> QueryInputs {
QueryInputs {
types: values.iter().map(|(var, val)| (var.clone(), val.value_type())).collect(),
values: values,
}
}
pub fn new(mut types: BTreeMap<Variable, ValueType>,
values: BTreeMap<Variable, TypedValue>) -> Result<QueryInputs> {
// Make sure that the types of the values agree with those in types, and collect.
for (var, v) in values.iter() {
let t = v.value_type();
let old = types.insert(var.clone(), t);
if let Some(old) = old {
if old != t {
bail!(ErrorKind::InputTypeDisagreement(var.name(), old, t));
}
}
}
Ok(QueryInputs { types: types, values: values })
}
}

View file

@ -59,6 +59,7 @@ use types::{
TableAlias, TableAlias,
}; };
mod inputs;
mod or; mod or;
mod pattern; mod pattern;
mod predicate; mod predicate;
@ -66,6 +67,8 @@ mod resolve;
use validate::validate_or_join; use validate::validate_or_join;
pub use self::inputs::QueryInputs;
// We do this a lot for errors. // We do this a lot for errors.
trait RcCloned<T> { trait RcCloned<T> {
fn cloned(&self) -> T; fn cloned(&self) -> T;
@ -89,6 +92,7 @@ trait Contains<K, T> {
trait Intersection<K> { trait Intersection<K> {
fn with_intersected_keys(&self, ks: &BTreeSet<K>) -> Self; fn with_intersected_keys(&self, ks: &BTreeSet<K>) -> Self;
fn keep_intersected_keys(&mut self, ks: &BTreeSet<K>);
} }
impl<K: Ord, T> Contains<K, T> for BTreeSet<K> { impl<K: Ord, T> Contains<K, T> for BTreeSet<K> {
@ -108,6 +112,22 @@ impl<K: Clone + Ord, V: Clone> Intersection<K> for BTreeMap<K, V> {
.filter_map(|(k, v)| ks.when_contains(k, || (k.clone(), v.clone()))) .filter_map(|(k, v)| ks.when_contains(k, || (k.clone(), v.clone())))
.collect() .collect()
} }
/// Remove all keys from the map that are not present in `ks`.
/// This implementation is terrible because there's no mutable iterator for BTreeMap.
fn keep_intersected_keys(&mut self, ks: &BTreeSet<K>) {
let mut to_remove = Vec::with_capacity(self.len() - ks.len());
{
for k in self.keys() {
if !ks.contains(k) {
to_remove.push(k.clone())
}
}
}
for k in to_remove.into_iter() {
self.remove(&k);
}
}
} }
/// A `ConjoiningClauses` (CC) is a collection of clauses that are combined with `JOIN`. /// A `ConjoiningClauses` (CC) is a collection of clauses that are combined with `JOIN`.
@ -223,6 +243,38 @@ impl ConjoiningClauses {
..Default::default() ..Default::default()
} }
} }
pub fn with_inputs<T>(in_variables: BTreeSet<Variable>, inputs: T) -> ConjoiningClauses
where T: Into<Option<QueryInputs>> {
ConjoiningClauses::with_inputs_and_alias_counter(in_variables, inputs, RcCounter::new())
}
pub fn with_inputs_and_alias_counter<T>(in_variables: BTreeSet<Variable>,
inputs: T,
alias_counter: RcCounter) -> ConjoiningClauses
where T: Into<Option<QueryInputs>> {
match inputs.into() {
None => ConjoiningClauses::with_alias_counter(alias_counter),
Some(QueryInputs { mut types, mut values }) => {
// Discard any bindings not mentioned in our :in clause.
types.keep_intersected_keys(&in_variables);
values.keep_intersected_keys(&in_variables);
let mut cc = ConjoiningClauses {
alias_counter: alias_counter,
input_variables: in_variables,
value_bindings: values,
..Default::default()
};
// Pre-fill our type mappings with the types of the input bindings.
cc.known_types
.extend(types.iter()
.map(|(k, v)| (k.clone(), unit_type_set(*v))));
cc
},
}
}
} }
/// Cloning. /// Cloning.
@ -254,28 +306,16 @@ impl ConjoiningClauses {
} }
} }
impl ConjoiningClauses {
#[allow(dead_code)]
fn with_value_bindings(bindings: BTreeMap<Variable, TypedValue>) -> ConjoiningClauses {
let mut cc = ConjoiningClauses {
value_bindings: bindings,
..Default::default()
};
// Pre-fill our type mappings with the types of the input bindings.
cc.known_types
.extend(cc.value_bindings
.iter()
.map(|(k, v)| (k.clone(), unit_type_set(v.value_type()))));
cc
}
}
impl ConjoiningClauses { impl ConjoiningClauses {
pub fn bound_value(&self, var: &Variable) -> Option<TypedValue> { pub fn bound_value(&self, var: &Variable) -> Option<TypedValue> {
self.value_bindings.get(var).cloned() self.value_bindings.get(var).cloned()
} }
/// Return a set of the variables externally bound to values.
pub fn value_bound_variables(&self) -> BTreeSet<Variable> {
self.value_bindings.keys().cloned().collect()
}
/// Return a single `ValueType` if the given variable is known to have a precise type. /// Return a single `ValueType` if the given variable is known to have a precise type.
/// Returns `None` if the type of the variable is unknown. /// Returns `None` if the type of the variable is unknown.
/// Returns `None` if the type of the variable is known but not precise -- "double /// Returns `None` if the type of the variable is known but not precise -- "double

View file

@ -270,6 +270,7 @@ mod testing {
use super::*; use super::*;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::rc::Rc; use std::rc::Rc;
use mentat_core::attribute::Unique; use mentat_core::attribute::Unique;
@ -288,6 +289,7 @@ mod testing {
}; };
use clauses::{ use clauses::{
QueryInputs,
add_attribute, add_attribute,
associate_ident, associate_ident,
ident, ident,
@ -660,7 +662,9 @@ mod testing {
let b: BTreeMap<Variable, TypedValue> = let b: BTreeMap<Variable, TypedValue> =
vec![(y.clone(), TypedValue::Boolean(true))].into_iter().collect(); vec![(y.clone(), TypedValue::Boolean(true))].into_iter().collect();
let mut cc = ConjoiningClauses::with_value_bindings(b); let inputs = QueryInputs::with_values(b);
let variables: BTreeSet<Variable> = vec![Variable::from_valid_name("?y")].into_iter().collect();
let mut cc = ConjoiningClauses::with_inputs(variables, inputs);
cc.apply_pattern(&schema, Pattern { cc.apply_pattern(&schema, Pattern {
source: None, source: None,
@ -705,7 +709,9 @@ mod testing {
let b: BTreeMap<Variable, TypedValue> = let b: BTreeMap<Variable, TypedValue> =
vec![(y.clone(), TypedValue::Long(42))].into_iter().collect(); vec![(y.clone(), TypedValue::Long(42))].into_iter().collect();
let mut cc = ConjoiningClauses::with_value_bindings(b); let inputs = QueryInputs::with_values(b);
let variables: BTreeSet<Variable> = vec![Variable::from_valid_name("?y")].into_iter().collect();
let mut cc = ConjoiningClauses::with_inputs(variables, inputs);
cc.apply_pattern(&schema, Pattern { cc.apply_pattern(&schema, Pattern {
source: None, source: None,
@ -737,7 +743,9 @@ mod testing {
let b: BTreeMap<Variable, TypedValue> = let b: BTreeMap<Variable, TypedValue> =
vec![(y.clone(), TypedValue::Long(42))].into_iter().collect(); vec![(y.clone(), TypedValue::Long(42))].into_iter().collect();
let mut cc = ConjoiningClauses::with_value_bindings(b); let inputs = QueryInputs::with_values(b);
let variables: BTreeSet<Variable> = vec![Variable::from_valid_name("?y")].into_iter().collect();
let mut cc = ConjoiningClauses::with_inputs(variables, inputs);
cc.apply_pattern(&schema, Pattern { cc.apply_pattern(&schema, Pattern {
source: None, source: None,

View file

@ -10,6 +10,8 @@
extern crate mentat_query; extern crate mentat_query;
use mentat_core::ValueType;
use self::mentat_query::{ use self::mentat_query::{
PlainSymbol, PlainSymbol,
}; };
@ -20,6 +22,11 @@ error_chain! {
} }
errors { errors {
InputTypeDisagreement(var: PlainSymbol, declared: ValueType, provided: ValueType) {
description("input type disagreement")
display("value of type {} provided for var {}, expected {}", provided, var, declared)
}
UnknownFunction(name: PlainSymbol) { UnknownFunction(name: PlainSymbol) {
description("no such function") description("no such function")
display("no function named {}", name) display("no function named {}", name)

View file

@ -15,6 +15,7 @@ extern crate mentat_core;
extern crate mentat_query; extern crate mentat_query;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::ops::Sub;
mod errors; mod errors;
mod types; mod types;
@ -41,6 +42,10 @@ pub use errors::{
Result, Result,
}; };
pub use clauses::{
QueryInputs,
};
#[allow(dead_code)] #[allow(dead_code)]
pub struct AlgebraicQuery { pub struct AlgebraicQuery {
default_source: SrcVar, default_source: SrcVar,
@ -74,16 +79,20 @@ impl AlgebraicQuery {
pub fn is_known_empty(&self) -> bool { pub fn is_known_empty(&self) -> bool {
self.cc.is_known_empty() self.cc.is_known_empty()
} }
/// Return a set of the input variables mentioned in the `:in` clause that have not yet been
/// bound. We do this by looking at the CC.
pub fn unbound_variables(&self) -> BTreeSet<Variable> {
self.cc.input_variables.sub(&self.cc.value_bound_variables())
}
} }
pub fn algebrize_with_counter(schema: &Schema, parsed: FindQuery, counter: usize) -> Result<AlgebraicQuery> { pub fn algebrize_with_counter(schema: &Schema, parsed: FindQuery, counter: usize) -> Result<AlgebraicQuery> {
let alias_counter = RcCounter::with_initial(counter); algebrize_with_inputs(schema, parsed, counter, QueryInputs::default())
let cc = clauses::ConjoiningClauses::with_alias_counter(alias_counter);
algebrize_with_cc(schema, parsed, cc)
} }
pub fn algebrize(schema: &Schema, parsed: FindQuery) -> Result<AlgebraicQuery> { pub fn algebrize(schema: &Schema, parsed: FindQuery) -> Result<AlgebraicQuery> {
algebrize_with_cc(schema, parsed, clauses::ConjoiningClauses::default()) algebrize_with_inputs(schema, parsed, 0, QueryInputs::default())
} }
/// Take an ordering list. Any variables that aren't fixed by the query are used to produce /// Take an ordering list. Any variables that aren't fixed by the query are used to produce
@ -122,8 +131,13 @@ fn validate_and_simplify_order(cc: &ConjoiningClauses, order: Option<Vec<Order>>
} }
} }
#[allow(dead_code)] pub fn algebrize_with_inputs(schema: &Schema,
pub fn algebrize_with_cc(schema: &Schema, parsed: FindQuery, mut cc: ConjoiningClauses) -> Result<AlgebraicQuery> { parsed: FindQuery,
counter: usize,
inputs: QueryInputs) -> Result<AlgebraicQuery> {
let alias_counter = RcCounter::with_initial(counter);
let mut cc = ConjoiningClauses::with_inputs_and_alias_counter(parsed.in_vars, inputs, alias_counter);
// TODO: integrate default source into pattern processing. // TODO: integrate default source into pattern processing.
// TODO: flesh out the rest of find-into-context. // TODO: flesh out the rest of find-into-context.
let where_clauses = parsed.where_clauses; let where_clauses = parsed.where_clauses;

View file

@ -5,7 +5,7 @@ workspace = ".."
[dependencies] [dependencies]
combine = "2.2.2" combine = "2.2.2"
error-chain = "0.9.0" error-chain = "0.8.1"
matches = "0.1" matches = "0.1"
[dependencies.edn] [dependencies.edn]

View file

@ -15,6 +15,8 @@ extern crate mentat_query;
use std; // To refer to std::result::Result. use std; // To refer to std::result::Result.
use std::collections::BTreeSet;
use self::combine::{eof, many, many1, optional, parser, satisfy, satisfy_map, Parser, ParseResult, Stream}; use self::combine::{eof, many, many1, optional, parser, satisfy, satisfy_map, Parser, ParseResult, Stream};
use self::combine::combinator::{choice, or, try}; use self::combine::combinator::{choice, or, try};
@ -65,6 +67,11 @@ error_chain! {
} }
errors { errors {
DuplicateVariableError {
description("duplicate variable")
display("duplicates in variable list")
}
NotAVariableError(value: edn::ValueAndSpan) { NotAVariableError(value: edn::ValueAndSpan) {
description("not a variable") description("not a variable")
display("not a variable: '{}'", value) display("not a variable: '{}'", value)
@ -328,6 +335,8 @@ def_parser!(Find, spec, FindSpec, {
def_matches_keyword!(Find, literal_find, "find"); def_matches_keyword!(Find, literal_find, "find");
def_matches_keyword!(Find, literal_in, "in");
def_matches_keyword!(Find, literal_with, "with"); def_matches_keyword!(Find, literal_with, "with");
def_matches_keyword!(Find, literal_where, "where"); def_matches_keyword!(Find, literal_where, "where");
@ -337,11 +346,26 @@ def_matches_keyword!(Find, literal_order, "order");
/// Express something close to a builder pattern for a `FindQuery`. /// Express something close to a builder pattern for a `FindQuery`.
enum FindQueryPart { enum FindQueryPart {
FindSpec(FindSpec), FindSpec(FindSpec),
With(Vec<Variable>), With(BTreeSet<Variable>),
In(BTreeSet<Variable>),
WhereClauses(Vec<WhereClause>), WhereClauses(Vec<WhereClause>),
Order(Vec<Order>), Order(Vec<Order>),
} }
def_parser!(Find, vars, BTreeSet<Variable>, {
vector().of_exactly(many(Query::variable()).and_then(|vars: Vec<Variable>| {
let given = vars.len();
let set: BTreeSet<Variable> = vars.into_iter().collect();
if given != set.len() {
// TODO: find out what the variable is!
let e = Box::new(Error::from_kind(ErrorKind::DuplicateVariableError));
Err(combine::primitives::Error::Other(e))
} else {
Ok(set)
}
}))
});
/// This is awkward, but will do for now. We use `keyword_map()` to optionally accept vector find /// This is awkward, but will do for now. We use `keyword_map()` to optionally accept vector find
/// queries, then we use `FindQueryPart` to collect parts that have heterogeneous types; and then we /// queries, then we use `FindQueryPart` to collect parts that have heterogeneous types; and then we
/// construct a `FindQuery` from them. /// construct a `FindQuery` from them.
@ -349,8 +373,9 @@ def_parser!(Find, query, FindQuery, {
let p_find_spec = Find::literal_find() let p_find_spec = Find::literal_find()
.with(vector().of_exactly(Find::spec().map(FindQueryPart::FindSpec))); .with(vector().of_exactly(Find::spec().map(FindQueryPart::FindSpec)));
let p_with_vars = Find::literal_with() let p_in_vars = Find::literal_in().with(Find::vars().map(FindQueryPart::In));
.with(vector().of_exactly(many(Query::variable()).map(FindQueryPart::With)));
let p_with_vars = Find::literal_with().with(Find::vars().map(FindQueryPart::With));
let p_where_clauses = Find::literal_where() let p_where_clauses = Find::literal_where()
.with(vector().of_exactly(Where::clauses().map(FindQueryPart::WhereClauses))).expected(":where clauses"); .with(vector().of_exactly(Where::clauses().map(FindQueryPart::WhereClauses))).expected(":where clauses");
@ -359,14 +384,16 @@ def_parser!(Find, query, FindQuery, {
.with(vector().of_exactly(many1(Query::order()).map(FindQueryPart::Order))); .with(vector().of_exactly(many1(Query::order()).map(FindQueryPart::Order)));
(or(map(), keyword_map())) (or(map(), keyword_map()))
.of_exactly(many(choice::<[&mut Parser<Input = ValueStream, Output = FindQueryPart>; 4], _>([ .of_exactly(many(choice::<[&mut Parser<Input = ValueStream, Output = FindQueryPart>; 5], _>([
&mut try(p_find_spec), &mut try(p_find_spec),
&mut try(p_in_vars),
&mut try(p_with_vars), &mut try(p_with_vars),
&mut try(p_where_clauses), &mut try(p_where_clauses),
&mut try(p_order_clauses), &mut try(p_order_clauses),
]))) ])))
.and_then(|parts: Vec<FindQueryPart>| -> std::result::Result<FindQuery, combine::primitives::Error<edn::ValueAndSpan, edn::ValueAndSpan>> { .and_then(|parts: Vec<FindQueryPart>| -> std::result::Result<FindQuery, combine::primitives::Error<edn::ValueAndSpan, edn::ValueAndSpan>> {
let mut find_spec = None; let mut find_spec = None;
let mut in_vars = None;
let mut with_vars = None; let mut with_vars = None;
let mut where_clauses = None; let mut where_clauses = None;
let mut order_clauses = None; let mut order_clauses = None;
@ -375,6 +402,7 @@ def_parser!(Find, query, FindQuery, {
match part { match part {
FindQueryPart::FindSpec(x) => find_spec = Some(x), FindQueryPart::FindSpec(x) => find_spec = Some(x),
FindQueryPart::With(x) => with_vars = Some(x), FindQueryPart::With(x) => with_vars = Some(x),
FindQueryPart::In(x) => in_vars = Some(x),
FindQueryPart::WhereClauses(x) => where_clauses = Some(x), FindQueryPart::WhereClauses(x) => where_clauses = Some(x),
FindQueryPart::Order(x) => order_clauses = Some(x), FindQueryPart::Order(x) => order_clauses = Some(x),
} }
@ -383,9 +411,9 @@ def_parser!(Find, query, FindQuery, {
Ok(FindQuery { Ok(FindQuery {
find_spec: find_spec.clone().ok_or(combine::primitives::Error::Unexpected("expected :find".into()))?, find_spec: find_spec.clone().ok_or(combine::primitives::Error::Unexpected("expected :find".into()))?,
default_source: SrcVar::DefaultSrc, default_source: SrcVar::DefaultSrc,
with: with_vars.unwrap_or(vec![]), with: with_vars.unwrap_or(BTreeSet::default()),
in_vars: vec![], // TODO in_vars: in_vars.unwrap_or(BTreeSet::default()),
in_sources: vec![], // TODO in_sources: BTreeSet::default(), // TODO
order: order_clauses, order: order_clauses,
where_clauses: where_clauses.ok_or(combine::primitives::Error::Unexpected("expected :where".into()))?, where_clauses: where_clauses.ok_or(combine::primitives::Error::Unexpected("expected :where".into()))?,
}) })
@ -521,6 +549,33 @@ mod test {
vec![variable(e.clone())]); vec![variable(e.clone())]);
} }
#[test]
fn test_repeated_vars() {
let e = edn::PlainSymbol::new("?e");
let f = edn::PlainSymbol::new("?f");
let input = edn::Value::Vector(vec![edn::Value::PlainSymbol(e.clone()),
edn::Value::PlainSymbol(f.clone()),]);
assert_parses_to!(Find::vars, input,
vec![variable(e.clone()), variable(f.clone())].into_iter().collect());
let g = edn::PlainSymbol::new("?g");
let input = edn::Value::Vector(vec![edn::Value::PlainSymbol(g.clone()),
edn::Value::PlainSymbol(g.clone()),]);
let mut par = Find::vars();
let result = par.parse(input.with_spans().into_atom_stream())
.map(|x| x.0)
.map_err(|e| if let Some(combine::primitives::Error::Other(x)) = e.errors.into_iter().next() {
// Pattern matching on boxes is rocket science until Rust Nightly features hit
// stable. ErrorKind isn't Clone, so convert to strings. We could pattern match
// for exact comparison here.
x.downcast::<Error>().ok().map(|e| e.to_string())
} else {
None
});
assert_eq!(result, Err(Some("duplicates in variable list".to_string())));
}
#[test] #[test]
fn test_or() { fn test_or() {
let oj = edn::PlainSymbol::new("or"); let oj = edn::PlainSymbol::new("or");

View file

@ -4,7 +4,7 @@ version = "0.0.1"
workspace = ".." workspace = ".."
[dependencies] [dependencies]
error-chain = "0.9.0" error-chain = "0.8.1"
[dependencies.rusqlite] [dependencies.rusqlite]
version = "0.10.1" version = "0.10.1"

View file

@ -14,11 +14,6 @@ use mentat_core::{
ValueType, ValueType,
}; };
use mentat_query::{
Direction,
Variable,
};
use mentat_query_algebrizer::{ use mentat_query_algebrizer::{
AlgebraicQuery, AlgebraicQuery,
ColumnAlternation, ColumnAlternation,

View file

@ -382,6 +382,7 @@ fn test_order_by() {
FROM `datoms` AS `datoms00` \ FROM `datoms` AS `datoms00` \
WHERE `datoms00`.a = 99 \ WHERE `datoms00`.a = 99 \
ORDER BY `?y` DESC"); ORDER BY `?y` DESC");
assert_eq!(args, vec![]);
// Unknown type. // Unknown type.
let input = r#"[:find ?x :with ?y :where [?x _ ?y] :order ?y ?x]"#; let input = r#"[:find ?x :with ?y :where [?x _ ?y] :order ?y ?x]"#;
@ -390,4 +391,5 @@ fn test_order_by() {
`all_datoms00`.value_type_tag AS `?y_value_type_tag` \ `all_datoms00`.value_type_tag AS `?y_value_type_tag` \
FROM `all_datoms` AS `all_datoms00` \ FROM `all_datoms` AS `all_datoms00` \
ORDER BY `?y_value_type_tag` ASC, `?y` ASC, `?x` ASC"); ORDER BY `?y_value_type_tag` ASC, `?y` ASC, `?x` ASC");
assert_eq!(args, vec![]);
} }

View file

@ -33,12 +33,19 @@
extern crate edn; extern crate edn;
extern crate mentat_core; extern crate mentat_core;
use std::collections::BTreeSet; use std::collections::{
BTreeSet,
};
use std::fmt; use std::fmt;
use std::rc::Rc; use std::rc::Rc;
use edn::{BigInt, OrderedFloat}; use edn::{BigInt, OrderedFloat};
pub use edn::{NamespacedKeyword, PlainSymbol}; pub use edn::{NamespacedKeyword, PlainSymbol};
use mentat_core::TypedValue;
use mentat_core::{
TypedValue,
};
pub type SrcVarName = String; // Do not include the required syntactic '$'. pub type SrcVarName = String; // Do not include the required syntactic '$'.
@ -138,7 +145,7 @@ pub enum Direction {
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Order(pub Direction, pub Variable); // Future: Element instead of Variable? pub struct Order(pub Direction, pub Variable); // Future: Element instead of Variable?
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SrcVar { pub enum SrcVar {
DefaultSrc, DefaultSrc,
NamedSrc(SrcVarName), NamedSrc(SrcVarName),
@ -600,9 +607,9 @@ pub enum WhereClause {
pub struct FindQuery { pub struct FindQuery {
pub find_spec: FindSpec, pub find_spec: FindSpec,
pub default_source: SrcVar, pub default_source: SrcVar,
pub with: Vec<Variable>, pub with: BTreeSet<Variable>,
pub in_vars: Vec<Variable>, pub in_vars: BTreeSet<Variable>,
pub in_sources: Vec<SrcVar>, pub in_sources: BTreeSet<SrcVar>,
pub where_clauses: Vec<WhereClause>, pub where_clauses: Vec<WhereClause>,
pub order: Option<Vec<Order>>, pub order: Option<Vec<Order>>,
// TODO: in_rules; // TODO: in_rules;

View file

@ -4,7 +4,7 @@ version = "0.0.1"
workspace = ".." workspace = ".."
[dependencies] [dependencies]
error-chain = "0.9.0" error-chain = "0.8.1"
ordered-float = "0.4.0" ordered-float = "0.4.0"
[dependencies.mentat_core] [dependencies.mentat_core]

View file

@ -10,26 +10,29 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use rusqlite; use rusqlite;
use edn; use edn;
use errors::*;
use mentat_core::{ use mentat_core::{
Schema, Schema,
TypedValue,
}; };
use mentat_db::db; use mentat_db::db;
use mentat_db::{ use mentat_db::{
transact, transact,
PartitionMap, PartitionMap,
TxReport, TxReport,
}; };
use mentat_tx_parser; use mentat_tx_parser;
use errors::*;
use query::{ use query::{
q_once, q_once,
QueryInputs,
QueryResults, QueryResults,
}; };
@ -112,7 +115,7 @@ impl Conn {
query: &str, query: &str,
inputs: T, inputs: T,
limit: U) -> Result<QueryResults> limit: U) -> Result<QueryResults>
where T: Into<Option<HashMap<String, TypedValue>>>, where T: Into<Option<QueryInputs>>,
U: Into<Option<u64>> U: Into<Option<u64>>
{ {

View file

@ -12,6 +12,8 @@
use rusqlite; use rusqlite;
use std::collections::BTreeSet;
use edn; use edn;
use mentat_db; use mentat_db;
use mentat_query_algebrizer; use mentat_query_algebrizer;
@ -40,6 +42,11 @@ error_chain! {
} }
errors { errors {
UnboundVariables(names: BTreeSet<String>) {
description("unbound variables at execution time")
display("variables {:?} unbound at execution time", names)
}
InvalidArgumentName(name: String) { InvalidArgumentName(name: String) {
description("invalid argument name") description("invalid argument name")
display("invalid argument name: '{}'", name) display("invalid argument name: '{}'", name)

View file

@ -54,7 +54,9 @@ pub use mentat_db::{
pub use query::{ pub use query::{
NamespacedKeyword, NamespacedKeyword,
PlainSymbol, PlainSymbol,
QueryInputs,
QueryResults, QueryResults,
Variable,
q_once, q_once,
}; };

View file

@ -8,21 +8,25 @@
// 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.
use std::collections::HashMap;
use rusqlite; use rusqlite;
use rusqlite::types::ToSql; use rusqlite::types::ToSql;
use mentat_core::{ use mentat_core::{
Schema, Schema,
TypedValue,
}; };
use mentat_query_algebrizer::algebrize; use mentat_query_algebrizer::{
algebrize_with_inputs,
};
pub use mentat_query_algebrizer::{
QueryInputs,
};
pub use mentat_query::{ pub use mentat_query::{
NamespacedKeyword, NamespacedKeyword,
PlainSymbol, PlainSymbol,
Variable,
}; };
use mentat_query_parser::{ use mentat_query_parser::{
@ -41,31 +45,31 @@ pub use mentat_query_projector::{
QueryResults, QueryResults,
}; };
use errors::Result; use errors::{
ErrorKind,
Result,
};
pub type QueryExecutionResult = Result<QueryResults>; pub type QueryExecutionResult = Result<QueryResults>;
/// Take an EDN query string, a reference to a open SQLite connection, a Mentat DB, and an optional /// Take an EDN query string, a reference to an open SQLite connection, a Mentat schema, and an
/// collection of input bindings (which should be keyed by `"?varname"`), and execute the query /// optional collection of input bindings (which should be keyed by `"?varname"`), and execute the
/// immediately, blocking the current thread. /// query immediately, blocking the current thread.
/// Returns a structure that corresponds to the kind of input query, populated with `TypedValue` /// Returns a structure that corresponds to the kind of input query, populated with `TypedValue`
/// instances. /// instances.
/// The caller is responsible for ensuring that the SQLite connection is in a transaction if /// The caller is responsible for ensuring that the SQLite connection has an open transaction if
/// isolation is required. /// isolation is required.
#[allow(unused_variables)]
pub fn q_once<'sqlite, 'schema, 'query, T, U> pub fn q_once<'sqlite, 'schema, 'query, T, U>
(sqlite: &'sqlite rusqlite::Connection, (sqlite: &'sqlite rusqlite::Connection,
schema: &'schema Schema, schema: &'schema Schema,
query: &'query str, query: &'query str,
inputs: T, inputs: T,
limit: U) -> QueryExecutionResult limit: U) -> QueryExecutionResult
where T: Into<Option<HashMap<String, TypedValue>>>, where T: Into<Option<QueryInputs>>,
U: Into<Option<u64>> U: Into<Option<u64>>
{ {
// TODO: validate inputs.
let parsed = parse_find_string(query)?; let parsed = parse_find_string(query)?;
let mut algebrized = algebrize(schema, parsed)?; let mut algebrized = algebrize_with_inputs(schema, parsed, 0, inputs.into().unwrap_or(QueryInputs::default()))?;
if algebrized.is_known_empty() { if algebrized.is_known_empty() {
// We don't need to do any SQL work at all. // We don't need to do any SQL work at all.
@ -74,6 +78,13 @@ pub fn q_once<'sqlite, 'schema, 'query, T, U>
algebrized.apply_limit(limit.into()); algebrized.apply_limit(limit.into());
// Because this is q_once, we can check that all of our `:in` variables are bound at this point.
// If they aren't, the user has made an error -- perhaps writing the wrong variable in `:in`, or
// not binding in the `QueryInput`.
let unbound = algebrized.unbound_variables();
if !unbound.is_empty() {
bail!(ErrorKind::UnboundVariables(unbound.into_iter().map(|v| v.to_string()).collect()));
}
let select = query_to_select(algebrized); let select = query_to_select(algebrized);
let SQLQuery { sql, args } = select.query.to_sql_query()?; let SQLQuery { sql, args } = select.query.to_sql_query()?;

View file

@ -21,11 +21,18 @@ use mentat_core::{
use mentat::{ use mentat::{
NamespacedKeyword, NamespacedKeyword,
QueryInputs,
QueryResults, QueryResults,
Variable,
new_connection, new_connection,
q_once, q_once,
}; };
use mentat::errors::{
Error,
ErrorKind,
};
#[test] #[test]
fn test_rel() { fn test_rel() {
let mut c = new_connection("").expect("Couldn't open conn."); let mut c = new_connection("").expect("Couldn't open conn.");
@ -159,3 +166,41 @@ fn test_coll() {
println!("Coll took {}µs", start.to(end).num_microseconds().unwrap()); println!("Coll took {}µs", start.to(end).num_microseconds().unwrap());
} }
#[test]
fn test_inputs() {
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.");
// entids::DB_INSTALL_VALUE_TYPE = 5.
let ee = (Variable::from_valid_name("?e"), TypedValue::Ref(5));
let inputs = QueryInputs::with_value_sequence(vec![ee]);
let results = q_once(&c, &db.schema,
"[:find ?i . :in ?e :where [?e :db/ident ?i]]", inputs, None)
.expect("query to succeed");
if let QueryResults::Scalar(Some(TypedValue::Keyword(value))) = results {
assert_eq!(value.as_ref(), &NamespacedKeyword::new("db.install", "valueType"));
} else {
panic!("Expected scalar.");
}
}
/// Ensure that a query won't be run without all of its `:in` variables being bound.
#[test]
fn test_unbound_inputs() {
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.");
// Bind the wrong var by 'mistake'.
let xx = (Variable::from_valid_name("?x"), TypedValue::Ref(5));
let inputs = QueryInputs::with_value_sequence(vec![xx]);
let results = q_once(&c, &db.schema,
"[:find ?i . :in ?e :where [?e :db/ident ?i]]", inputs, None);
match results {
Result::Err(Error(ErrorKind::UnboundVariables(vars), _)) => {
assert_eq!(vars, vec!["?e".to_string()].into_iter().collect());
},
_ => panic!("Expected unbound variables."),
}
}

View file

@ -5,7 +5,7 @@ workspace = ".."
[dependencies] [dependencies]
combine = "2.2.2" combine = "2.2.2"
error-chain = "0.9.0" error-chain = "0.8.1"
[dependencies.edn] [dependencies.edn]
path = "../edn" path = "../edn"