Ergonomics improvements, including a kw macro. (#537) r=emily

* Add TypedValue::instant(micros).
* Add From<f64> 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.
This commit is contained in:
Richard Newman 2018-02-01 09:17:07 -08:00
parent 3bf7459315
commit 2614f498be
11 changed files with 190 additions and 106 deletions

View file

@ -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::<Utc>::from_micros(micros).into()
}
}
trait MicrosecondPrecision {
@ -301,6 +307,12 @@ impl From<i32> for TypedValue {
}
}
impl From<f64> 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 {

View file

@ -42,4 +42,10 @@ pub use types::{
Value,
ValueAndSpan,
};
pub use symbols::{Keyword, NamespacedKeyword, PlainSymbol, NamespacedSymbol};
pub use symbols::{
Keyword,
NamespacedKeyword,
NamespacedSymbol,
PlainSymbol,
};

View file

@ -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<T>(namespace: T, name: T) -> Self where T: Into<String> {
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(),

View file

@ -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<Iterator<Item=&Element> + '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

View file

@ -105,6 +105,8 @@ pub struct Conn {
}
pub trait Queryable {
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
where T: Into<Option<QueryInputs>>;
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryResults>
where T: Into<Option<QueryInputs>>;
fn lookup_values_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Vec<TypedValue>>
@ -135,6 +137,11 @@ impl<'a, 'c> Queryable for InProgressRead<'a, 'c> {
self.0.q_once(query, inputs)
}
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
where T: Into<Option<QueryInputs>> {
self.0.q_explain(query, inputs)
}
fn lookup_values_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Vec<TypedValue>>
where E: Into<Entid> {
self.0.lookup_values_for_attribute(entity, attribute)
@ -156,6 +163,14 @@ impl<'a, 'c> Queryable for InProgress<'a, 'c> {
inputs)
}
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
where T: Into<Option<QueryInputs>> {
q_explain(&*(self.transaction),
&self.schema,
query,
inputs)
}
fn lookup_values_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Vec<TypedValue>>
where E: Into<Entid> {
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<Vec<TypedValue>> {
lookup_values_for_attribute(sqlite, &*self.current_schema(), entity, attribute)
}
pub fn lookup_value_for_attribute(&self,
sqlite: &rusqlite::Connection,
entity: Entid,

View file

@ -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<KnownEntid> for TypedValueOr<TempIdHandle> {
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!

View file

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

View file

@ -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<T> 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);
}
}

View file

@ -20,7 +20,7 @@ fn can_import_sqlite() {
data: Option<Vec<u8>>,
}
let conn = mentat::get_connection();
let conn = mentat::new_connection("").expect("SQLite connected");
conn.execute("CREATE TABLE person (
id INTEGER PRIMARY KEY,

View file

@ -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::<Utc>::from_str("2016-01-01T11:00:00.000Z").unwrap());
// Fetch a value.

View file

@ -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(),
};