From 2614f498beecd6744a9bf8bc47c539a8c85e998b Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Thu, 1 Feb 2018 09:17:07 -0800 Subject: [PATCH] Ergonomics improvements, including a `kw` macro. (#537) r=emily MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add TypedValue::instant(micros). * Add From for TypedValue. * Add lookup_values_for_attribute to Conn. * Add q_explain to Queryable. * Expose an iterator over FindSpec's columns. * Export edn from mentat crate. Export QueryExecutionResult. * Implement Display for Variable and Element. * Introduce a `kw` macro. This allows you to write: ```rust kw!(:foo/bar) ``` instead of ```rust NamespacedKeyword::new("foo", "bar") ``` … and it's more efficient, too. Add `mentat::open`, eliminate use of `mentat_db` in some places. --- core/src/lib.rs | 18 +++++++++-- edn/src/lib.rs | 8 ++++- edn/src/symbols.rs | 16 ++++++---- query/src/lib.rs | 26 +++++++++++++++ src/conn.rs | 22 +++++++++++++ src/entity_builder.rs | 31 ++++++++---------- src/lib.rs | 72 ++++++++++++++++++++++++++++++------------ src/vocabulary.rs | 69 ++++++++++++++++------------------------ tests/external_test.rs | 2 +- tests/query.rs | 7 ++-- tests/vocabulary.rs | 25 ++++++++------- 11 files changed, 190 insertions(+), 106 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index 9d42e69c..695fbc2c 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -8,15 +8,15 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. +extern crate chrono; extern crate enum_set; +extern crate ordered_float; +extern crate uuid; #[macro_use] extern crate lazy_static; -extern crate ordered_float; -extern crate chrono; extern crate edn; -extern crate uuid; pub mod values; @@ -231,6 +231,12 @@ impl TypedValue { pub fn current_instant() -> TypedValue { Utc::now().into() } + + /// Construct a new `TypedValue::Instant` instance from the provided + /// microsecond timestamp. + pub fn instant(micros: i64) -> TypedValue { + DateTime::::from_micros(micros).into() + } } trait MicrosecondPrecision { @@ -301,6 +307,12 @@ impl From for TypedValue { } } +impl From for TypedValue { + fn from(value: f64) -> TypedValue { + TypedValue::Double(OrderedFloat(value)) + } +} + /// Type safe representation of the possible return values from SQLite's `typeof` #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)] pub enum SQLTypeAffinity { diff --git a/edn/src/lib.rs b/edn/src/lib.rs index 6a0130b8..37041c4c 100644 --- a/edn/src/lib.rs +++ b/edn/src/lib.rs @@ -42,4 +42,10 @@ pub use types::{ Value, ValueAndSpan, }; -pub use symbols::{Keyword, NamespacedKeyword, PlainSymbol, NamespacedSymbol}; + +pub use symbols::{ + Keyword, + NamespacedKeyword, + NamespacedSymbol, + PlainSymbol, +}; diff --git a/edn/src/symbols.rs b/edn/src/symbols.rs index 818a821b..cd14ab99 100644 --- a/edn/src/symbols.rs +++ b/edn/src/symbols.rs @@ -10,6 +10,13 @@ use std::fmt::{Display, Formatter}; +#[macro_export] +macro_rules! ns_keyword { + ($ns: expr, $name: expr) => {{ + $crate::NamespacedKeyword::new($ns, $name) + }} +} + /// A simplification of Clojure's Symbol. #[derive(Clone,Debug,Eq,Hash,Ord,PartialOrd,PartialEq)] pub struct PlainSymbol(pub String); @@ -136,6 +143,8 @@ impl NamespacedKeyword { /// let keyword = NamespacedKeyword::new("foo", "bar"); /// assert_eq!(keyword.to_string(), ":foo/bar"); /// ``` + /// + /// See also the `kw!` macro in the main `mentat` crate. pub fn new(namespace: T, name: T) -> Self where T: Into { let n = name.into(); let ns = namespace.into(); @@ -302,13 +311,6 @@ impl Display for NamespacedKeyword { } } -#[macro_export] -macro_rules! ns_keyword { - ($ns: expr, $name: expr) => {{ - $crate::NamespacedKeyword::new($ns, $name) - }} -} - #[test] fn test_ns_keyword_macro() { assert_eq!(ns_keyword!("test", "name").to_string(), diff --git a/query/src/lib.rs b/query/src/lib.rs index a494734e..e1eea1d7 100644 --- a/query/src/lib.rs +++ b/query/src/lib.rs @@ -127,6 +127,12 @@ impl fmt::Debug for Variable { } } +impl std::fmt::Display for Variable { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct QueryFunction(pub PlainSymbol); @@ -443,6 +449,16 @@ pub enum Element { // Pull(Pull), // TODO } +impl std::fmt::Display for Element { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + &Element::Variable(ref var) => { + write!(f, "{}", var) + }, + } + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub enum Limit { None, @@ -543,6 +559,16 @@ impl FindSpec { pub fn requires_distinct(&self) -> bool { !self.is_unit_limited() } + + pub fn columns<'s>(&'s self) -> Box + 's> { + use FindSpec::*; + match self { + &FindScalar(ref e) => Box::new(std::iter::once(e)), + &FindColl(ref e) => Box::new(std::iter::once(e)), + &FindTuple(ref v) => Box::new(v.iter()), + &FindRel(ref v) => Box::new(v.iter()), + } + } } // Datomic accepts variable or placeholder. DataScript accepts recursive bindings. Mentat sticks diff --git a/src/conn.rs b/src/conn.rs index 4d5893e9..23848270 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -105,6 +105,8 @@ pub struct Conn { } pub trait Queryable { + fn q_explain(&self, query: &str, inputs: T) -> Result + where T: Into>; fn q_once(&self, query: &str, inputs: T) -> Result where T: Into>; fn lookup_values_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> @@ -135,6 +137,11 @@ impl<'a, 'c> Queryable for InProgressRead<'a, 'c> { self.0.q_once(query, inputs) } + fn q_explain(&self, query: &str, inputs: T) -> Result + where T: Into> { + self.0.q_explain(query, inputs) + } + fn lookup_values_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> where E: Into { self.0.lookup_values_for_attribute(entity, attribute) @@ -156,6 +163,14 @@ impl<'a, 'c> Queryable for InProgress<'a, 'c> { inputs) } + fn q_explain(&self, query: &str, inputs: T) -> Result + where T: Into> { + q_explain(&*(self.transaction), + &self.schema, + query, + inputs) + } + fn lookup_values_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> where E: Into { lookup_values_for_attribute(&*(self.transaction), &self.schema, entity, attribute) @@ -362,6 +377,13 @@ impl Conn { q_explain(sqlite, &*self.current_schema(), query, inputs) } + pub fn lookup_values_for_attribute(&self, + sqlite: &rusqlite::Connection, + entity: Entid, + attribute: &edn::NamespacedKeyword) -> Result> { + lookup_values_for_attribute(sqlite, &*self.current_schema(), entity, attribute) + } + pub fn lookup_value_for_attribute(&self, sqlite: &rusqlite::Connection, entity: Entid, diff --git a/src/entity_builder.rs b/src/entity_builder.rs index fd7b078a..c46328b0 100644 --- a/src/entity_builder.rs +++ b/src/entity_builder.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. +#![macro_use] + // We have a little bit of a dilemma in Mentat. // The public data format for transacting is, fundamentally, a big string: EDN. // The internal data format for transacting is required to encode the complexities of @@ -23,7 +25,7 @@ // a: Entid::Ident(NamespacedKeyword::new("test", "a1")), // v: Value::Text("v1".into()), // }), -// a: Entid::Ident(NamespacedKeyword::new("test", "a")), +// a: Entid::Ident(kw!(:test/a)), // v: AtomOrLookupRefOrVectorOrMapNotation::Atom(ValueAndSpan::new(SpannedValue::Text("v".into()), Span(44, 47))), // })); // @@ -322,30 +324,23 @@ impl FromThing for TypedValueOr { mod testing { extern crate mentat_db; - use mentat_core::{ - Entid, - HasSchema, - NamespacedKeyword, - TypedValue, - }; - use errors::{ Error, + ErrorKind, }; - use errors::ErrorKind::{ - DbError, - }; - - use mentat_db::TxReport; - + // For matching inside a test. use mentat_db::ErrorKind::{ UnrecognizedEntid, }; use ::{ Conn, + Entid, + HasSchema, Queryable, + TypedValue, + TxReport, }; use super::*; @@ -378,7 +373,7 @@ mod testing { let mut in_progress = conn.begin_transaction(&mut sqlite).expect("begun successfully"); // This should fail: unrecognized entid. - if let Err(Error(DbError(UnrecognizedEntid(e)), _)) = in_progress.transact_terms(terms, tempids) { + if let Err(Error(ErrorKind::DbError(UnrecognizedEntid(e)), _)) = in_progress.transact_terms(terms, tempids) { assert_eq!(e, 999); } else { panic!("Should have rejected the entid."); @@ -390,9 +385,9 @@ mod testing { let mut sqlite = mentat_db::db::new_connection("").unwrap(); let mut conn = Conn::connect(&mut sqlite).unwrap(); - let foo_one = NamespacedKeyword::new("foo", "one"); - let foo_many = NamespacedKeyword::new("foo", "many"); - let foo_ref = NamespacedKeyword::new("foo", "ref"); + let foo_one = kw!(:foo/one); + let foo_many = kw!(:foo/many); + let foo_ref = kw!(:foo/ref); let report: TxReport; // Give ourselves a schema to work with! diff --git a/src/lib.rs b/src/lib.rs index d9da5348..bd2aba18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ extern crate lazy_static; extern crate rusqlite; -extern crate edn; +pub extern crate edn; extern crate mentat_core; extern crate mentat_db; extern crate mentat_query; @@ -30,7 +30,44 @@ extern crate mentat_sql; extern crate mentat_tx; extern crate mentat_tx_parser; -use rusqlite::Connection; +pub use mentat_core::{ + Attribute, + Entid, + HasSchema, + NamespacedKeyword, + TypedValue, + Uuid, + ValueType, +}; + +pub use mentat_db::{ + CORE_SCHEMA_VERSION, + DB_SCHEMA_CORE, + TxReport, + new_connection, +}; + +/// Produce the appropriate `NamespacedKeyword` for the provided namespace and name. +/// This lives here because we can't re-export macros: +/// https://github.com/rust-lang/rust/issues/29638. +#[macro_export] +macro_rules! kw { + ( : $ns:ident / $n:ident ) => { + // We don't need to go through `new` -- `ident` is strict enough. + $crate::NamespacedKeyword { + namespace: stringify!($ns).into(), + name: stringify!($n).into(), + } + }; + + ( : $ns:ident$(. $nss:ident)+ / $n:ident ) => { + // We don't need to go through `new` -- `ident` is strict enough. + $crate::NamespacedKeyword { + namespace: concat!(stringify!($ns) $(, ".", stringify!($nss))+).into(), + name: stringify!($n).into(), + } + }; +} pub mod errors; pub mod ident; @@ -43,29 +80,17 @@ pub fn get_name() -> String { return String::from("mentat"); } -// Will ultimately not return the sqlite connection directly -pub fn get_connection() -> Connection { - return Connection::open_in_memory().unwrap(); +/// Open a Mentat store at the provided path. +pub fn open(path: &str) -> errors::Result<(rusqlite::Connection, Conn)> { + let mut connection = new_connection(path)?; + let conn = Conn::connect(&mut connection)?; + Ok((connection, conn)) } -pub use mentat_core::{ - Attribute, - Entid, - TypedValue, - Uuid, - ValueType, -}; - -pub use mentat_db::{ - CORE_SCHEMA_VERSION, - DB_SCHEMA_CORE, - new_connection, -}; - pub use query::{ IntoResult, - NamespacedKeyword, PlainSymbol, + QueryExecutionResult, QueryExplanation, QueryInputs, QueryPlanStep, @@ -84,9 +109,16 @@ pub use conn::{ #[cfg(test)] mod tests { use edn::symbols::Keyword; + use super::*; #[test] fn can_import_edn() { assert_eq!("foo", Keyword::new("foo").0); } + + #[test] + fn test_kw() { + assert_eq!(kw!(:foo/bar), NamespacedKeyword::new("foo", "bar")); + assert_eq!(kw!(:org.mozilla.foo/bar_baz), NamespacedKeyword::new("org.mozilla.foo", "bar_baz")); + } } diff --git a/src/vocabulary.rs b/src/vocabulary.rs index c03628d6..1c671c85 100644 --- a/src/vocabulary.rs +++ b/src/vocabulary.rs @@ -23,12 +23,11 @@ //! Typical use is the following: //! //! ``` +//! #[macro_use(kw)] //! extern crate mentat; -//! extern crate mentat_db; // So we can use SQLite connection utilities. //! //! use mentat::{ //! Conn, -//! NamespacedKeyword, //! ValueType, //! }; //! @@ -41,8 +40,7 @@ //! }; //! //! fn main() { -//! let mut sqlite = mentat_db::db::new_connection("").expect("SQLite connected"); -//! let mut conn = Conn::connect(&mut sqlite).expect("connected"); +//! let (mut sqlite, mut conn) = mentat::open("").expect("connected"); //! //! { //! // Read the list of installed vocabularies. @@ -64,10 +62,10 @@ //! //! // Make sure our vocabulary is installed, and install if necessary. //! in_progress.ensure_vocabulary(&Definition { -//! name: NamespacedKeyword::new("example", "links"), +//! name: kw!(:example/links), //! version: 1, //! attributes: vec![ -//! (NamespacedKeyword::new("link", "title"), +//! (kw!(:link/title), //! vocabulary::AttributeBuilder::default() //! .value_type(ValueType::String) //! .multival(false) @@ -85,22 +83,19 @@ use std::collections::BTreeMap; -use mentat_core::{ - Attribute, - Entid, - HasSchema, - KnownEntid, - NamespacedKeyword, - TypedValue, - ValueType, -}; - pub use mentat_core::attribute; use mentat_core::attribute::Unique; +use mentat_core::KnownEntid; use ::{ CORE_SCHEMA_VERSION, + Attribute, + Entid, + HasSchema, IntoResult, + NamespacedKeyword, + TypedValue, + ValueType, }; use ::conn::{ @@ -170,25 +165,25 @@ impl Vocabularies { lazy_static! { static ref DB_SCHEMA_CORE: NamespacedKeyword = { - NamespacedKeyword::new("db.schema", "core") + kw!(:db.schema/core) }; static ref DB_SCHEMA_ATTRIBUTE: NamespacedKeyword = { - NamespacedKeyword::new("db.schema", "attribute") + kw!(:db.schema/attribute) }; static ref DB_SCHEMA_VERSION: NamespacedKeyword = { - NamespacedKeyword::new("db.schema", "version") + kw!(:db.schema/version) }; static ref DB_IDENT: NamespacedKeyword = { - NamespacedKeyword::new("db", "ident") + kw!(:db/ident) }; static ref DB_UNIQUE: NamespacedKeyword = { - NamespacedKeyword::new("db", "unique") + kw!(:db/unique) }; static ref DB_UNIQUE_VALUE: NamespacedKeyword = { - NamespacedKeyword::new("db.unique", "value") + kw!(:db.unique/value) }; static ref DB_UNIQUE_IDENTITY: NamespacedKeyword = { - NamespacedKeyword::new("db.unique", "identity") + kw!(:db.unique/identity) }; static ref DB_IS_COMPONENT: NamespacedKeyword = { NamespacedKeyword::new("db", "isComponent") @@ -197,19 +192,19 @@ lazy_static! { NamespacedKeyword::new("db", "valueType") }; static ref DB_INDEX: NamespacedKeyword = { - NamespacedKeyword::new("db", "index") + kw!(:db/index) }; static ref DB_FULLTEXT: NamespacedKeyword = { - NamespacedKeyword::new("db", "fulltext") + kw!(:db/fulltext) }; static ref DB_CARDINALITY: NamespacedKeyword = { - NamespacedKeyword::new("db", "cardinality") + kw!(:db/cardinality) }; static ref DB_CARDINALITY_ONE: NamespacedKeyword = { - NamespacedKeyword::new("db.cardinality", "one") + kw!(:db.cardinality/one) }; static ref DB_CARDINALITY_MANY: NamespacedKeyword = { - NamespacedKeyword::new("db.cardinality", "many") + kw!(:db.cardinality/many) }; // Not yet supported. @@ -574,32 +569,24 @@ impl HasVocabularies for T where T: HasSchema + Queryable { #[cfg(test)] mod tests { - use ::{ - NamespacedKeyword, - Conn, - new_connection, - }; - use super::HasVocabularies; #[test] fn test_read_vocabularies() { - let mut sqlite = new_connection("").expect("could open conn"); - let mut conn = Conn::connect(&mut sqlite).expect("could open store"); + let (mut sqlite, mut conn) = ::open("").expect("opened"); let vocabularies = conn.begin_read(&mut sqlite).expect("in progress") .read_vocabularies().expect("OK"); assert_eq!(vocabularies.len(), 1); - let core = vocabularies.get(&NamespacedKeyword::new("db.schema", "core")).expect("exists"); + let core = vocabularies.get(&kw!(:db.schema/core)).expect("exists"); assert_eq!(core.version, 1); } #[test] fn test_core_schema() { - let mut c = new_connection("").expect("could open conn"); - let mut conn = Conn::connect(&mut c).expect("could open store"); - let in_progress = conn.begin_transaction(&mut c).expect("in progress"); + let (mut sqlite, mut conn) = ::open("").expect("opened"); + let in_progress = conn.begin_transaction(&mut sqlite).expect("in progress"); let vocab = in_progress.read_vocabularies().expect("vocabulary"); assert_eq!(1, vocab.len()); - assert_eq!(1, vocab.get(&NamespacedKeyword::new("db.schema", "core")).expect("core vocab").version); + assert_eq!(1, vocab.get(&kw!(:db.schema/core)).expect("core vocab").version); } } diff --git a/tests/external_test.rs b/tests/external_test.rs index a0d2b404..0f874ca6 100644 --- a/tests/external_test.rs +++ b/tests/external_test.rs @@ -20,7 +20,7 @@ fn can_import_sqlite() { data: Option>, } - let conn = mentat::get_connection(); + let conn = mentat::new_connection("").expect("SQLite connected"); conn.execute("CREATE TABLE person ( id INTEGER PRIMARY KEY, diff --git a/tests/query.rs b/tests/query.rs index 7738d45a..9b2e5330 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -11,6 +11,7 @@ extern crate chrono; extern crate time; +#[macro_use] extern crate mentat; extern crate mentat_core; extern crate mentat_db; @@ -482,9 +483,9 @@ fn test_lookup() { ]"#).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 foo_date = kw!(:foo/date); + let foo_many = kw!(:foo/many); + let db_ident = kw!(:db/ident); let expected = TypedValue::Instant(DateTime::::from_str("2016-01-01T11:00:00.000Z").unwrap()); // Fetch a value. diff --git a/tests/vocabulary.rs b/tests/vocabulary.rs index 33825586..2a90ea44 100644 --- a/tests/vocabulary.rs +++ b/tests/vocabulary.rs @@ -11,6 +11,7 @@ #[macro_use] extern crate lazy_static; +#[macro_use] extern crate mentat; extern crate mentat_core; extern crate mentat_db; @@ -47,16 +48,16 @@ use mentat::errors::{ lazy_static! { static ref FOO_NAME: NamespacedKeyword = { - NamespacedKeyword::new("foo", "name") + kw!(:foo/name) }; static ref FOO_MOMENT: NamespacedKeyword = { - NamespacedKeyword::new("foo", "moment") + kw!(:foo/moment) }; static ref FOO_VOCAB: vocabulary::Definition = { vocabulary::Definition { - name: NamespacedKeyword::new("org.mozilla", "foo"), + name: kw!(:org.mozilla/foo), version: 1, attributes: vec![ (FOO_NAME.clone(), @@ -129,24 +130,24 @@ fn test_add_vocab() { .fulltext(true) .build(); let bar_only = vec![ - (NamespacedKeyword::new("foo", "bar"), bar.clone()), + (kw!(:foo/bar), bar.clone()), ]; let baz_only = vec![ - (NamespacedKeyword::new("foo", "baz"), baz.clone()), + (kw!(:foo/baz), baz.clone()), ]; let bar_and_baz = vec![ - (NamespacedKeyword::new("foo", "bar"), bar.clone()), - (NamespacedKeyword::new("foo", "baz"), baz.clone()), + (kw!(:foo/bar), bar.clone()), + (kw!(:foo/baz), baz.clone()), ]; let foo_v1_a = vocabulary::Definition { - name: NamespacedKeyword::new("org.mozilla", "foo"), + name: kw!(:org.mozilla/foo), version: 1, attributes: bar_only.clone(), }; let foo_v1_b = vocabulary::Definition { - name: NamespacedKeyword::new("org.mozilla", "foo"), + name: kw!(:org.mozilla/foo), version: 1, attributes: bar_and_baz.clone(), }; @@ -244,11 +245,11 @@ fn test_add_vocab() { .multival(true) .build(); let bar_and_malformed_baz = vec![ - (NamespacedKeyword::new("foo", "bar"), bar), - (NamespacedKeyword::new("foo", "baz"), malformed_baz.clone()), + (kw!(:foo/bar), bar), + (kw!(:foo/baz), malformed_baz.clone()), ]; let foo_v1_malformed = vocabulary::Definition { - name: NamespacedKeyword::new("org.mozilla", "foo"), + name: kw!(:org.mozilla/foo), version: 1, attributes: bar_and_malformed_baz.clone(), };