UUIDs and instants. Fixes #44, #45, #426, #427. (#438) r=nalexander

* Pre: unused import in translate.rs.

* Part 2: take a dependency on rusqlite for query arguments.

* Part 1: flatten V2 schema into V1. Add UUID and URI.

Bump expected ident and bootstrap datom count in tests.

* Part 5: parse edn::Value::Uuid.

* Part 3: extend ValueType and TypedValue to include Uuid.

* Part 4: add Uuid to query arguments.

* Part 6: extend db to support Uuid.

* Part 8: add a tx-parser test for #f NaN and #uuid.

* Part 7: parse and algebrize UUIDs in queries.

* Part 1: parse #inst in EDN and throughout query engine.

* Part 3: handle instants in db.

* Part 2: instants never matches integers in queries.

* Part 4: use DateTime for tx_instants.

* Add a test for adding and querying UUIDs and instants.

* Review comments.
This commit is contained in:
Richard Newman 2017-04-28 20:11:55 -07:00 committed by GitHub
parent bd389d2f0d
commit daca8def57
28 changed files with 495 additions and 126 deletions

View file

@ -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"

View file

@ -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<f64>),
Instant(DateTime<UTC>),
// TODO: &str throughout?
String(Rc<String>),
Keyword(Rc<NamespacedKeyword>),
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,
}
}
}

View file

@ -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<Vec<Value>>
}
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<Entity> {
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.

View file

@ -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<T>(uri: T) -> rusqlite::Result<rusqlite::Connection> 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<i32> {
pub fn create_current_version(conn: &mut rusqlite::Connection) -> Result<DB> {
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::<UTC>::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<TypedValue> {
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,

View file

@ -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.

View file

@ -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> {
UTC::now()
}

View file

@ -115,12 +115,14 @@ pub fn update_schema_map_from_entid_triples<U>(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)))
}
},

View file

@ -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)),

View file

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

View file

@ -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<UTC>,
/// A map from string literal tempid to resolved or allocated entid.
///

View file

@ -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"

View file

@ -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<UTC> =
"#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<UTC> =
"#instmicros" whitespace+ d:$( digit+ ) {
let micros = d.parse::<i64>().unwrap();
let seconds: i64 = micros / 1000000;
let nanos: u32 = ((micros % 1000000).abs() as u32) * 1000;
UTC.timestamp(seconds, nanos)
}
pub inst_millis -> DateTime<UTC> =
"#instmillis" whitespace+ d:$( digit+ ) {
let millis = d.parse::<i64>().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
}

View file

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

View file

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

View file

@ -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<UTC>),
BigInteger(BigInt),
Float(OrderedFloat<f64>),
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<UTC>),
BigInteger(BigInt),
Float(OrderedFloat<f64>),
Text(String),
Uuid(Uuid),
PlainSymbol(symbols::PlainSymbol),
NamespacedSymbol(symbols::NamespacedSymbol),
Keyword(symbols::Keyword),
@ -123,9 +134,11 @@ impl From<SpannedValue> 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<UTC>,);
def_as!(as_float, $t::Float, f64, |v: OrderedFloat<f64>| v.into_inner());
def_as_ref!(as_big_integer, $t::BigInteger, BigInt);
def_as_ref!(as_ordered_float, $t::Float, OrderedFloat<f64>);
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<UTC>,);
def_into!(into_big_integer, $t::BigInteger, BigInt,);
def_into!(into_ordered_float, $t::Float, OrderedFloat<f64>,);
def_into!(into_float, $t::Float, f64, |v: OrderedFloat<f64>| 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<UTC> {
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<UTC> {
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::<UTC>::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)));

View file

@ -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]

View file

@ -568,7 +568,7 @@ impl ConjoiningClauses {
fn constrain_to_tx(&mut self, tx: &PatternNonValuePlace) {
match *tx {
PatternNonValuePlace::Placeholder => (),
_ => unimplemented!(), // TODO
_ => unimplemented!(), // TODO: #440.
}
}

View file

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

View file

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

View file

@ -11,6 +11,9 @@ matches = "0.1"
[dependencies.edn]
path = "../edn"
[dependencies.mentat_core]
path = "../core"
[dependencies.mentat_parser_utils]
path = "../parser-utils"

View file

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

View file

@ -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,

View file

@ -76,8 +76,8 @@ fn prepopulated_schema() -> Schema {
prepopulated_typed_schema(ValueType::String)
}
fn make_arg(name: &'static str, value: &'static str) -> (String, Rc<String>) {
(name.to_string(), Rc::new(value.to_string()))
fn make_arg(name: &'static str, value: &'static str) -> (String, Rc<mentat_sql::Value>) {
(name.to_string(), Rc::new(mentat_sql::Value::Text(value.to_string())))
}
#[test]

View file

@ -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<f64>),
Text(Rc<String>),
Instant(DateTime<UTC>),
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<PatternValuePlace> 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,
}
}
}

View file

@ -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"

View file

@ -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<String>)>,
pub args: Vec<(String, Rc<rusqlite::types::Value>)>,
}
/// Gratefully based on Diesel's QueryBuilder trait:
@ -88,7 +95,7 @@ pub struct SQLiteQueryBuilder {
arg_prefix: String,
arg_counter: i64,
args: Vec<(String, Rc<String>)>,
args: Vec<(String, Rc<rusqlite::types::Value>)>,
}
impl SQLiteQueryBuilder {
@ -105,7 +112,7 @@ impl SQLiteQueryBuilder {
}
}
fn push_static_arg(&mut self, val: Rc<String>) {
fn push_static_arg(&mut self, val: Rc<rusqlite::types::Value>) {
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<String>`, 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<rusqlite::types::Value> {
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"))]);
}
}

View file

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

View file

@ -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#"