Part 4: pass inputs through algebrizing and execution. (#418)

This also adds a test that an `UnboundVariables` error is raised if a
variable mentioned in the `:in` clause isn't bound.
This commit is contained in:
Richard Newman 2017-04-17 13:14:30 -07:00
parent dfc846e483
commit 60c082b61e
10 changed files with 159 additions and 48 deletions

View file

@ -59,6 +59,7 @@ use types::{
TableAlias,
};
mod inputs;
mod or;
mod pattern;
mod predicate;
@ -66,6 +67,8 @@ mod resolve;
use validate::validate_or_join;
pub use self::inputs::QueryInputs;
// We do this a lot for errors.
trait RcCloned<T> {
fn cloned(&self) -> T;
@ -240,6 +243,38 @@ impl ConjoiningClauses {
..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.
@ -271,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 {
pub fn bound_value(&self, var: &Variable) -> Option<TypedValue> {
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.
/// Returns `None` if the type of the variable is unknown.
/// 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 std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::rc::Rc;
use mentat_core::attribute::Unique;
@ -288,6 +289,7 @@ mod testing {
};
use clauses::{
QueryInputs,
add_attribute,
associate_ident,
ident,
@ -660,7 +662,9 @@ mod testing {
let b: BTreeMap<Variable, TypedValue> =
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 {
source: None,
@ -705,7 +709,9 @@ mod testing {
let b: BTreeMap<Variable, TypedValue> =
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 {
source: None,
@ -737,7 +743,9 @@ mod testing {
let b: BTreeMap<Variable, TypedValue> =
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 {
source: None,

View file

@ -10,6 +10,8 @@
extern crate mentat_query;
use mentat_core::ValueType;
use self::mentat_query::{
PlainSymbol,
};
@ -20,6 +22,11 @@ error_chain! {
}
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) {
description("no such function")
display("no function named {}", name)

View file

@ -15,6 +15,7 @@ extern crate mentat_core;
extern crate mentat_query;
use std::collections::BTreeSet;
use std::ops::Sub;
mod errors;
mod types;
@ -41,6 +42,10 @@ pub use errors::{
Result,
};
pub use clauses::{
QueryInputs,
};
#[allow(dead_code)]
pub struct AlgebraicQuery {
default_source: SrcVar,
@ -74,16 +79,20 @@ impl AlgebraicQuery {
pub fn is_known_empty(&self) -> bool {
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> {
let alias_counter = RcCounter::with_initial(counter);
let cc = clauses::ConjoiningClauses::with_alias_counter(alias_counter);
algebrize_with_cc(schema, parsed, cc)
algebrize_with_inputs(schema, parsed, counter, QueryInputs::default())
}
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
@ -122,8 +131,13 @@ fn validate_and_simplify_order(cc: &ConjoiningClauses, order: Option<Vec<Order>>
}
}
#[allow(dead_code)]
pub fn algebrize_with_cc(schema: &Schema, parsed: FindQuery, mut cc: ConjoiningClauses) -> Result<AlgebraicQuery> {
pub fn algebrize_with_inputs(schema: &Schema,
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: flesh out the rest of find-into-context.
let where_clauses = parsed.where_clauses;

View file

@ -34,7 +34,6 @@ extern crate edn;
extern crate mentat_core;
use std::collections::{
BTreeMap,
BTreeSet,
};
@ -46,7 +45,6 @@ pub use edn::{NamespacedKeyword, PlainSymbol};
use mentat_core::{
TypedValue,
ValueType,
};
pub type SrcVarName = String; // Do not include the required syntactic '$'.

View file

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

View file

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

View file

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

View file

@ -8,17 +8,20 @@
// 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::HashMap;
use rusqlite;
use rusqlite::types::ToSql;
use mentat_core::{
Schema,
TypedValue,
};
use mentat_query_algebrizer::algebrize;
use mentat_query_algebrizer::{
algebrize_with_inputs,
};
pub use mentat_query_algebrizer::{
QueryInputs,
};
pub use mentat_query::{
NamespacedKeyword,
@ -42,31 +45,31 @@ pub use mentat_query_projector::{
QueryResults,
};
use errors::Result;
use errors::{
ErrorKind,
Result,
};
pub type QueryExecutionResult = Result<QueryResults>;
/// Take an EDN query string, a reference to a open SQLite connection, a Mentat DB, and an optional
/// collection of input bindings (which should be keyed by `"?varname"`), and execute the query
/// immediately, blocking the current thread.
/// Take an EDN query string, a reference to an open SQLite connection, a Mentat schema, and an
/// optional collection of input bindings (which should be keyed by `"?varname"`), and execute the
/// query immediately, blocking the current thread.
/// Returns a structure that corresponds to the kind of input query, populated with `TypedValue`
/// 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.
#[allow(unused_variables)]
pub fn q_once<'sqlite, 'schema, 'query, T, U>
(sqlite: &'sqlite rusqlite::Connection,
schema: &'schema Schema,
query: &'query str,
inputs: T,
limit: U) -> QueryExecutionResult
where T: Into<Option<HashMap<Variable, TypedValue>>>,
where T: Into<Option<QueryInputs>>,
U: Into<Option<u64>>
{
// TODO: validate inputs.
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() {
// We don't need to do any SQL work at all.
@ -75,6 +78,13 @@ pub fn q_once<'sqlite, 'schema, 'query, T, U>
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 SQLQuery { sql, args } = select.query.to_sql_query()?;

View file

@ -21,11 +21,18 @@ use mentat_core::{
use mentat::{
NamespacedKeyword,
QueryInputs,
QueryResults,
Variable,
new_connection,
q_once,
};
use mentat::errors::{
Error,
ErrorKind,
};
#[test]
fn test_rel() {
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());
}
#[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."),
}
}