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)
|
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 {
|
pub fn with_values(values: BTreeMap<Variable, TypedValue>) -> QueryInputs {
|
||||||
QueryInputs {
|
QueryInputs {
|
||||||
types: values.iter().map(|(var, val)| (var.clone(), val.value_type())).collect(),
|
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::{
|
use query::{
|
||||||
lookup_value_for_attribute,
|
lookup_value_for_attribute,
|
||||||
lookup_values_for_attribute,
|
lookup_values_for_attribute,
|
||||||
|
PreparedResult,
|
||||||
q_once,
|
q_once,
|
||||||
|
q_prepare,
|
||||||
q_explain,
|
q_explain,
|
||||||
QueryExplanation,
|
QueryExplanation,
|
||||||
QueryInputs,
|
QueryInputs,
|
||||||
|
@ -138,6 +140,8 @@ pub trait Queryable {
|
||||||
where T: Into<Option<QueryInputs>>;
|
where T: Into<Option<QueryInputs>>;
|
||||||
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryOutput>
|
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryOutput>
|
||||||
where T: Into<Option<QueryInputs>>;
|
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>>
|
fn lookup_values_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Vec<TypedValue>>
|
||||||
where E: Into<Entid>;
|
where E: Into<Entid>;
|
||||||
fn lookup_value_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Option<TypedValue>>
|
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)
|
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>
|
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
|
||||||
where T: Into<Option<QueryInputs>> {
|
where T: Into<Option<QueryInputs>> {
|
||||||
self.0.q_explain(query, inputs)
|
self.0.q_explain(query, inputs)
|
||||||
|
@ -193,6 +202,15 @@ impl<'a, 'c> Queryable for InProgress<'a, 'c> {
|
||||||
inputs)
|
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>
|
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
|
||||||
where T: Into<Option<QueryInputs>> {
|
where T: Into<Option<QueryInputs>> {
|
||||||
q_explain(&*(self.transaction),
|
q_explain(&*(self.transaction),
|
||||||
|
@ -379,6 +397,11 @@ impl Queryable for Store {
|
||||||
self.conn.q_once(&self.sqlite, query, inputs)
|
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>
|
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
|
||||||
where T: Into<Option<QueryInputs>> {
|
where T: Into<Option<QueryInputs>> {
|
||||||
self.conn.q_explain(&self.sqlite, query, inputs)
|
self.conn.q_explain(&self.sqlite, query, inputs)
|
||||||
|
@ -445,6 +468,19 @@ impl Conn {
|
||||||
inputs)
|
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,
|
pub fn q_explain<T>(&self,
|
||||||
sqlite: &rusqlite::Connection,
|
sqlite: &rusqlite::Connection,
|
||||||
query: &str,
|
query: &str,
|
||||||
|
@ -568,6 +604,9 @@ mod tests {
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
TypedValue,
|
TypedValue,
|
||||||
};
|
};
|
||||||
|
use query::{
|
||||||
|
Variable,
|
||||||
|
};
|
||||||
|
|
||||||
use ::QueryResults;
|
use ::QueryResults;
|
||||||
|
|
||||||
|
@ -672,6 +711,43 @@ mod tests {
|
||||||
assert_eq!(tempid_offset + 3, tempid_offset_after);
|
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]
|
#[test]
|
||||||
fn test_compound_rollback() {
|
fn test_compound_rollback() {
|
||||||
let mut sqlite = db::new_connection("").unwrap();
|
let mut sqlite = db::new_connection("").unwrap();
|
||||||
|
|
|
@ -87,5 +87,10 @@ error_chain! {
|
||||||
description("missing core vocabulary")
|
description("missing core vocabulary")
|
||||||
display("missing core attribute {}", kw)
|
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,
|
parse_find_string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use mentat_query_projector::{
|
||||||
|
Projector,
|
||||||
|
};
|
||||||
|
|
||||||
use mentat_sql::{
|
use mentat_sql::{
|
||||||
SQLQuery,
|
SQLQuery,
|
||||||
};
|
};
|
||||||
|
@ -74,6 +78,34 @@ use cache::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type QueryExecutionResult = Result<QueryOutput>;
|
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 {
|
pub trait IntoResult {
|
||||||
fn into_scalar_result(self) -> Result<Option<TypedValue>>;
|
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)
|
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>
|
pub fn q_explain<'sqlite, 'schema, 'query, T>
|
||||||
(sqlite: &'sqlite rusqlite::Connection,
|
(sqlite: &'sqlite rusqlite::Connection,
|
||||||
schema: &'schema Schema,
|
schema: &'schema Schema,
|
||||||
|
|
Loading…
Reference in a new issue