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:
parent
dfc846e483
commit
60c082b61e
10 changed files with 159 additions and 48 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 '$'.
|
||||
|
@ -743,4 +741,4 @@ impl ContainsVariables for Pattern {
|
|||
acc_ref(acc, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>>
|
||||
{
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -54,7 +54,9 @@ pub use mentat_db::{
|
|||
pub use query::{
|
||||
NamespacedKeyword,
|
||||
PlainSymbol,
|
||||
QueryInputs,
|
||||
QueryResults,
|
||||
Variable,
|
||||
q_once,
|
||||
};
|
||||
|
||||
|
|
38
src/query.rs
38
src/query.rs
|
@ -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()?;
|
||||
|
||||
|
|
|
@ -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."),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue