diff --git a/Cargo.toml b/Cargo.toml index 8849b8c9..a9f80375 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [] rustc_version = "0.1.7" [dependencies] +chrono = "0.3" clap = "2.19.3" error-chain = "0.8.1" nickel = "0.9.0" diff --git a/core/src/lib.rs b/core/src/lib.rs index 67b4d183..e83030ab 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,7 +25,20 @@ use std::rc::Rc; use enum_set::EnumSet; use self::ordered_float::OrderedFloat; -use self::edn::NamespacedKeyword; +use self::edn::{ + NamespacedKeyword, +}; + +pub use edn::{ + Uuid, +}; + +pub use edn::{ + DateTime, + FromMicros, + ToMicros, + UTC, +}; /// Core types defining a Mentat knowledge base. @@ -48,6 +61,7 @@ pub enum ValueType { Double, String, Keyword, + Uuid, } impl ValueType { @@ -61,6 +75,7 @@ impl ValueType { s.insert(ValueType::Double); s.insert(ValueType::String); s.insert(ValueType::Keyword); + s.insert(ValueType::Uuid); s } } @@ -86,6 +101,7 @@ impl ValueType { ValueType::Double => values::DB_TYPE_DOUBLE.clone(), ValueType::String => values::DB_TYPE_STRING.clone(), ValueType::Keyword => values::DB_TYPE_KEYWORD.clone(), + ValueType::Uuid => values::DB_TYPE_UUID.clone(), } } } @@ -100,6 +116,7 @@ impl fmt::Display for ValueType { ValueType::Double => ":db.type/double", ValueType::String => ":db.type/string", ValueType::Keyword => ":db.type/keyword", + ValueType::Uuid => ":db.type/uuid", }) } } @@ -113,9 +130,11 @@ pub enum TypedValue { Boolean(bool), Long(i64), Double(OrderedFloat), + Instant(DateTime), // TODO: &str throughout? String(Rc), Keyword(Rc), + Uuid(Uuid), // It's only 128 bits, so this should be acceptable to clone. } impl TypedValue { @@ -136,9 +155,11 @@ impl TypedValue { &TypedValue::Ref(_) => ValueType::Ref, &TypedValue::Boolean(_) => ValueType::Boolean, &TypedValue::Long(_) => ValueType::Long, + &TypedValue::Instant(_) => ValueType::Instant, &TypedValue::Double(_) => ValueType::Double, &TypedValue::String(_) => ValueType::String, &TypedValue::Keyword(_) => ValueType::Keyword, + &TypedValue::Uuid(_) => ValueType::Uuid, } } @@ -155,6 +176,10 @@ impl TypedValue { pub fn typed_string(s: &str) -> TypedValue { TypedValue::String(Rc::new(s.to_string())) } + + pub fn current_instant() -> TypedValue { + TypedValue::Instant(UTC::now()) + } } // Put this here rather than in `db` simply because it's widely needed. @@ -173,6 +198,7 @@ impl SQLValueType for ValueType { ValueType::Long => 5, ValueType::Double => 5, ValueType::String => 10, + ValueType::Uuid => 11, ValueType::Keyword => 13, } } @@ -182,6 +208,8 @@ impl SQLValueType for ValueType { /// /// ``` /// use mentat_core::{ValueType, SQLValueType}; + /// assert!(!ValueType::Instant.accommodates_integer(1493399581314)); + /// assert!(!ValueType::Instant.accommodates_integer(1493399581314000)); /// assert!(ValueType::Boolean.accommodates_integer(1)); /// assert!(!ValueType::Boolean.accommodates_integer(-1)); /// assert!(!ValueType::Boolean.accommodates_integer(10)); @@ -190,11 +218,13 @@ impl SQLValueType for ValueType { fn accommodates_integer(&self, int: i64) -> bool { use ValueType::*; match *self { - Instant | Long | Double => true, + Instant => false, // Always use #inst. + Long | Double => true, Ref => int >= 0, Boolean => (int == 0) || (int == 1), ValueType::String => false, Keyword => false, + Uuid => false, } } } diff --git a/db/src/bootstrap.rs b/db/src/bootstrap.rs index d844d440..e8611615 100644 --- a/db/src/bootstrap.rs +++ b/db/src/bootstrap.rs @@ -61,6 +61,8 @@ lazy_static! { (ns_keyword!("db.type", "long"), entids::DB_TYPE_LONG), (ns_keyword!("db.type", "double"), entids::DB_TYPE_DOUBLE), (ns_keyword!("db.type", "string"), entids::DB_TYPE_STRING), + (ns_keyword!("db.type", "uuid"), entids::DB_TYPE_UUID), + (ns_keyword!("db.type", "uri"), entids::DB_TYPE_URI), (ns_keyword!("db.type", "boolean"), entids::DB_TYPE_BOOLEAN), (ns_keyword!("db.type", "instant"), entids::DB_TYPE_INSTANT), (ns_keyword!("db.type", "bytes"), entids::DB_TYPE_BYTES), @@ -69,16 +71,11 @@ lazy_static! { (ns_keyword!("db.unique", "value"), entids::DB_UNIQUE_VALUE), (ns_keyword!("db.unique", "identity"), entids::DB_UNIQUE_IDENTITY), (ns_keyword!("db", "doc"), entids::DB_DOC), + (ns_keyword!("db.schema", "version"), entids::DB_SCHEMA_VERSION), + (ns_keyword!("db.schema", "attribute"), entids::DB_SCHEMA_ATTRIBUTE), ] }; - static ref V2_IDENTS: Vec<(symbols::NamespacedKeyword, i64)> = { - [(*V1_IDENTS).clone(), - vec![(ns_keyword!("db.schema", "version"), entids::DB_SCHEMA_VERSION), - (ns_keyword!("db.schema", "attribute"), entids::DB_SCHEMA_ATTRIBUTE), - ]].concat() - }; - static ref V1_PARTS: Vec<(symbols::NamespacedKeyword, i64, i64)> = { vec![(ns_keyword!("db.part", "db"), 0, (1 + V1_IDENTS.len()) as i64), (ns_keyword!("db.part", "user"), 0x10000, 0x10000), @@ -86,13 +83,6 @@ lazy_static! { ] }; - static ref V2_PARTS: Vec<(symbols::NamespacedKeyword, i64, i64)> = { - vec![(ns_keyword!("db.part", "db"), 0, (1 + V2_IDENTS.len()) as i64), - (ns_keyword!("db.part", "user"), 0x10000, 0x10000), - (ns_keyword!("db.part", "tx"), TX0, TX0), - ] - }; - static ref V1_SYMBOLIC_SCHEMA: Value = { let s = r#" {:db/ident {:db/valueType :db.type/keyword @@ -108,7 +98,7 @@ lazy_static! { ;; TODO: support user-specified functions in the future. ;; :db.install/function {:db/valueType :db.type/ref ;; :db/cardinality :db.cardinality/many} - :db/txInstant {:db/valueType :db.type/long + :db/txInstant {:db/valueType :db.type/instant :db/cardinality :db.cardinality/one :db/index true} :db/valueType {:db/valueType :db.type/ref @@ -126,16 +116,8 @@ lazy_static! { :db/fulltext {:db/valueType :db.type/boolean :db/cardinality :db.cardinality/one} :db/noHistory {:db/valueType :db.type/boolean - :db/cardinality :db.cardinality/one}}"#; - edn::parse::value(s) - .map(|v| v.without_spans()) - .map_err(|_| ErrorKind::BadBootstrapDefinition("Unable to parse V1_SYMBOLIC_SCHEMA".into())) - .unwrap() - }; - - static ref V2_SYMBOLIC_SCHEMA: Value = { - let s = r#" -{:db.alter/attribute {:db/valueType :db.type/ref + :db/cardinality :db.cardinality/one} + :db.alter/attribute {:db/valueType :db.type/ref :db/cardinality :db.cardinality/many} :db.schema/version {:db/valueType :db.type/long :db/cardinality :db.cardinality/one} @@ -146,13 +128,9 @@ lazy_static! { :db/index true :db/unique :db.unique/value :db/cardinality :db.cardinality/many}}"#; - let right = edn::parse::value(s) + edn::parse::value(s) .map(|v| v.without_spans()) - .map_err(|_| ErrorKind::BadBootstrapDefinition("Unable to parse V2_SYMBOLIC_SCHEMA".into())) - .unwrap(); - - edn::utils::merge(&V1_SYMBOLIC_SCHEMA, &right) - .ok_or(ErrorKind::BadBootstrapDefinition("Unable to parse V2_SYMBOLIC_SCHEMA".into())) + .map_err(|_| ErrorKind::BadBootstrapDefinition("Unable to parse V1_SYMBOLIC_SCHEMA".into())) .unwrap() }; } @@ -248,27 +226,27 @@ fn symbolic_schema_to_assertions(symbolic_schema: &Value) -> Result> } pub fn bootstrap_partition_map() -> PartitionMap { - V2_PARTS[..].iter() + V1_PARTS[..].iter() .map(|&(ref part, start, index)| (part.to_string(), Partition::new(start, index))) .collect() } pub fn bootstrap_ident_map() -> IdentMap { - V2_IDENTS[..].iter() + V1_IDENTS[..].iter() .map(|&(ref ident, entid)| (ident.clone(), entid)) .collect() } pub fn bootstrap_schema() -> Schema { let ident_map = bootstrap_ident_map(); - let bootstrap_triples = symbolic_schema_to_triples(&ident_map, &V2_SYMBOLIC_SCHEMA).unwrap(); + let bootstrap_triples = symbolic_schema_to_triples(&ident_map, &V1_SYMBOLIC_SCHEMA).unwrap(); Schema::from_ident_map_and_triples(ident_map, bootstrap_triples).unwrap() } pub fn bootstrap_entities() -> Vec { let bootstrap_assertions: Value = Value::Vector([ - symbolic_schema_to_assertions(&V2_SYMBOLIC_SCHEMA).unwrap(), - idents_to_assertions(&V2_IDENTS[..]), + symbolic_schema_to_assertions(&V1_SYMBOLIC_SCHEMA).unwrap(), + idents_to_assertions(&V1_IDENTS[..]), ].concat()); // Failure here is a coding error (since the inputs are fixed), not a runtime error. diff --git a/db/src/db.rs b/db/src/db.rs index 5c1d6435..ccceb0ea 100644 --- a/db/src/db.rs +++ b/db/src/db.rs @@ -26,17 +26,26 @@ use rusqlite::limits::Limit; use ::{repeat_values, to_namespaced_keyword}; use bootstrap; -use edn::types::Value; + +use edn::{ + DateTime, + UTC, + Uuid, + Value, +}; + use entids; use mentat_core::{ attribute, Attribute, AttributeBitFlags, Entid, + FromMicros, IdentMap, Schema, SchemaMap, TypedValue, + ToMicros, ValueType, }; use errors::{ErrorKind, Result, ResultExt}; @@ -72,10 +81,8 @@ pub fn new_connection(uri: T) -> rusqlite::Result where /// Version history: /// -/// 1: initial schema. -/// 2: added :db.schema/version and /attribute in bootstrap; assigned idents 36 and 37, so we bump -/// the part range here; tie bootstrapping to the SQLite user_version. -pub const CURRENT_VERSION: i32 = 2; +/// 1: initial Rust Mentat schema. +pub const CURRENT_VERSION: i32 = 1; /// MIN_SQLITE_VERSION should be changed when there's a new minimum version of sqlite required /// for the project to work. @@ -93,9 +100,9 @@ fn to_bool_ref(x: bool) -> &'static bool { } lazy_static! { - /// SQL statements to be executed, in order, to create the Mentat SQL schema (version 2). + /// SQL statements to be executed, in order, to create the Mentat SQL schema (version 1). #[cfg_attr(rustfmt, rustfmt_skip)] - static ref V2_STATEMENTS: Vec<&'static str> = { vec![ + static ref V1_STATEMENTS: Vec<&'static str> = { vec![ r#"CREATE TABLE datoms (e INTEGER NOT NULL, a SMALLINT NOT NULL, v BLOB NOT NULL, tx INTEGER NOT NULL, value_type_tag SMALLINT NOT NULL, index_avet TINYINT NOT NULL DEFAULT 0, index_vaet TINYINT NOT NULL DEFAULT 0, @@ -203,7 +210,7 @@ fn get_user_version(conn: &rusqlite::Connection) -> Result { pub fn create_current_version(conn: &mut rusqlite::Connection) -> Result { let tx = conn.transaction()?; - for statement in (&V2_STATEMENTS).iter() { + for statement in (&V1_STATEMENTS).iter() { tx.execute(statement, &[])?; } @@ -347,11 +354,24 @@ impl TypedSQLValue for TypedValue { match (value_type_tag, value) { (0, rusqlite::types::Value::Integer(x)) => Ok(TypedValue::Ref(x)), (1, rusqlite::types::Value::Integer(x)) => Ok(TypedValue::Boolean(0 != x)), + + // Negative integers are simply times before 1970. + (4, rusqlite::types::Value::Integer(x)) => Ok(TypedValue::Instant(DateTime::::from_micros(x))), + // SQLite distinguishes integral from decimal types, allowing long and double to // share a tag. (5, rusqlite::types::Value::Integer(x)) => Ok(TypedValue::Long(x)), (5, rusqlite::types::Value::Real(x)) => Ok(TypedValue::Double(x.into())), (10, rusqlite::types::Value::Text(x)) => Ok(TypedValue::String(Rc::new(x))), + (11, rusqlite::types::Value::Blob(x)) => { + let u = Uuid::from_bytes(x.as_slice()); + if u.is_err() { + // Rather than exposing Uuid's ParseError… + bail!(ErrorKind::BadSQLValuePair(rusqlite::types::Value::Blob(x), + value_type_tag)); + } + Ok(TypedValue::Uuid(u.unwrap())) + }, (13, rusqlite::types::Value::Text(x)) => { to_namespaced_keyword(&x).map(|k| TypedValue::Keyword(Rc::new(k))) }, @@ -369,7 +389,9 @@ impl TypedSQLValue for TypedValue { fn from_edn_value(value: &Value) -> Option { match value { &Value::Boolean(x) => Some(TypedValue::Boolean(x)), + &Value::Instant(x) => Some(TypedValue::Instant(x)), &Value::Integer(x) => Some(TypedValue::Long(x)), + &Value::Uuid(x) => Some(TypedValue::Uuid(x)), &Value::Float(ref x) => Some(TypedValue::Double(x.clone())), &Value::Text(ref x) => Some(TypedValue::String(Rc::new(x.clone()))), &Value::NamespacedKeyword(ref x) => Some(TypedValue::Keyword(Rc::new(x.clone()))), @@ -382,10 +404,12 @@ impl TypedSQLValue for TypedValue { match self { &TypedValue::Ref(x) => (rusqlite::types::Value::Integer(x).into(), 0), &TypedValue::Boolean(x) => (rusqlite::types::Value::Integer(if x { 1 } else { 0 }).into(), 1), + &TypedValue::Instant(x) => (rusqlite::types::Value::Integer(x.to_micros()).into(), 4), // SQLite distinguishes integral from decimal types, allowing long and double to share a tag. &TypedValue::Long(x) => (rusqlite::types::Value::Integer(x).into(), 5), &TypedValue::Double(x) => (rusqlite::types::Value::Real(x.into_inner()).into(), 5), &TypedValue::String(ref x) => (rusqlite::types::ValueRef::Text(x.as_str()).into(), 10), + &TypedValue::Uuid(ref u) => (rusqlite::types::Value::Blob(u.as_bytes().to_vec()).into(), 11), &TypedValue::Keyword(ref x) => (rusqlite::types::ValueRef::Text(&x.to_string()).into(), 13), } } @@ -395,9 +419,11 @@ impl TypedSQLValue for TypedValue { match self { &TypedValue::Ref(x) => (Value::Integer(x), ValueType::Ref), &TypedValue::Boolean(x) => (Value::Boolean(x), ValueType::Boolean), + &TypedValue::Instant(x) => (Value::Instant(x), ValueType::Instant), &TypedValue::Long(x) => (Value::Integer(x), ValueType::Long), &TypedValue::Double(x) => (Value::Float(x), ValueType::Double), &TypedValue::String(ref x) => (Value::Text(x.as_ref().clone()), ValueType::String), + &TypedValue::Uuid(ref u) => (Value::Uuid(u.clone()), ValueType::Uuid), &TypedValue::Keyword(ref x) => (Value::NamespacedKeyword(x.as_ref().clone()), ValueType::Keyword), } } @@ -1173,12 +1199,12 @@ mod tests { // Does not include :db/txInstant. let datoms = debug::datoms_after(&conn, &db.schema, 0).unwrap(); - assert_eq!(datoms.0.len(), 74); + assert_eq!(datoms.0.len(), 76); // Includes :db/txInstant. let transactions = debug::transactions_after(&conn, &db.schema, 0).unwrap(); assert_eq!(transactions.0.len(), 1); - assert_eq!(transactions.0[0].0.len(), 75); + assert_eq!(transactions.0[0].0.len(), 77); let test_conn = TestConn { sqlite: conn, diff --git a/db/src/entids.rs b/db/src/entids.rs index 50d06b7c..1ba910b0 100644 --- a/db/src/entids.rs +++ b/db/src/entids.rs @@ -44,18 +44,18 @@ pub const DB_TYPE_KEYWORD: Entid = 24; pub const DB_TYPE_LONG: Entid = 25; pub const DB_TYPE_DOUBLE: Entid = 26; pub const DB_TYPE_STRING: Entid = 27; -pub const DB_TYPE_BOOLEAN: Entid = 28; -pub const DB_TYPE_INSTANT: Entid = 29; -pub const DB_TYPE_BYTES: Entid = 30; -pub const DB_CARDINALITY_ONE: Entid = 31; -pub const DB_CARDINALITY_MANY: Entid = 32; -pub const DB_UNIQUE_VALUE: Entid = 33; -pub const DB_UNIQUE_IDENTITY: Entid = 34; -pub const DB_DOC: Entid = 35; - -// Added in SQL schema v2. -pub const DB_SCHEMA_VERSION: Entid = 36; -pub const DB_SCHEMA_ATTRIBUTE: Entid = 37; +pub const DB_TYPE_UUID: Entid = 28; +pub const DB_TYPE_URI: Entid = 29; +pub const DB_TYPE_BOOLEAN: Entid = 30; +pub const DB_TYPE_INSTANT: Entid = 31; +pub const DB_TYPE_BYTES: Entid = 32; +pub const DB_CARDINALITY_ONE: Entid = 33; +pub const DB_CARDINALITY_MANY: Entid = 34; +pub const DB_UNIQUE_VALUE: Entid = 35; +pub const DB_UNIQUE_IDENTITY: Entid = 36; +pub const DB_DOC: Entid = 37; +pub const DB_SCHEMA_VERSION: Entid = 38; +pub const DB_SCHEMA_ATTRIBUTE: Entid = 39; /// Return `false` if the given attribute will not change the metadata: recognized idents, schema, /// partitions in the partition map. diff --git a/db/src/lib.rs b/db/src/lib.rs index 2767d84b..07f5bc5b 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -11,12 +11,12 @@ #[macro_use] extern crate error_chain; extern crate itertools; + #[macro_use] extern crate lazy_static; extern crate rusqlite; -extern crate time; - extern crate tabwriter; +extern crate time; #[macro_use] extern crate edn; @@ -24,8 +24,15 @@ extern crate mentat_core; extern crate mentat_tx; extern crate mentat_tx_parser; -use itertools::Itertools; use std::iter::repeat; + +use itertools::Itertools; + +use mentat_core::{ + DateTime, + UTC, +}; + pub use errors::{Error, ErrorKind, ResultExt, Result}; pub mod db; @@ -89,10 +96,7 @@ pub fn repeat_values(values_per_tuple: usize, tuples: usize) -> String { values } -/// Return the current time in milliseconds after the Unix epoch according to the local clock. -/// -/// Compare `Date.now()` in JavaScript, `System.currentTimeMillis` in Java. -pub fn now() -> i64 { - let now = time::get_time(); - (now.sec as i64 * 1_000) + (now.nsec as i64 / (1_000_000)) +/// Return the current time as a UTC `DateTime` instance. +pub fn now() -> DateTime { + UTC::now() } diff --git a/db/src/metadata.rs b/db/src/metadata.rs index 918bdb14..3d67af58 100644 --- a/db/src/metadata.rs +++ b/db/src/metadata.rs @@ -115,12 +115,14 @@ pub fn update_schema_map_from_entid_triples(schema_map: &mut SchemaMap, asser entids::DB_VALUE_TYPE => { match *value { - TypedValue::Ref(entids::DB_TYPE_REF) => { builder.value_type(ValueType::Ref); }, TypedValue::Ref(entids::DB_TYPE_BOOLEAN) => { builder.value_type(ValueType::Boolean); }, - TypedValue::Ref(entids::DB_TYPE_DOUBLE) => { builder.value_type(ValueType::Double); }, - TypedValue::Ref(entids::DB_TYPE_LONG) => { builder.value_type(ValueType::Long); }, - TypedValue::Ref(entids::DB_TYPE_STRING) => { builder.value_type(ValueType::String); }, + TypedValue::Ref(entids::DB_TYPE_DOUBLE) => { builder.value_type(ValueType::Double); }, + TypedValue::Ref(entids::DB_TYPE_INSTANT) => { builder.value_type(ValueType::Instant); }, TypedValue::Ref(entids::DB_TYPE_KEYWORD) => { builder.value_type(ValueType::Keyword); }, + TypedValue::Ref(entids::DB_TYPE_LONG) => { builder.value_type(ValueType::Long); }, + TypedValue::Ref(entids::DB_TYPE_REF) => { builder.value_type(ValueType::Ref); }, + TypedValue::Ref(entids::DB_TYPE_STRING) => { builder.value_type(ValueType::String); }, + TypedValue::Ref(entids::DB_TYPE_UUID) => { builder.value_type(ValueType::Uuid); }, _ => bail!(ErrorKind::BadSchemaAssertion(format!("Expected [... :db/valueType :db.type/*] but got [... :db/valueType {:?}] for entid {} and attribute {}", value, entid, attr))) } }, diff --git a/db/src/schema.rs b/db/src/schema.rs index d9e82269..ccd79093 100644 --- a/db/src/schema.rs +++ b/db/src/schema.rs @@ -240,6 +240,7 @@ impl SchemaTypeChecking for Schema { (&ValueType::Long, tv @ TypedValue::Long(_)) => Ok(tv), (&ValueType::Double, tv @ TypedValue::Double(_)) => Ok(tv), (&ValueType::String, tv @ TypedValue::String(_)) => Ok(tv), + (&ValueType::Uuid, tv @ TypedValue::Uuid(_)) => Ok(tv), (&ValueType::Keyword, tv @ TypedValue::Keyword(_)) => Ok(tv), // Ref coerces a little: we interpret some things depending on the schema as a Ref. (&ValueType::Ref, TypedValue::Long(x)) => Ok(TypedValue::Ref(x)), diff --git a/db/src/tx.rs b/db/src/tx.rs index e7464a34..ee6db13e 100644 --- a/db/src/tx.rs +++ b/db/src/tx.rs @@ -70,10 +70,13 @@ use internal_types::{ TermWithTempIds, TermWithoutTempIds, replace_lookup_ref}; + use mentat_core::{ + DateTime, + Schema, + UTC, attribute, intern_set, - Schema, }; use mentat_tx::entities as entmod; use mentat_tx::entities::{ @@ -127,10 +130,7 @@ pub struct Tx<'conn, 'a> { tx_id: Entid, /// The timestamp when the transaction began to be committed. - /// - /// This is milliseconds after the Unix epoch according to the transactor's local clock. - // TODO: :db.type/instant. - tx_instant: i64, + tx_instant: DateTime, } impl<'conn, 'a> Tx<'conn, 'a> { @@ -140,7 +140,7 @@ impl<'conn, 'a> Tx<'conn, 'a> { schema_for_mutation: &'a Schema, schema: &'a Schema, tx_id: Entid, - tx_instant: i64) -> Tx<'conn, 'a> { + tx_instant: DateTime) -> Tx<'conn, 'a> { Tx { store: store, partition_map: partition_map, @@ -532,7 +532,7 @@ impl<'conn, 'a> Tx<'conn, 'a> { non_fts_one.push((self.tx_id, entids::DB_TX_INSTANT, self.schema.require_attribute_for_entid(entids::DB_TX_INSTANT).unwrap(), - TypedValue::Long(self.tx_instant), + TypedValue::Instant(self.tx_instant), true)); if !non_fts_one.is_empty() { diff --git a/db/src/types.rs b/db/src/types.rs index ab73da52..ab6dfd8f 100644 --- a/db/src/types.rs +++ b/db/src/types.rs @@ -16,12 +16,14 @@ use std::collections::BTreeMap; extern crate mentat_core; pub use self::mentat_core::{ + DateTime, Entid, ValueType, TypedValue, Attribute, AttributeBitFlags, Schema, + UTC, }; /// Represents one partition of the entid space. @@ -78,16 +80,13 @@ pub type AVMap<'a> = HashMap<&'a AVPair, Entid>; /// A transaction report summarizes an applied transaction. // TODO: include map of resolved tempids. -#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialOrd, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)] pub struct TxReport { /// The transaction ID of the transaction. pub tx_id: Entid, /// The timestamp when the transaction began to be committed. - /// - /// This is milliseconds after the Unix epoch according to the transactor's local clock. - // TODO: :db.type/instant. - pub tx_instant: i64, + pub tx_instant: DateTime, /// A map from string literal tempid to resolved or allocated entid. /// diff --git a/edn/Cargo.toml b/edn/Cargo.toml index aea1ed00..6f0c7f62 100644 --- a/edn/Cargo.toml +++ b/edn/Cargo.toml @@ -11,10 +11,12 @@ build = "build.rs" readme = "./README.md" [dependencies] +chrono = "0.3" itertools = "0.5.9" num = "0.1.35" ordered-float = "0.4.0" pretty = "0.2.0" +uuid = "0.5.0" [build-dependencies] peg = "0.5.1" diff --git a/edn/src/edn.rustpeg b/edn/src/edn.rustpeg index b7328ba5..01c67c69 100644 --- a/edn/src/edn.rustpeg +++ b/edn/src/edn.rustpeg @@ -14,8 +14,14 @@ use std::collections::{BTreeSet, BTreeMap, LinkedList}; use std::iter::FromIterator; use std::f64::{NAN, INFINITY, NEG_INFINITY}; +use chrono::{ + DateTime, + TimeZone, + UTC +}; use num::BigInt; use ordered_float::OrderedFloat; +use uuid::Uuid; use types::{SpannedValue, Span, ValueAndSpan}; @@ -142,6 +148,60 @@ pub text -> ValueAndSpan = } } + +// RFC 3339 timestamps. #inst "1985-04-12T23:20:50.52Z" +// We accept an arbitrary depth of decimals. +// Note that we discard the timezone information -- all times are translated to UTC. +pub inst_string -> DateTime = + "#inst" whitespace+ "\"" d:$( [0-9]*<4> "-" [0-2][0-9] "-" [0-3][0-9] + "T" + [0-2][0-9] ":" [0-5][0-9] ":" [0-6][0-9] + ("." [0-9]+)? + "Z" / (("+" / "-") [0-2][0-9] ":" [0-5][0-9]) + ) + "\"" {? + DateTime::parse_from_rfc3339(d) + .map(|t| t.with_timezone(&UTC)) + .map_err(|_| "invalid datetime") // Oh, rustpeg. + } + +pub inst_micros -> DateTime = + "#instmicros" whitespace+ d:$( digit+ ) { + let micros = d.parse::().unwrap(); + let seconds: i64 = micros / 1000000; + let nanos: u32 = ((micros % 1000000).abs() as u32) * 1000; + UTC.timestamp(seconds, nanos) + } + +pub inst_millis -> DateTime = + "#instmillis" whitespace+ d:$( digit+ ) { + let millis = d.parse::().unwrap(); + let seconds: i64 = millis / 1000; + let nanos: u32 = ((millis % 1000).abs() as u32) * 1000000; + UTC.timestamp(seconds, nanos) + } + +pub inst -> ValueAndSpan = + start:#position t:(inst_millis / inst_micros / inst_string) end:#position { + ValueAndSpan { + inner: SpannedValue::Instant(t), + span: Span::new(start, end) + } + } + +pub uuid_string -> Uuid = + "\"" u:$( [a-f0-9]*<8> "-" [a-f0-9]*<4> "-" [a-f0-9]*<4> "-" [a-f0-9]*<4> "-" [a-f0-9]*<12> ) "\"" { + Uuid::parse_str(u).expect("this is a valid UUID string") + } + +pub uuid -> ValueAndSpan = + start:#position "#uuid" whitespace+ u:(uuid_string) end:#position { + ValueAndSpan { + inner: SpannedValue::Uuid(u), + span: Span::new(start, end) + } + } + namespace_divider = "." namespace_separator = "/" @@ -220,7 +280,7 @@ pub map -> ValueAndSpan = // It's important that float comes before integer or the parser assumes that // floats are integers and fails to parse pub value -> ValueAndSpan = - __ v:(nil / nan / infinity / boolean / float / octalinteger / hexinteger / basedinteger / bigint / integer / text / keyword / symbol / list / vector / map / set) __ { + __ v:(nil / nan / infinity / boolean / float / octalinteger / hexinteger / basedinteger / inst / uuid / bigint / integer / text / keyword / symbol / list / vector / map / set) __ { v } diff --git a/edn/src/lib.rs b/edn/src/lib.rs index 4536b6e8..dce9a9c6 100644 --- a/edn/src/lib.rs +++ b/edn/src/lib.rs @@ -8,10 +8,12 @@ // 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 itertools; extern crate num; extern crate ordered_float; extern crate pretty; +extern crate uuid; pub mod symbols; pub mod types; @@ -23,8 +25,20 @@ pub mod parse { include!(concat!(env!("OUT_DIR"), "/edn.rs")); } +// Re-export the types we use. +pub use chrono::{DateTime, UTC}; pub use num::BigInt; pub use ordered_float::OrderedFloat; +pub use uuid::Uuid; + +// Export from our modules. pub use parse::ParseError; -pub use types::{Span, SpannedValue, Value, ValueAndSpan}; +pub use types::{ + FromMicros, + Span, + SpannedValue, + ToMicros, + Value, + ValueAndSpan, +}; pub use symbols::{Keyword, NamespacedKeyword, PlainSymbol, NamespacedSymbol}; diff --git a/edn/src/pretty_print.rs b/edn/src/pretty_print.rs index 949e158b..e52cdb5f 100644 --- a/edn/src/pretty_print.rs +++ b/edn/src/pretty_print.rs @@ -69,6 +69,7 @@ impl Value { Value::NamespacedKeyword(ref v) => pp.text(":").append(v.namespace.as_ref()).append("/").append(v.name.as_ref()), Value::Keyword(ref v) => pp.text(":").append(v.0.as_ref()), Value::Text(ref v) => pp.text("\"").append(v.as_ref()).append("\""), + Value::Uuid(ref u) => pp.text("#uuid \"").append(u.hyphenated().to_string()).append("\""), _ => pp.text(self.to_string()) } } diff --git a/edn/src/types.rs b/edn/src/types.rs index c511c480..241664ec 100644 --- a/edn/src/types.rs +++ b/edn/src/types.rs @@ -15,9 +15,16 @@ use std::cmp::{Ordering, Ord, PartialOrd}; use std::fmt::{Display, Formatter}; use std::f64; -use symbols; +use chrono::{ + DateTime, + TimeZone, // For UTC::timestamp. The compiler incorrectly complains that this is unused. + UTC, +}; use num::BigInt; use ordered_float::OrderedFloat; +use uuid::Uuid; + +use symbols; /// Value represents one of the allowed values in an EDN string. #[derive(PartialEq, Eq, Hash, Clone, Debug)] @@ -25,9 +32,11 @@ pub enum Value { Nil, Boolean(bool), Integer(i64), + Instant(DateTime), BigInteger(BigInt), Float(OrderedFloat), Text(String), + Uuid(Uuid), PlainSymbol(symbols::PlainSymbol), NamespacedSymbol(symbols::NamespacedSymbol), Keyword(symbols::Keyword), @@ -52,9 +61,11 @@ pub enum SpannedValue { Nil, Boolean(bool), Integer(i64), + Instant(DateTime), BigInteger(BigInt), Float(OrderedFloat), Text(String), + Uuid(Uuid), PlainSymbol(symbols::PlainSymbol), NamespacedSymbol(symbols::NamespacedSymbol), Keyword(symbols::Keyword), @@ -123,9 +134,11 @@ impl From for Value { SpannedValue::Nil => Value::Nil, SpannedValue::Boolean(v) => Value::Boolean(v), SpannedValue::Integer(v) => Value::Integer(v), + SpannedValue::Instant(v) => Value::Instant(v), SpannedValue::BigInteger(v) => Value::BigInteger(v), SpannedValue::Float(v) => Value::Float(v), SpannedValue::Text(v) => Value::Text(v), + SpannedValue::Uuid(v) => Value::Uuid(v), SpannedValue::PlainSymbol(v) => Value::PlainSymbol(v), SpannedValue::NamespacedSymbol(v) => Value::NamespacedSymbol(v), SpannedValue::Keyword(v) => Value::Keyword(v), @@ -268,9 +281,11 @@ macro_rules! def_common_value_methods { def_is!(is_nil, $t::Nil); def_is!(is_boolean, $t::Boolean(_)); def_is!(is_integer, $t::Integer(_)); + def_is!(is_instant, $t::Instant(_)); def_is!(is_big_integer, $t::BigInteger(_)); def_is!(is_float, $t::Float(_)); def_is!(is_text, $t::Text(_)); + def_is!(is_uuid, $t::Uuid(_)); def_is!(is_symbol, $t::PlainSymbol(_)); def_is!(is_namespaced_symbol, $t::NamespacedSymbol(_)); def_is!(is_keyword, $t::Keyword(_)); @@ -288,11 +303,13 @@ macro_rules! def_common_value_methods { def_as!(as_boolean, $t::Boolean, bool,); def_as!(as_integer, $t::Integer, i64,); + def_as!(as_instant, $t::Instant, DateTime,); def_as!(as_float, $t::Float, f64, |v: OrderedFloat| v.into_inner()); def_as_ref!(as_big_integer, $t::BigInteger, BigInt); def_as_ref!(as_ordered_float, $t::Float, OrderedFloat); def_as_ref!(as_text, $t::Text, String); + def_as_ref!(as_uuid, $t::Uuid, Uuid); def_as_ref!(as_symbol, $t::PlainSymbol, symbols::PlainSymbol); def_as_ref!(as_namespaced_symbol, $t::NamespacedSymbol, symbols::NamespacedSymbol); def_as_ref!(as_keyword, $t::Keyword, symbols::Keyword); @@ -304,10 +321,12 @@ macro_rules! def_common_value_methods { def_into!(into_boolean, $t::Boolean, bool,); def_into!(into_integer, $t::Integer, i64,); + def_into!(into_instant, $t::Instant, DateTime,); def_into!(into_big_integer, $t::BigInteger, BigInt,); def_into!(into_ordered_float, $t::Float, OrderedFloat,); def_into!(into_float, $t::Float, f64, |v: OrderedFloat| v.into_inner()); def_into!(into_text, $t::Text, String,); + def_into!(into_uuid, $t::Uuid, Uuid,); def_into!(into_symbol, $t::PlainSymbol, symbols::PlainSymbol,); def_into!(into_namespaced_symbol, $t::NamespacedSymbol, symbols::NamespacedSymbol,); def_into!(into_keyword, $t::Keyword, symbols::Keyword,); @@ -336,15 +355,17 @@ macro_rules! def_common_value_methods { $t::Integer(_) => 2, $t::BigInteger(_) => 3, $t::Float(_) => 4, - $t::Text(_) => 5, - $t::PlainSymbol(_) => 6, - $t::NamespacedSymbol(_) => 7, - $t::Keyword(_) => 8, - $t::NamespacedKeyword(_) => 9, - $t::Vector(_) => 10, - $t::List(_) => 11, - $t::Set(_) => 12, - $t::Map(_) => 13, + $t::Instant(_) => 5, + $t::Text(_) => 6, + $t::Uuid(_) => 7, + $t::PlainSymbol(_) => 8, + $t::NamespacedSymbol(_) => 9, + $t::Keyword(_) => 10, + $t::NamespacedKeyword(_) => 11, + $t::Vector(_) => 12, + $t::List(_) => 13, + $t::Set(_) => 14, + $t::Map(_) => 15, } } @@ -353,9 +374,11 @@ macro_rules! def_common_value_methods { $t::Nil => false, $t::Boolean(_) => false, $t::Integer(_) => false, + $t::Instant(_) => false, $t::BigInteger(_) => false, $t::Float(_) => false, $t::Text(_) => false, + $t::Uuid(_) => false, $t::PlainSymbol(_) => false, $t::NamespacedSymbol(_) => false, $t::Keyword(_) => false, @@ -389,9 +412,11 @@ macro_rules! def_common_value_ord { (&$t::Nil, &$t::Nil) => Ordering::Equal, (&$t::Boolean(a), &$t::Boolean(b)) => b.cmp(&a), (&$t::Integer(a), &$t::Integer(b)) => b.cmp(&a), + (&$t::Instant(a), &$t::Instant(b)) => b.cmp(&a), (&$t::BigInteger(ref a), &$t::BigInteger(ref b)) => b.cmp(a), (&$t::Float(ref a), &$t::Float(ref b)) => b.cmp(a), (&$t::Text(ref a), &$t::Text(ref b)) => b.cmp(a), + (&$t::Uuid(ref a), &$t::Uuid(ref b)) => b.cmp(a), (&$t::PlainSymbol(ref a), &$t::PlainSymbol(ref b)) => b.cmp(a), (&$t::NamespacedSymbol(ref a), &$t::NamespacedSymbol(ref b)) => b.cmp(a), (&$t::Keyword(ref a), &$t::Keyword(ref b)) => b.cmp(a), @@ -414,6 +439,7 @@ macro_rules! def_common_value_display { $t::Nil => write!($f, "nil"), $t::Boolean(v) => write!($f, "{}", v), $t::Integer(v) => write!($f, "{}", v), + $t::Instant(v) => write!($f, "{}", v), $t::BigInteger(ref v) => write!($f, "{}N", v), // TODO: make sure float syntax is correct. $t::Float(ref v) => { @@ -429,6 +455,7 @@ macro_rules! def_common_value_display { } // TODO: EDN escaping. $t::Text(ref v) => write!($f, "\"{}\"", v), + $t::Uuid(ref u) => write!($f, "#uuid \"{}\"", u.hyphenated().to_string()), $t::PlainSymbol(ref v) => v.fmt($f), $t::NamespacedSymbol(ref v) => v.fmt($f), $t::Keyword(ref v) => v.fmt($f), @@ -518,8 +545,29 @@ impl Display for ValueAndSpan { } } +pub trait FromMicros { + fn from_micros(ts: i64) -> Self; +} + +impl FromMicros for DateTime { + fn from_micros(ts: i64) -> Self { + UTC.timestamp(ts / 100_000, ((ts % 100_000).abs() as u32) * 1_000) + } +} + +pub trait ToMicros { + fn to_micros(&self) -> i64; +} + +impl ToMicros for DateTime { + fn to_micros(&self) -> i64 { + (self.timestamp() * 100_000) + (self.timestamp_subsec_micros() as i64) + } +} + #[cfg(test)] mod test { + extern crate chrono; extern crate ordered_float; extern crate num; @@ -532,9 +580,21 @@ mod test { use parse; + use chrono::{ + DateTime, + TimeZone, + UTC, + }; use num::BigInt; use ordered_float::OrderedFloat; + #[test] + fn test_micros_roundtrip() { + let ts_micros: i64 = 1493399581314000; + let dt = DateTime::::from_micros(ts_micros); + assert_eq!(dt.to_micros(), ts_micros); + } + #[test] fn test_value_from() { assert_eq!(Value::from_float(42f64), Value::Float(OrderedFloat::from(42f64))); diff --git a/edn/tests/tests.rs b/edn/tests/tests.rs index c3c9b5f2..918bdc11 100644 --- a/edn/tests/tests.rs +++ b/edn/tests/tests.rs @@ -8,9 +8,11 @@ // 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 edn; extern crate num; extern crate ordered_float; +extern crate uuid; use std::collections::{BTreeSet, BTreeMap, LinkedList}; use std::iter::FromIterator; @@ -21,7 +23,16 @@ use num::traits::{Zero, One}; use ordered_float::OrderedFloat; use edn::parse::{self, ParseError}; -use edn::types::{Value, SpannedValue, Span, ValueAndSpan}; +use edn::types::{ + Value, + ValueAndSpan, + Span, + SpannedValue, +}; +use chrono::{ + TimeZone, + UTC, +}; use edn::symbols; use edn::utils; @@ -55,7 +66,7 @@ macro_rules! fn_parse_into_value { } // These look exactly like their `parse::foo` counterparts, but -// automatically convert the returned result into Value. Use `parse:foo` +// automatically convert the returned result into Value. Use `parse::foo` // if you want the original ValueAndSpan instance. fn_parse_into_value!(nil); fn_parse_into_value!(nan); @@ -242,6 +253,24 @@ fn test_span_integer() { }); } +#[test] +fn test_uuid() { + assert!(parse::uuid("#uuid\"550e8400-e29b-41d4-a716-446655440000\"").is_err()); // No whitespace. + assert!(parse::uuid("#uuid \"z50e8400-e29b-41d4-a716-446655440000\"").is_err()); // Not hex. + assert!(parse::uuid("\"z50e8400-e29b-41d4-a716-446655440000\"").is_err()); // No tag. + assert!(parse::uuid("#uuid \"aaaaaaaae29b-41d4-a716-446655440000\"").is_err()); // Hyphens. + assert!(parse::uuid("#uuid \"aaaaaaaa-e29b-41d4-a716-446655440\"").is_err()); // Truncated. + assert!(parse::uuid("#uuid \"A50e8400-e29b-41d4-a716-446655440000\"").is_err()); // Capital. + + let expected = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000") + .expect("valid UUID"); + let actual = parse::uuid("#uuid \"550e8400-e29b-41d4-a716-446655440000\"") + .expect("parse success") + .inner + .into(); + assert_eq!(self::Value::Uuid(expected), actual); +} + #[test] fn test_bigint() { use self::Value::*; @@ -394,6 +423,12 @@ fn test_value() { assert_eq!(value("(1)").unwrap(), List(LinkedList::from_iter(vec![Integer(1)]))); assert_eq!(value("#{1}").unwrap(), Set(BTreeSet::from_iter(vec![Integer(1)]))); assert_eq!(value("{1 2}").unwrap(), Map(BTreeMap::from_iter(vec![(Integer(1), Integer(2))]))); + assert_eq!(value("#uuid \"e43c6f3e-3123-49b7-8098-9b47a7bc0fa4\"").unwrap(), + Uuid(uuid::Uuid::parse_str("e43c6f3e-3123-49b7-8098-9b47a7bc0fa4").unwrap())); + assert_eq!(value("#instmillis 1493410985187").unwrap(), Instant(UTC.timestamp(1493410985, 187000000))); + assert_eq!(value("#instmicros 1493410985187123").unwrap(), Instant(UTC.timestamp(1493410985, 187123000))); + assert_eq!(value("#inst \"2017-04-28T20:23:05.187Z\"").unwrap(), + Instant(UTC.timestamp(1493410985, 187000000))); } #[test] diff --git a/query-algebrizer/src/clauses/mod.rs b/query-algebrizer/src/clauses/mod.rs index 917947eb..5b47beff 100644 --- a/query-algebrizer/src/clauses/mod.rs +++ b/query-algebrizer/src/clauses/mod.rs @@ -568,7 +568,7 @@ impl ConjoiningClauses { fn constrain_to_tx(&mut self, tx: &PatternNonValuePlace) { match *tx { PatternNonValuePlace::Placeholder => (), - _ => unimplemented!(), // TODO + _ => unimplemented!(), // TODO: #440. } } @@ -874,4 +874,4 @@ mod tests { assert!(alias_zero != alias_one); assert!(alias_one != alias_two); } -} \ No newline at end of file +} diff --git a/query-algebrizer/src/clauses/pattern.rs b/query-algebrizer/src/clauses/pattern.rs index 65bcfd01..f9c084a3 100644 --- a/query-algebrizer/src/clauses/pattern.rs +++ b/query-algebrizer/src/clauses/pattern.rs @@ -81,7 +81,7 @@ impl ConjoiningClauses { // Sorry for the duplication; Rust makes it a pain to abstract this. // The transaction part of a pattern must be an entid, variable, or placeholder. - self.constrain_to_tx(&pattern.tx); + self.constrain_to_tx(&pattern.tx); // See #440. self.constrain_to_ref(&pattern.entity); self.constrain_to_ref(&pattern.attribute); diff --git a/query-algebrizer/src/clauses/resolve.rs b/query-algebrizer/src/clauses/resolve.rs index aba0ba35..9cf27807 100644 --- a/query-algebrizer/src/clauses/resolve.rs +++ b/query-algebrizer/src/clauses/resolve.rs @@ -54,6 +54,8 @@ impl ConjoiningClauses { SrcVar(_) | Constant(NonIntegerConstant::Boolean(_)) | Constant(NonIntegerConstant::Text(_)) | + Constant(NonIntegerConstant::Uuid(_)) | + Constant(NonIntegerConstant::Instant(_)) | // Instants are covered elsewhere. Constant(NonIntegerConstant::BigInteger(_)) => { self.mark_known_empty(EmptyBecause::NonNumericArgument); bail!(ErrorKind::NonNumericArgument(function.clone(), position)); @@ -80,6 +82,8 @@ impl ConjoiningClauses { Constant(NonIntegerConstant::Boolean(val)) => Ok(QueryValue::TypedValue(TypedValue::Boolean(val))), Constant(NonIntegerConstant::Float(f)) => Ok(QueryValue::TypedValue(TypedValue::Double(f))), Constant(NonIntegerConstant::Text(s)) => Ok(QueryValue::TypedValue(TypedValue::typed_string(s.as_str()))), + Constant(NonIntegerConstant::Uuid(u)) => Ok(QueryValue::TypedValue(TypedValue::Uuid(u))), + Constant(NonIntegerConstant::Instant(u)) => Ok(QueryValue::TypedValue(TypedValue::Instant(u))), Constant(NonIntegerConstant::BigInteger(_)) => unimplemented!(), SrcVar(_) => unimplemented!(), } diff --git a/query-parser/Cargo.toml b/query-parser/Cargo.toml index 7dde1314..69488bbd 100644 --- a/query-parser/Cargo.toml +++ b/query-parser/Cargo.toml @@ -11,6 +11,9 @@ matches = "0.1" [dependencies.edn] path = "../edn" +[dependencies.mentat_core] + path = "../core" + [dependencies.mentat_parser_utils] path = "../parser-utils" diff --git a/query-parser/tests/find_tests.rs b/query-parser/tests/find_tests.rs index 7e577fdd..be8ddd6c 100644 --- a/query-parser/tests/find_tests.rs +++ b/query-parser/tests/find_tests.rs @@ -9,9 +9,12 @@ // specific language governing permissions and limitations under the License. extern crate edn; +extern crate mentat_core; extern crate mentat_query; extern crate mentat_query_parser; +use std::rc::Rc; + use edn::{ NamespacedKeyword, PlainSymbol, @@ -23,6 +26,7 @@ use mentat_query::{ FindSpec, FnArg, Limit, + NonIntegerConstant, Order, OrJoin, OrWhereClause, @@ -267,3 +271,17 @@ fn can_parse_limit() { let variable_without_in = "[:find ?x :where [?x :foo/baz ?y] :limit ?limit]"; assert!(parse_find_string(variable_without_in).is_err()); } + +#[test] +fn can_parse_uuid() { + let expected = edn::Uuid::parse_str("4cb3f828-752d-497a-90c9-b1fd516d5644").expect("valid uuid"); + let s = "[:find ?x :where [?x :foo/baz #uuid \"4cb3f828-752d-497a-90c9-b1fd516d5644\"]]"; + assert_eq!(parse_find_string(s).expect("parsed").where_clauses.pop().expect("a where clause"), + WhereClause::Pattern( + Pattern::new(None, + PatternNonValuePlace::Variable(Variable::from_valid_name("?x")), + PatternNonValuePlace::Ident(Rc::new(NamespacedKeyword::new("foo", "baz"))), + PatternValuePlace::Constant(NonIntegerConstant::Uuid(expected)), + PatternNonValuePlace::Placeholder) + .expect("valid pattern"))); +} diff --git a/query-translator/src/translate.rs b/query-translator/src/translate.rs index 17c5d00e..0dfd28a1 100644 --- a/query-translator/src/translate.rs +++ b/query-translator/src/translate.rs @@ -8,8 +8,6 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. -use std::rc::Rc; - use mentat_core::{ SQLValueType, TypedValue, diff --git a/query-translator/tests/translate.rs b/query-translator/tests/translate.rs index a13c2fa7..1342acf9 100644 --- a/query-translator/tests/translate.rs +++ b/query-translator/tests/translate.rs @@ -76,8 +76,8 @@ fn prepopulated_schema() -> Schema { prepopulated_typed_schema(ValueType::String) } -fn make_arg(name: &'static str, value: &'static str) -> (String, Rc) { - (name.to_string(), Rc::new(value.to_string())) +fn make_arg(name: &'static str, value: &'static str) -> (String, Rc) { + (name.to_string(), Rc::new(mentat_sql::Value::Text(value.to_string()))) } #[test] @@ -550,4 +550,4 @@ fn test_complex_nested_or_join_type_projection() { AS `c00` \ LIMIT 1"); assert_eq!(args, vec![]); -} \ No newline at end of file +} diff --git a/query/src/lib.rs b/query/src/lib.rs index cf102545..1f2a6d3e 100644 --- a/query/src/lib.rs +++ b/query/src/lib.rs @@ -40,8 +40,18 @@ use std::collections::{ use std::fmt; use std::rc::Rc; -use edn::{BigInt, OrderedFloat}; -pub use edn::{NamespacedKeyword, PlainSymbol}; +use edn::{ + BigInt, + DateTime, + OrderedFloat, + Uuid, + UTC, +}; + +pub use edn::{ + NamespacedKeyword, + PlainSymbol, +}; use mentat_core::{ TypedValue, @@ -178,6 +188,8 @@ pub enum NonIntegerConstant { BigInteger(BigInt), Float(OrderedFloat), Text(Rc), + Instant(DateTime), + Uuid(Uuid), } impl NonIntegerConstant { @@ -187,6 +199,8 @@ impl NonIntegerConstant { NonIntegerConstant::Boolean(v) => TypedValue::Boolean(v), NonIntegerConstant::Float(v) => TypedValue::Double(v), NonIntegerConstant::Text(v) => TypedValue::String(v), + NonIntegerConstant::Instant(v) => TypedValue::Instant(v), + NonIntegerConstant::Uuid(v) => TypedValue::Uuid(v), } } } @@ -310,10 +324,22 @@ impl FromValue for PatternValuePlace { Some(PatternValuePlace::Constant(NonIntegerConstant::Float(x))), edn::SpannedValue::BigInteger(ref x) => Some(PatternValuePlace::Constant(NonIntegerConstant::BigInteger(x.clone()))), + edn::SpannedValue::Instant(x) => + Some(PatternValuePlace::Constant(NonIntegerConstant::Instant(x))), edn::SpannedValue::Text(ref x) => // TODO: intern strings. #398. Some(PatternValuePlace::Constant(NonIntegerConstant::Text(Rc::new(x.clone())))), - _ => None, + edn::SpannedValue::Uuid(ref u) => + Some(PatternValuePlace::Constant(NonIntegerConstant::Uuid(u.clone()))), + + // These don't appear in queries. + edn::SpannedValue::Nil => None, + edn::SpannedValue::NamespacedSymbol(_) => None, + edn::SpannedValue::Keyword(_) => None, + edn::SpannedValue::Map(_) => None, + edn::SpannedValue::List(_) => None, + edn::SpannedValue::Set(_) => None, + edn::SpannedValue::Vector(_) => None, } } } @@ -761,4 +787,4 @@ impl ContainsVariables for Pattern { acc_ref(acc, v) } } -} \ No newline at end of file +} diff --git a/sql/Cargo.toml b/sql/Cargo.toml index 868d07e4..abca4fa2 100644 --- a/sql/Cargo.toml +++ b/sql/Cargo.toml @@ -7,5 +7,10 @@ workspace = ".." error-chain = "0.8.1" ordered-float = "0.4.0" +[dependencies.rusqlite] +version = "0.10.1" +# System sqlite might be very old. +features = ["bundled", "limits"] + [dependencies.mentat_core] path = "../core" diff --git a/sql/src/lib.rs b/sql/src/lib.rs index 292ddd3d..7b926de4 100644 --- a/sql/src/lib.rs +++ b/sql/src/lib.rs @@ -11,13 +11,20 @@ #[macro_use] extern crate error_chain; extern crate ordered_float; +extern crate rusqlite; + extern crate mentat_core; use std::rc::Rc; use ordered_float::OrderedFloat; -use mentat_core::TypedValue; +use mentat_core::{ + ToMicros, + TypedValue, +}; + +pub use rusqlite::types::Value; error_chain! { types { @@ -47,7 +54,7 @@ pub struct SQLQuery { pub sql: String, /// These will eventually perhaps be rusqlite `ToSql` instances. - pub args: Vec<(String, Rc)>, + pub args: Vec<(String, Rc)>, } /// Gratefully based on Diesel's QueryBuilder trait: @@ -88,7 +95,7 @@ pub struct SQLiteQueryBuilder { arg_prefix: String, arg_counter: i64, - args: Vec<(String, Rc)>, + args: Vec<(String, Rc)>, } impl SQLiteQueryBuilder { @@ -105,7 +112,7 @@ impl SQLiteQueryBuilder { } } - fn push_static_arg(&mut self, val: Rc) { + fn push_static_arg(&mut self, val: Rc) { let arg = format!("{}{}", self.arg_prefix, self.arg_counter); self.arg_counter = self.arg_counter + 1; self.push_named_arg(arg.as_str()); @@ -136,11 +143,25 @@ impl QueryBuilder for SQLiteQueryBuilder { &Boolean(v) => self.push_sql(if v { "1" } else { "0" }), &Long(v) => self.push_sql(v.to_string().as_str()), &Double(OrderedFloat(v)) => self.push_sql(v.to_string().as_str()), - - // These are both `Rc`. We can just clone an `Rc`, but we - // must make a new single `String`, wrapped in an `Rc`, for keywords. - &String(ref s) => self.push_static_arg(s.clone()), - &Keyword(ref s) => self.push_static_arg(Rc::new(s.as_ref().to_string())), + &Instant(dt) => { + self.push_sql(format!("{}", dt.to_micros()).as_str()); // TODO: argument instead? + }, + &Uuid(ref u) => { + // Get a byte array. + let bytes = u.as_bytes().clone(); + let v = Rc::new(rusqlite::types::Value::Blob(bytes.to_vec())); + self.push_static_arg(v); + }, + // These are both `Rc`. Unfortunately, we can't use that fact when + // turning these into rusqlite Values. + &String(ref s) => { + let v = Rc::new(rusqlite::types::Value::Text(s.as_ref().clone())); + self.push_static_arg(v); + }, + &Keyword(ref s) => { + let v = Rc::new(rusqlite::types::Value::Text(s.as_ref().to_string())); + self.push_static_arg(v); + }, } Ok(()) } @@ -180,6 +201,10 @@ impl QueryBuilder for SQLiteQueryBuilder { mod tests { use super::*; + fn string_arg(s: &str) -> Rc { + Rc::new(rusqlite::types::Value::Text(s.to_string())) + } + #[test] fn test_sql() { let mut s = SQLiteQueryBuilder::new(); @@ -188,14 +213,14 @@ mod tests { s.push_sql(" WHERE "); s.push_identifier("bar").unwrap(); s.push_sql(" = "); - s.push_static_arg(Rc::new("frobnicate".to_string())); + s.push_static_arg(string_arg("frobnicate")); s.push_sql(" OR "); - s.push_static_arg(Rc::new("swoogle".to_string())); + s.push_static_arg(string_arg("swoogle")); let q = s.finish(); assert_eq!(q.sql.as_str(), "SELECT `foo` WHERE `bar` = $v0 OR $v1"); assert_eq!(q.args, - vec![("$v0".to_string(), Rc::new("frobnicate".to_string())), - ("$v1".to_string(), Rc::new("swoogle".to_string()))]); + vec![("$v0".to_string(), string_arg("frobnicate")), + ("$v1".to_string(), string_arg("swoogle"))]); } } diff --git a/tests/query.rs b/tests/query.rs index 389bc94a..cdc88ce9 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -8,15 +8,22 @@ // 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 time; extern crate mentat; extern crate mentat_core; extern crate mentat_db; +use std::str::FromStr; + +use chrono::FixedOffset; + use mentat_core::{ TypedValue, ValueType, + UTC, + Uuid, }; use mentat::{ @@ -28,6 +35,8 @@ use mentat::{ q_once, }; +use mentat::conn::Conn; + use mentat::errors::{ Error, ErrorKind, @@ -46,7 +55,7 @@ fn test_rel() { let end = time::PreciseTime::now(); // This will need to change each time we add a default ident. - assert_eq!(37, results.len()); + assert_eq!(39, results.len()); // Every row is a pair of a Ref and a Keyword. if let QueryResults::Rel(ref rel) = results { @@ -154,7 +163,7 @@ fn test_coll() { .expect("Query failed"); let end = time::PreciseTime::now(); - assert_eq!(37, results.len()); + assert_eq!(39, results.len()); if let QueryResults::Coll(ref coll) = results { assert!(coll.iter().all(|item| item.matches_type(ValueType::Ref))); @@ -204,3 +213,44 @@ fn test_unbound_inputs() { _ => panic!("Expected unbound variables."), } } + +#[test] +fn test_instants_and_uuids() { + // We assume, perhaps foolishly, that the clocks on test machines won't lose more than an + // hour while this test is running. + let start = UTC::now() + FixedOffset::west(60 * 60); + + 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 "s" :db/ident :foo/uuid] + [:db/add "s" :db/valueType :db.type/uuid] + [:db/add "s" :db/cardinality :db.cardinality/one] + ]"#).unwrap(); + conn.transact(&mut c, r#"[ + [:db/add "u" :foo/uuid #uuid "cf62d552-6569-4d1b-b667-04703041dfc4"] + ]"#).unwrap(); + + // We don't yet support getting the tx from a pattern (#440), so run wild. + let r = conn.q_once(&mut c, + r#"[:find [?x ?u ?when] + :where [?x :foo/uuid ?u] + [?tx :db/txInstant ?when]]"#, None); + match r { + Result::Ok(QueryResults::Tuple(Some(vals))) => { + let mut vals = vals.into_iter(); + match (vals.next(), vals.next(), vals.next(), vals.next()) { + (Some(TypedValue::Ref(e)), + Some(TypedValue::Uuid(u)), + Some(TypedValue::Instant(t)), + None) => { + assert!(e > 39); // There are at least this many entities in the store. + assert_eq!(Ok(u), Uuid::from_str("cf62d552-6569-4d1b-b667-04703041dfc4")); + assert!(t > start); + }, + _ => panic!("Unexpected results."), + } + }, + _ => panic!("Expected query to work."), + } +} diff --git a/tx-parser/tests/parser.rs b/tx-parser/tests/parser.rs index 7abd2819..57994e8a 100644 --- a/tx-parser/tests/parser.rs +++ b/tx-parser/tests/parser.rs @@ -25,6 +25,33 @@ use mentat_tx::entities::{ }; use mentat_tx_parser::Tx; +#[test] +fn test_float_and_uuid() { + let expected_uuid = edn::Uuid::parse_str("267bab92-ee39-4ca2-b7f0-1163a85af1fb").expect("valid uuid"); + let input = r#" +[[:db/add 101 :test/a #uuid "267bab92-ee39-4ca2-b7f0-1163a85af1fb"] + [:db/add 102 :test/b #f NaN]] + "#; + let edn = parse::value(input).expect("to parse test input"); + + let result = Tx::parse(edn); + assert_eq!(result.unwrap(), + vec![ + Entity::AddOrRetract { + op: OpType::Add, + e: EntidOrLookupRefOrTempId::Entid(Entid::Entid(101)), + a: Entid::Ident(NamespacedKeyword::new("test", "a")), + v: AtomOrLookupRefOrVectorOrMapNotation::Atom(edn::ValueAndSpan::new(edn::SpannedValue::Uuid(expected_uuid), edn::Span(23, 67))), + }, + Entity::AddOrRetract { + op: OpType::Add, + e: EntidOrLookupRefOrTempId::Entid(Entid::Entid(102)), + a: Entid::Ident(NamespacedKeyword::new("test", "b")), + v: AtomOrLookupRefOrVectorOrMapNotation::Atom(edn::ValueAndSpan::new(edn::SpannedValue::Float(edn::OrderedFloat(std::f64::NAN)), edn::Span(91, 97))), + }, + ]); +} + #[test] fn test_entities() { let input = r#"