diff --git a/query-algebrizer/src/clauses/inputs.rs b/query-algebrizer/src/clauses/inputs.rs index 4644fc4d..904c536c 100644 --- a/query-algebrizer/src/clauses/inputs.rs +++ b/query-algebrizer/src/clauses/inputs.rs @@ -51,6 +51,13 @@ impl QueryInputs { QueryInputs::with_values(values) } + pub fn with_type_sequence(types: Vec<(Variable, ValueType)>) -> QueryInputs { + QueryInputs { + types: types.into_iter().collect(), + values: BTreeMap::default(), + } + } + pub fn with_values(values: BTreeMap) -> QueryInputs { QueryInputs { types: values.iter().map(|(var, val)| (var.clone(), val.value_type())).collect(), diff --git a/src/conn.rs b/src/conn.rs index 2e45f5c3..5e14259d 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -65,7 +65,9 @@ use errors::*; use query::{ lookup_value_for_attribute, lookup_values_for_attribute, + PreparedResult, q_once, + q_prepare, q_explain, QueryExplanation, QueryInputs, @@ -138,6 +140,8 @@ pub trait Queryable { where T: Into>; fn q_once(&self, query: &str, inputs: T) -> Result where T: Into>; + fn q_prepare(&self, query: &str, inputs: T) -> PreparedResult + where T: Into>; fn lookup_values_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> where E: Into; fn lookup_value_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> @@ -167,6 +171,11 @@ impl<'a, 'c> Queryable for InProgressRead<'a, 'c> { self.0.q_once(query, inputs) } + fn q_prepare(&self, query: &str, inputs: T) -> PreparedResult + where T: Into> { + self.0.q_prepare(query, inputs) + } + fn q_explain(&self, query: &str, inputs: T) -> Result where T: Into> { self.0.q_explain(query, inputs) @@ -193,6 +202,15 @@ impl<'a, 'c> Queryable for InProgress<'a, 'c> { inputs) } + fn q_prepare(&self, query: &str, inputs: T) -> PreparedResult + where T: Into> { + + q_prepare(&*(self.transaction), + &self.schema, + query, + inputs) + } + fn q_explain(&self, query: &str, inputs: T) -> Result where T: Into> { q_explain(&*(self.transaction), @@ -379,6 +397,11 @@ impl Queryable for Store { self.conn.q_once(&self.sqlite, query, inputs) } + fn q_prepare(&self, query: &str, inputs: T) -> PreparedResult + where T: Into> { + self.conn.q_prepare(&self.sqlite, query, inputs) + } + fn q_explain(&self, query: &str, inputs: T) -> Result where T: Into> { self.conn.q_explain(&self.sqlite, query, inputs) @@ -445,6 +468,19 @@ impl Conn { inputs) } + pub fn q_prepare<'sqlite, 'query, T>(&self, + sqlite: &'sqlite rusqlite::Connection, + query: &'query str, + inputs: T) -> PreparedResult<'sqlite> + where T: Into> { + + let metadata = self.metadata.lock().unwrap(); + q_prepare(sqlite, + &*metadata.schema, + query, + inputs) + } + pub fn q_explain(&self, sqlite: &rusqlite::Connection, query: &str, @@ -568,6 +604,9 @@ mod tests { use mentat_core::{ TypedValue, }; + use query::{ + Variable, + }; use ::QueryResults; @@ -672,6 +711,43 @@ mod tests { assert_eq!(tempid_offset + 3, tempid_offset_after); } + #[test] + fn test_simple_prepared_query() { + let mut c = db::new_connection("").expect("Couldn't open conn."); + let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); + conn.transact(&mut c, r#"[ + [:db/add "s" :db/ident :foo/boolean] + [:db/add "s" :db/valueType :db.type/boolean] + [:db/add "s" :db/cardinality :db.cardinality/one] + ]"#).expect("successful transaction"); + + let report = conn.transact(&mut c, r#"[ + [:db/add "u" :foo/boolean true] + [:db/add "p" :foo/boolean false] + ]"#).expect("successful transaction"); + let yes = report.tempids.get("u").expect("found it").clone(); + + let vv = Variable::from_valid_name("?v"); + + let values = QueryInputs::with_value_sequence(vec![(vv, true.into())]); + + let read = conn.begin_read(&mut c).expect("read"); + + // N.B., you might choose to algebrize _without_ validating that the + // types are known. In this query we know that `?v` must be a boolean, + // and so we can kinda generate our own required input types! + let mut prepared = read.q_prepare(r#"[:find [?x ...] + :in ?v + :where [?x :foo/boolean ?v]]"#, + values).expect("prepare succeeded"); + + let yeses = prepared.run(None).expect("result"); + assert_eq!(yeses.results, QueryResults::Coll(vec![TypedValue::Ref(yes)])); + + let yeses_again = prepared.run(None).expect("result"); + assert_eq!(yeses_again.results, QueryResults::Coll(vec![TypedValue::Ref(yes)])); + } + #[test] fn test_compound_rollback() { let mut sqlite = db::new_connection("").unwrap(); diff --git a/src/errors.rs b/src/errors.rs index fc8902cb..99b16643 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -87,5 +87,10 @@ error_chain! { description("missing core vocabulary") display("missing core attribute {}", kw) } + + PreparedQuerySchemaMismatch { + description("schema changed since query was prepared") + display("schema changed since query was prepared") + } } } diff --git a/src/query.rs b/src/query.rs index c804af27..45870939 100644 --- a/src/query.rs +++ b/src/query.rs @@ -51,6 +51,10 @@ use mentat_query_parser::{ parse_find_string, }; +use mentat_query_projector::{ + Projector, +}; + use mentat_sql::{ SQLQuery, }; @@ -74,6 +78,34 @@ use cache::{ }; pub type QueryExecutionResult = Result; +pub type PreparedResult<'sqlite> = Result>; + +pub enum PreparedQuery<'sqlite> { + Empty { + find_spec: Rc, + }, + Bound { + statement: rusqlite::Statement<'sqlite>, + args: Vec<(String, Rc)>, + projector: Box, + }, +} + +impl<'sqlite> PreparedQuery<'sqlite> { + pub fn run(&mut self, _inputs: T) -> QueryExecutionResult where T: Into> { + match self { + &mut PreparedQuery::Empty { ref find_spec } => { + Ok(QueryOutput::empty(find_spec)) + }, + &mut PreparedQuery::Bound { ref mut statement, ref args, ref projector } => { + let rows = run_statement(statement, args)?; + projector + .project(rows) + .map_err(|e| e.into()) + } + } + } +} pub trait IntoResult { fn into_scalar_result(self) -> Result>; @@ -309,6 +341,40 @@ pub fn q_once<'sqlite, 'schema, 'query, T> run_algebrized_query(sqlite, algebrized) } +pub fn q_prepare<'sqlite, 'schema, 'query, T> +(sqlite: &'sqlite rusqlite::Connection, + schema: &'schema Schema, + query: &'query str, + inputs: T) -> PreparedResult<'sqlite> + where T: Into> +{ + let algebrized = algebrize_query_str(schema, query, inputs)?; + + let unbound = algebrized.unbound_variables(); + if !unbound.is_empty() { + // TODO: Allow binding variables at execution time, not just + // preparation time. + bail!(ErrorKind::UnboundVariables(unbound.into_iter().map(|v| v.to_string()).collect())); + } + + if algebrized.is_known_empty() { + // We don't need to do any SQL work at all. + return Ok(PreparedQuery::Empty { + find_spec: algebrized.find_spec, + }); + } + + let select = query_to_select(algebrized)?; + let SQLQuery { sql, args } = select.query.to_sql_query()?; + let statement = sqlite.prepare(sql.as_str())?; + + Ok(PreparedQuery::Bound { + statement, + args, + projector: select.projector + }) +} + pub fn q_explain<'sqlite, 'schema, 'query, T> (sqlite: &'sqlite rusqlite::Connection, schema: &'schema Schema,