Implement q_prepare with pre-bound variables. r=rnewman

This commit is contained in:
Kit Cambridge 2018-01-23 19:02:25 -08:00 committed by Kit Cambridge
parent 715d434945
commit a6341f6fd6
4 changed files with 154 additions and 0 deletions

View file

@ -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<Variable, TypedValue>) -> QueryInputs {
QueryInputs {
types: values.iter().map(|(var, val)| (var.clone(), val.value_type())).collect(),

View file

@ -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<Option<QueryInputs>>;
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryOutput>
where T: Into<Option<QueryInputs>>;
fn q_prepare<T>(&self, query: &str, inputs: T) -> PreparedResult
where T: Into<Option<QueryInputs>>;
fn lookup_values_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Vec<TypedValue>>
where E: Into<Entid>;
fn lookup_value_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Option<TypedValue>>
@ -167,6 +171,11 @@ impl<'a, 'c> Queryable for InProgressRead<'a, 'c> {
self.0.q_once(query, inputs)
}
fn q_prepare<T>(&self, query: &str, inputs: T) -> PreparedResult
where T: Into<Option<QueryInputs>> {
self.0.q_prepare(query, inputs)
}
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
where T: Into<Option<QueryInputs>> {
self.0.q_explain(query, inputs)
@ -193,6 +202,15 @@ impl<'a, 'c> Queryable for InProgress<'a, 'c> {
inputs)
}
fn q_prepare<T>(&self, query: &str, inputs: T) -> PreparedResult
where T: Into<Option<QueryInputs>> {
q_prepare(&*(self.transaction),
&self.schema,
query,
inputs)
}
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
where T: Into<Option<QueryInputs>> {
q_explain(&*(self.transaction),
@ -379,6 +397,11 @@ impl Queryable for Store {
self.conn.q_once(&self.sqlite, query, inputs)
}
fn q_prepare<T>(&self, query: &str, inputs: T) -> PreparedResult
where T: Into<Option<QueryInputs>> {
self.conn.q_prepare(&self.sqlite, query, inputs)
}
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
where T: Into<Option<QueryInputs>> {
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<Option<QueryInputs>> {
let metadata = self.metadata.lock().unwrap();
q_prepare(sqlite,
&*metadata.schema,
query,
inputs)
}
pub fn q_explain<T>(&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();

View file

@ -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")
}
}
}

View file

@ -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<QueryOutput>;
pub type PreparedResult<'sqlite> = Result<PreparedQuery<'sqlite>>;
pub enum PreparedQuery<'sqlite> {
Empty {
find_spec: Rc<FindSpec>,
},
Bound {
statement: rusqlite::Statement<'sqlite>,
args: Vec<(String, Rc<rusqlite::types::Value>)>,
projector: Box<Projector>,
},
}
impl<'sqlite> PreparedQuery<'sqlite> {
pub fn run<T>(&mut self, _inputs: T) -> QueryExecutionResult where T: Into<Option<QueryInputs>> {
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<Option<TypedValue>>;
@ -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<Option<QueryInputs>>
{
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,