From e8ec59e464879580079323fcf481c074fa669456 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Mon, 11 Dec 2017 11:08:10 -0800 Subject: [PATCH] Implement a simple direct lookup API. Fixes #111 (#503) r=grisha * Add some helpers and refactor how queries are run (once). * Implement lookup_value_for_attribute. * Add a multi-value test for lookup_value_for_attribute. --- query/src/lib.rs | 21 +++++++++ src/conn.rs | 21 +++++++++ src/errors.rs | 6 +++ src/lib.rs | 2 + src/query.rs | 118 +++++++++++++++++++++++++++++++++++++++++------ tests/query.rs | 45 ++++++++++++++++++ 6 files changed, 198 insertions(+), 15 deletions(-) diff --git a/query/src/lib.rs b/query/src/lib.rs index 1dc7168b..7271d81e 100644 --- a/query/src/lib.rs +++ b/query/src/lib.rs @@ -646,6 +646,12 @@ pub struct Pattern { } impl Pattern { + pub fn simple(e: PatternNonValuePlace, + a: PatternNonValuePlace, + v: PatternValuePlace) -> Option { + Pattern::new(None, e, a, v, PatternNonValuePlace::Placeholder) + } + pub fn new(src: Option, e: PatternNonValuePlace, a: PatternNonValuePlace, @@ -788,6 +794,21 @@ pub struct FindQuery { // TODO: in_rules; } +impl FindQuery { + pub fn simple(spec: FindSpec, where_clauses: Vec) -> FindQuery { + FindQuery { + find_spec: spec, + default_source: SrcVar::DefaultSrc, + with: BTreeSet::default(), + in_vars: BTreeSet::default(), + in_sources: BTreeSet::default(), + limit: Limit::None, + where_clauses: where_clauses, + order: None, + } + } +} + impl OrJoin { pub fn new(unify_vars: UnifyVars, clauses: Vec) -> OrJoin { OrJoin { diff --git a/src/conn.rs b/src/conn.rs index 3cdccf29..00567be7 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -20,7 +20,9 @@ use rusqlite::{ use edn; use mentat_core::{ + Entid, Schema, + TypedValue, }; use mentat_db::db; @@ -36,6 +38,7 @@ use mentat_tx_parser; use errors::*; use query::{ + lookup_value_for_attribute, q_once, QueryInputs, QueryResults, @@ -120,6 +123,12 @@ impl<'a, 'c> InProgress<'a, 'c> { inputs) } + pub fn lookup_value_for_attribute(&self, + entity: Entid, + attribute: &edn::NamespacedKeyword) -> Result> { + lookup_value_for_attribute(&*(self.transaction), &self.schema, entity, attribute) + } + pub fn transact(self, transaction: &str) -> Result> { let assertion_vector = edn::parse::value(transaction)?; let entities = mentat_tx_parser::Tx::parse(&assertion_vector)?; @@ -205,6 +214,13 @@ impl Conn { inputs) } + pub fn lookup_value_for_attribute(&self, + sqlite: &rusqlite::Connection, + entity: Entid, + attribute: &edn::NamespacedKeyword) -> Result> { + lookup_value_for_attribute(sqlite, &*self.current_schema(), entity, attribute) + } + /// Take a SQLite transaction. /// IMMEDIATE means 'start the transaction now, but don't exclude readers'. It prevents other /// connections from taking immediate or exclusive transactions. This is appropriate for our @@ -398,6 +414,11 @@ mod tests { assert_eq!(during, QueryResults::Scalar(Some(TypedValue::Ref(one)))); + // And we can do direct lookup, too. + let kw = in_progress.lookup_value_for_attribute(one, &edn::NamespacedKeyword::new("db", "ident")) + .expect("lookup succeeded"); + assert_eq!(kw, Some(TypedValue::Keyword(edn::NamespacedKeyword::new("a", "keyword1").into()))); + in_progress.rollback() .expect("rollback succeeded"); } diff --git a/src/errors.rs b/src/errors.rs index 17c9f35a..72023e8e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -16,6 +16,7 @@ use std::collections::BTreeSet; use edn; use mentat_db; +use mentat_query; use mentat_query_algebrizer; use mentat_query_parser; use mentat_query_projector; @@ -53,5 +54,10 @@ error_chain! { description("invalid argument name") display("invalid argument name: '{}'", name) } + + UnknownAttribute(kw: mentat_query::NamespacedKeyword) { + description("unknown attribute") + display("unknown attribute: '{}'", kw) + } } } diff --git a/src/lib.rs b/src/lib.rs index 0fcf3675..bb8f5b9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,8 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. +#![recursion_limit="128"] + #[macro_use] extern crate error_chain; diff --git a/src/query.rs b/src/query.rs index 256cbfea..0af18af6 100644 --- a/src/query.rs +++ b/src/query.rs @@ -12,11 +12,13 @@ use rusqlite; use rusqlite::types::ToSql; use mentat_core::{ + Entid, Schema, TypedValue, }; use mentat_query_algebrizer::{ + AlgebraicQuery, algebrize_with_inputs, }; @@ -30,6 +32,16 @@ pub use mentat_query::{ Variable, }; +use mentat_query::{ + Element, + FindQuery, + FindSpec, + Pattern, + PatternNonValuePlace, + PatternValuePlace, + WhereClause, +}; + use mentat_query_parser::{ parse_find_string, }; @@ -78,35 +90,91 @@ impl IntoResult for QueryExecutionResult { } } -/// 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 has an open transaction if -/// isolation is required. -pub fn q_once<'sqlite, 'schema, 'query, T> +fn fetch_values<'sqlite, 'schema> (sqlite: &'sqlite rusqlite::Connection, schema: &'schema Schema, - query: &'query str, - inputs: T) -> QueryExecutionResult - where T: Into> -{ - let parsed = parse_find_string(query)?; - let algebrized = algebrize_with_inputs(schema, parsed, 0, inputs.into().unwrap_or(QueryInputs::default()))?; + entity: Entid, + attribute: Entid, + only_one: bool) -> QueryExecutionResult { + let v = Variable::from_valid_name("?v"); + // This should never fail. + // TODO: it should be possible to algebrize with variable entity and attribute, + // particularly with known type, allowing the use of prepared statements. + let pattern = Pattern::simple(PatternNonValuePlace::Entid(entity), + PatternNonValuePlace::Entid(attribute), + PatternValuePlace::Variable(v.clone())) + .unwrap(); + + let element = Element::Variable(v); + let spec = if only_one { FindSpec::FindScalar(element) } else { FindSpec::FindColl(element) }; + let query = FindQuery::simple(spec, + vec![WhereClause::Pattern(pattern)]); + + let algebrized = algebrize_with_inputs(schema, query, 0, QueryInputs::default())?; + + run_algebrized_query(sqlite, algebrized) +} + +fn lookup_attribute(schema: &Schema, attribute: &NamespacedKeyword) -> Result { + schema.get_entid(attribute) + .ok_or_else(|| ErrorKind::UnknownAttribute(attribute.clone()).into()) +} + +/// Return a single value for the provided entity and attribute. +/// If the attribute is multi-valued, an arbitrary value is returned. +/// If no value is present for that entity, `None` is returned. +/// If `attribute` isn't an attribute, `None` is returned. +pub fn lookup_value<'sqlite, 'schema> +(sqlite: &'sqlite rusqlite::Connection, + schema: &'schema Schema, + entity: Entid, + attribute: Entid) -> Result> { + fetch_values(sqlite, schema, entity, attribute, true).into_scalar_result() +} + +pub fn lookup_values<'sqlite, 'schema> +(sqlite: &'sqlite rusqlite::Connection, + schema: &'schema Schema, + entity: Entid, + attribute: Entid) -> Result> { + fetch_values(sqlite, schema, entity, attribute, false).into_coll_result() +} + +/// Return a single value for the provided entity and attribute. +/// If the attribute is multi-valued, an arbitrary value is returned. +/// If no value is present for that entity, `None` is returned. +/// If `attribute` doesn't name an attribute, an error is returned. +pub fn lookup_value_for_attribute<'sqlite, 'schema, 'attribute> +(sqlite: &'sqlite rusqlite::Connection, + schema: &'schema Schema, + entity: Entid, + attribute: &'attribute NamespacedKeyword) -> Result> { + lookup_value(sqlite, schema, entity, lookup_attribute(schema, attribute)?) +} + +pub fn lookup_values_for_attribute<'sqlite, 'schema, 'attribute> +(sqlite: &'sqlite rusqlite::Connection, + schema: &'schema Schema, + entity: Entid, + attribute: &'attribute NamespacedKeyword) -> Result> { + lookup_values(sqlite, schema, entity, lookup_attribute(schema, attribute)?) +} + +fn run_algebrized_query<'sqlite>(sqlite: &'sqlite rusqlite::Connection, algebrized: AlgebraicQuery) -> QueryExecutionResult { if algebrized.is_known_empty() { // We don't need to do any SQL work at all. return Ok(QueryResults::empty(&algebrized.find_spec)); } - // Because this is q_once, we can check that all of our `:in` variables are bound at this point. + // Because we are running 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()?; @@ -126,3 +194,23 @@ pub fn q_once<'sqlite, 'schema, 'query, T> .project(rows) .map_err(|e| e.into()) } + +/// 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 has an open transaction if +/// isolation is required. +pub fn q_once<'sqlite, 'schema, 'query, T> +(sqlite: &'sqlite rusqlite::Connection, + schema: &'schema Schema, + query: &'query str, + inputs: T) -> QueryExecutionResult + where T: Into> +{ + let parsed = parse_find_string(query)?; + let algebrized = algebrize_with_inputs(schema, parsed, 0, inputs.into().unwrap_or(QueryInputs::default()))?; + + run_algebrized_query(sqlite, algebrized) +} diff --git a/tests/query.rs b/tests/query.rs index 7697d8b5..484ac0a3 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -21,6 +21,7 @@ use std::str::FromStr; use chrono::FixedOffset; use mentat_core::{ + DateTime, TypedValue, ValueType, Utc, @@ -454,3 +455,47 @@ fn test_instant_range_query() { _ => panic!("Expected query to work."), } } + +#[test] +fn test_lookup() { + let mut c = 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 "a" :db/ident :foo/date] + [:db/add "a" :db/valueType :db.type/instant] + [:db/add "a" :db/cardinality :db.cardinality/one] + [:db/add "b" :db/ident :foo/many] + [:db/add "b" :db/valueType :db.type/long] + [:db/add "b" :db/cardinality :db.cardinality/many] + ]"#).unwrap(); + + let ids = conn.transact(&mut c, r#"[ + [:db/add "b" :foo/many 123] + [:db/add "b" :foo/many 456] + [:db/add "b" :foo/date #inst "2016-01-01T11:00:00.000Z"] + [:db/add "c" :foo/date #inst "2016-06-01T11:00:01.000Z"] + [:db/add "d" :foo/date #inst "2017-01-01T11:00:02.000Z"] + [:db/add "e" :foo/date #inst "2017-06-01T11:00:03.000Z"] + ]"#).unwrap().tempids; + + let entid = ids.get("b").unwrap(); + let foo_date = NamespacedKeyword::new("foo", "date"); + let foo_many = NamespacedKeyword::new("foo", "many"); + let db_ident = NamespacedKeyword::new("db", "ident"); + let expected = TypedValue::Instant(DateTime::::from_str("2016-01-01T11:00:00.000Z").unwrap()); + + // Fetch a value. + assert_eq!(expected, conn.lookup_value_for_attribute(&c, *entid, &foo_date).unwrap().unwrap()); + + // Try to fetch a missing attribute. + assert!(conn.lookup_value_for_attribute(&c, *entid, &db_ident).unwrap().is_none()); + + // Try to fetch from a non-existent entity. + assert!(conn.lookup_value_for_attribute(&c, 12344567, &foo_date).unwrap().is_none()); + + // Fetch a multi-valued property. + let two_longs = vec![TypedValue::Long(123), TypedValue::Long(456)]; + let fetched_many = conn.lookup_value_for_attribute(&c, *entid, &foo_many).unwrap().unwrap(); + assert!(two_longs.contains(&fetched_many)); +}