Implement q_prepare
with pre-bound variables. r=rnewman
This commit is contained in:
parent
715d434945
commit
a6341f6fd6
4 changed files with 154 additions and 0 deletions
|
@ -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(),
|
||||
|
|
76
src/conn.rs
76
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<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();
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
66
src/query.rs
66
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<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,
|
||||
|
|
Loading…
Reference in a new issue