diff --git a/Cargo.toml b/Cargo.toml index e55ee8ad..56b97c0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ rustc_version = "0.1.7" [dependencies] chrono = "0.4" error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" } +lazy_static = "0.2" time = "0.1" [dependencies.rusqlite] diff --git a/db/src/lib.rs b/db/src/lib.rs index 7ff02cb9..dc1f6b7f 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -44,7 +44,7 @@ pub mod errors; mod metadata; mod schema; pub mod types; -mod internal_types; +pub mod internal_types; // pub because we need them for building entities programmatically. mod upsert_resolution; mod tx; @@ -70,7 +70,11 @@ pub use db::{ new_connection, }; -pub use tx::transact; +pub use tx::{ + transact, + transact_terms, +}; + pub use types::{ DB, PartitionMap, diff --git a/db/src/tx.rs b/db/src/tx.rs index 744d0e80..48280411 100644 --- a/db/src/tx.rs +++ b/db/src/tx.rs @@ -510,9 +510,6 @@ impl<'conn, 'a> Tx<'conn, 'a> { /// This approach is explained in https://github.com/mozilla/mentat/wiki/Transacting. // TODO: move this to the transactor layer. pub fn transact_entities(&mut self, entities: I) -> Result where I: IntoIterator { - // TODO: push these into an internal transaction report? - let mut tempids: BTreeMap = BTreeMap::default(); - // Pipeline stage 1: entities -> terms with tempids and lookup refs. let (terms_with_temp_ids_and_lookup_refs, tempid_set, lookup_ref_set) = self.entities_into_terms_with_temp_ids_and_lookup_refs(entities)?; @@ -522,9 +519,16 @@ impl<'conn, 'a> Tx<'conn, 'a> { let terms_with_temp_ids = self.resolve_lookup_refs(&lookup_ref_map, terms_with_temp_ids_and_lookup_refs)?; + self.transact_simple_terms(terms_with_temp_ids, tempid_set) + } + + pub fn transact_simple_terms(&mut self, terms: I, tempid_set: InternSet) -> Result where I: IntoIterator { + // TODO: push these into an internal transaction report? + let mut tempids: BTreeMap = BTreeMap::default(); + // Pipeline stage 3: upsert tempids -> terms without tempids or lookup refs. // Now we can collect upsert populations. - let (mut generation, inert_terms) = Generation::from(terms_with_temp_ids, &self.schema)?; + let (mut generation, inert_terms) = Generation::from(terms, &self.schema)?; // And evolve them forward. while generation.can_evolve() { @@ -685,28 +689,20 @@ impl<'conn, 'a> Tx<'conn, 'a> { } } -/// Transact the given `entities` against the given SQLite `conn`, using the given metadata. -/// If you want this work to occur inside a SQLite transaction, establish one on the connection -/// prior to calling this function. -/// -/// This approach is explained in https://github.com/mozilla/mentat/wiki/Transacting. -// TODO: move this to the transactor layer. -pub fn transact<'conn, 'a, I>( - conn: &'conn rusqlite::Connection, - mut partition_map: PartitionMap, - schema_for_mutation: &'a Schema, - schema: &'a Schema, - entities: I) -> Result<(TxReport, PartitionMap, Option)> where I: IntoIterator { - +/// Initialize a new Tx object with a new tx id and a tx instant. Kick off the SQLite conn, too. +fn start_tx<'conn, 'a>(conn: &'conn rusqlite::Connection, + mut partition_map: PartitionMap, + schema_for_mutation: &'a Schema, + schema: &'a Schema) -> Result> { let tx_instant = ::now(); // Label the transaction with the timestamp when we first see it: leading edge. let tx_id = partition_map.allocate_entid(":db.part/tx"); conn.begin_tx_application()?; - let mut tx = Tx::new(conn, partition_map, schema_for_mutation, schema, tx_id, tx_instant); - - let report = tx.transact_entities(entities)?; + Ok(Tx::new(conn, partition_map, schema_for_mutation, schema, tx_id, tx_instant)) +} +fn conclude_tx(tx: Tx, report: TxReport) -> Result<(TxReport, PartitionMap, Option)> { // If the schema has moved on, return it. let next_schema = match tx.schema_for_mutation { Cow::Borrowed(_) => None, @@ -714,3 +710,35 @@ pub fn transact<'conn, 'a, I>( }; Ok((report, tx.partition_map, next_schema)) } + +/// Transact the given `entities` against the given SQLite `conn`, using the given metadata. +/// If you want this work to occur inside a SQLite transaction, establish one on the connection +/// prior to calling this function. +/// +/// This approach is explained in https://github.com/mozilla/mentat/wiki/Transacting. +// TODO: move this to the transactor layer. +pub fn transact<'conn, 'a, I>(conn: &'conn rusqlite::Connection, + partition_map: PartitionMap, + schema_for_mutation: &'a Schema, + schema: &'a Schema, + entities: I) -> Result<(TxReport, PartitionMap, Option)> + where I: IntoIterator { + + let mut tx = start_tx(conn, partition_map, schema_for_mutation, schema)?; + let report = tx.transact_entities(entities)?; + conclude_tx(tx, report) +} + +/// Just like `transact`, but accepts lower-level inputs to allow bypassing the parser interface. +pub fn transact_terms<'conn, 'a, I>(conn: &'conn rusqlite::Connection, + partition_map: PartitionMap, + schema_for_mutation: &'a Schema, + schema: &'a Schema, + terms: I, + tempid_set: InternSet) -> Result<(TxReport, PartitionMap, Option)> + where I: IntoIterator { + + let mut tx = start_tx(conn, partition_map, schema_for_mutation, schema)?; + let report = tx.transact_simple_terms(terms, tempid_set)?; + conclude_tx(tx, report) +} diff --git a/src/conn.rs b/src/conn.rs index 53b11716..31352b42 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -30,15 +30,22 @@ use mentat_core::{ ValueType, }; +use mentat_core::intern_set::InternSet; + use mentat_db::db; use mentat_db::{ transact, + transact_terms, PartitionMap, TxReport, }; +use mentat_db::internal_types::TermWithTempIds; + use mentat_tx; +use mentat_tx::entities::TempId; + use mentat_tx_parser; use errors::*; @@ -52,6 +59,9 @@ use query::{ QueryResults, }; +use entity_builder::{ + InProgressBuilder, +}; /// Connection metadata required to query from, or apply transactions to, a Mentat store. /// @@ -113,7 +123,6 @@ pub struct InProgress<'a, 'c> { generation: u64, partition_map: PartitionMap, schema: Schema, - last_report: Option, // For now we track only the last, but we could accumulate all. } /// Represents an in-progress set of reads to the store. Just like `InProgress`, @@ -222,8 +231,27 @@ impl<'a, 'c> HasSchema for InProgress<'a, 'c> { } } + impl<'a, 'c> InProgress<'a, 'c> { - pub fn transact_entities(&mut self, entities: I) -> Result<()> where I: IntoIterator { + pub fn builder(self) -> InProgressBuilder<'a, 'c> { + InProgressBuilder::new(self) + } + + pub fn transact_terms(&mut self, terms: I, tempid_set: InternSet) -> Result where I: IntoIterator { + let (report, next_partition_map, next_schema) = transact_terms(&self.transaction, + self.partition_map.clone(), + &self.schema, + &self.schema, + terms, + tempid_set)?; + self.partition_map = next_partition_map; + if let Some(schema) = next_schema { + self.schema = schema; + } + Ok(report) + } + + pub fn transact_entities(&mut self, entities: I) -> Result where I: IntoIterator { // We clone the partition map here, rather than trying to use a Cell or using a mutable // reference, for two reasons: // 1. `transact` allocates new IDs in partitions before and while doing work that might @@ -237,26 +265,20 @@ impl<'a, 'c> InProgress<'a, 'c> { if let Some(schema) = next_schema { self.schema = schema; } - self.last_report = Some(report); - Ok(()) + Ok(report) } - pub fn transact(&mut self, transaction: &str) -> Result<()> { + pub fn transact(&mut self, transaction: &str) -> Result { let assertion_vector = edn::parse::value(transaction)?; let entities = mentat_tx_parser::Tx::parse(&assertion_vector)?; self.transact_entities(entities) } - pub fn last_report(&self) -> Option<&TxReport> { - self.last_report.as_ref() - } - - pub fn rollback(mut self) -> Result<()> { - self.last_report = None; + pub fn rollback(self) -> Result<()> { self.transaction.rollback().map_err(|e| e.into()) } - pub fn commit(self) -> Result> { + pub fn commit(self) -> Result<()> { // The mutex is taken during this entire method. let mut metadata = self.mutex.lock().unwrap(); @@ -273,11 +295,12 @@ impl<'a, 'c> InProgress<'a, 'c> { metadata.generation += 1; metadata.partition_map = self.partition_map; + if self.schema != *(metadata.schema) { metadata.schema = Arc::new(self.schema); } - Ok(self.last_report) + Ok(()) } } @@ -362,7 +385,6 @@ impl Conn { generation: current_generation, partition_map: current_partition_map, schema: (*current_schema).clone(), - last_report: None, }) } @@ -394,8 +416,8 @@ impl Conn { let entities = mentat_tx_parser::Tx::parse(&assertion_vector)?; let mut in_progress = self.begin_transaction(sqlite)?; - in_progress.transact_entities(entities)?; - let report = in_progress.commit()?.expect("we always get a report"); + let report = in_progress.transact_entities(entities)?; + in_progress.commit()?; Ok(report) } @@ -488,9 +510,9 @@ mod tests { // Scoped borrow of `conn`. { let mut in_progress = conn.begin_transaction(&mut sqlite).expect("begun successfully"); - in_progress.transact(t).expect("transacted successfully"); - let one = in_progress.last_report().unwrap().tempids.get("one").expect("found one").clone(); - let two = in_progress.last_report().unwrap().tempids.get("two").expect("found two").clone(); + let report = in_progress.transact(t).expect("transacted successfully"); + let one = report.tempids.get("one").expect("found one").clone(); + let two = report.tempids.get("two").expect("found two").clone(); assert!(one != two); assert!(one == tempid_offset || one == tempid_offset + 1); assert!(two == tempid_offset || two == tempid_offset + 1); @@ -499,10 +521,9 @@ mod tests { .expect("query succeeded"); assert_eq!(during, QueryResults::Scalar(Some(TypedValue::Ref(one)))); - in_progress.transact(t2).expect("t2 succeeded"); - let report = in_progress.commit() - .expect("commit succeeded"); - let three = report.unwrap().tempids.get("three").expect("found three").clone(); + let report = in_progress.transact(t2).expect("t2 succeeded"); + in_progress.commit().expect("commit succeeded"); + let three = report.tempids.get("three").expect("found three").clone(); assert!(one != three); assert!(two != three); } @@ -528,10 +549,10 @@ mod tests { // Scoped borrow of `sqlite`. { let mut in_progress = conn.begin_transaction(&mut sqlite).expect("begun successfully"); - in_progress.transact(t).expect("transacted successfully"); + let report = in_progress.transact(t).expect("transacted successfully"); - let one = in_progress.last_report().unwrap().tempids.get("one").expect("found it").clone(); - let two = in_progress.last_report().unwrap().tempids.get("two").expect("found it").clone(); + let one = report.tempids.get("one").expect("found it").clone(); + let two = report.tempids.get("two").expect("found it").clone(); // The IDs are contiguous, starting at the previous part index. assert!(one != two); diff --git a/src/entity_builder.rs b/src/entity_builder.rs new file mode 100644 index 00000000..fd7b078a --- /dev/null +++ b/src/entity_builder.rs @@ -0,0 +1,455 @@ +// Copyright 2016 Mozilla +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +// We have a little bit of a dilemma in Mentat. +// The public data format for transacting is, fundamentally, a big string: EDN. +// The internal data format for transacting is required to encode the complexities of +// processing that format: temporary IDs, lookup refs, input spans, etc. +// +// See mentat_tx::entities::Entity and all of its child enums to see how complex this gets. +// +// A programmatic consumer doesn't want to build something that looks like: +// +// Entity::AddOrRetract { +// op: OpType::Add, +// e: EntidOrLookupRefOrTempId::LookupRef(LookupRef { +// a: Entid::Ident(NamespacedKeyword::new("test", "a1")), +// v: Value::Text("v1".into()), +// }), +// a: Entid::Ident(NamespacedKeyword::new("test", "a")), +// v: AtomOrLookupRefOrVectorOrMapNotation::Atom(ValueAndSpan::new(SpannedValue::Text("v".into()), Span(44, 47))), +// })); +// +// but neither do they want to pay the cost of parsing +// +// [[:test/a1 "v1"] :test/a "v"] +// +// at runtime. +// +// It's tempting to think that we can do something 'easy' here -- to skip the hard work of transacting +// tempids, for example -- but to do so will hobble the system for little payoff. It's also worth +// remembering that the transactor does significant validation work, which we don't want to +// reimplement here. +// +// The win we seek is to make it easier to _write_ these inputs without significantly restricting +// what can be said. +// +// There are two ways we could go from here. +// +// The first is to expose tx parsing as a macro: parse that string at compile time into the +// equivalent `Entity` data structure. That's fine for completely static input data. +// +// The second is to expose a declarative, programmatic builder pattern for constructing entities. +// +// We probably need both, but this file provides the latter. Unfortunately, Entity -- the input to +// the transactor -- is intimately tied to EDN and to spanned values. + +use mentat_core::{ + KnownEntid, + TypedValue, +}; + +use mentat_core::intern_set::InternSet; +use mentat_core::util::Either; + +use mentat_db::{ + TxReport, +}; + +use mentat_db::internal_types::{ + KnownEntidOr, + TempIdHandle, + Term, + TermWithTempIds, + TypedValueOr, +}; + +use mentat_tx::entities::{ + OpType, + TempId, +}; + +use conn::{ + InProgress, +}; + +use errors::{ + Result, +}; + +pub type Terms = (Vec, InternSet); + +pub struct TermBuilder { + tempids: InternSet, + terms: Vec, +} + +pub struct EntityBuilder { + builder: T, + entity: KnownEntidOr, +} + +pub trait BuildTerms where Self: Sized { + fn describe_tempid(self, name: &str) -> EntityBuilder; + fn describe(self, entity: E) -> EntityBuilder where E: IntoThing>; + fn add(&mut self, e: E, a: KnownEntid, v: V) -> Result<()> + where E: IntoThing>, + V: IntoThing>; + fn retract(&mut self, e: E, a: KnownEntid, v: V) -> Result<()> + where E: IntoThing>, + V: IntoThing>; +} + +impl BuildTerms for TermBuilder { + fn describe_tempid(mut self, name: &str) -> EntityBuilder { + let e = self.named_tempid(name.into()); + self.describe(e) + } + + fn describe(self, entity: E) -> EntityBuilder where E: IntoThing> { + EntityBuilder { + builder: self, + entity: entity.into_thing(), + } + } + + fn add(&mut self, e: E, a: KnownEntid, v: V) -> Result<()> + where E: IntoThing>, + V: IntoThing> { + let e = e.into_thing(); + let v = v.into_thing(); + self.terms.push(Term::AddOrRetract(OpType::Add, e, a.into(), v)); + Ok(()) + } + + fn retract(&mut self, e: E, a: KnownEntid, v: V) -> Result<()> + where E: IntoThing>, + V: IntoThing> { + let e = e.into_thing(); + let v = v.into_thing(); + self.terms.push(Term::AddOrRetract(OpType::Retract, e, a.into(), v)); + Ok(()) + } +} + +impl TermBuilder { + pub fn build(self) -> Result { + Ok((self.terms, self.tempids)) + } + + pub fn new() -> TermBuilder { + TermBuilder { + tempids: InternSet::new(), + terms: vec![], + } + } + + pub fn named_tempid(&mut self, name: String) -> TempIdHandle { + self.tempids.intern(TempId::External(name)) + } + + #[allow(dead_code)] + pub fn numbered_tempid(&mut self, id: i64) -> TempIdHandle { + self.tempids.intern(TempId::Internal(id)) + } +} + +impl EntityBuilder where T: BuildTerms { + pub fn finish(self) -> (T, KnownEntidOr) { + (self.builder, self.entity) + } + + pub fn add(&mut self, a: KnownEntid, v: V) -> Result<()> + where V: IntoThing> { + self.builder.add(self.entity.clone(), a, v) + } +} + +pub struct InProgressBuilder<'a, 'c> { + in_progress: InProgress<'a, 'c>, + builder: TermBuilder, +} + +impl<'a, 'c> InProgressBuilder<'a, 'c> { + pub fn new(in_progress: InProgress<'a, 'c>) -> Self { + InProgressBuilder { + in_progress: in_progress, + builder: TermBuilder::new(), + } + } + + /// Build the terms from this builder and transact them against the current + /// `InProgress`. This method _always_ returns the `InProgress` -- failure doesn't + /// imply an automatic rollback. + pub fn transact(self) -> (InProgress<'a, 'c>, Result) { + let mut in_progress = self.in_progress; + let result = self.builder + .build() + .and_then(|(terms, tempid_set)| { + in_progress.transact_terms(terms, tempid_set) + }); + (in_progress, result) + } + + /// Transact the contents of the builder and commit the `InProgress`. If any + /// step fails, roll back. Return the `TxReport`. + pub fn commit(self) -> Result { + let mut in_progress = self.in_progress; + self.builder + .build() + .and_then(|(terms, tempid_set)| { + in_progress.transact_terms(terms, tempid_set) + .and_then(|report| { + in_progress.commit()?; + Ok(report) + }) + }) + } +} + +impl<'a, 'c> BuildTerms for InProgressBuilder<'a, 'c> { + fn describe_tempid(mut self, name: &str) -> EntityBuilder> { + let e = self.builder.named_tempid(name.into()); + self.describe(e) + } + + fn describe(self, entity: E) -> EntityBuilder> where E: IntoThing> { + EntityBuilder { + builder: self, + entity: entity.into_thing(), + } + } + + fn add(&mut self, e: E, a: KnownEntid, v: V) -> Result<()> + where E: IntoThing>, + V: IntoThing> { + self.builder.add(e, a, v) + } + + fn retract(&mut self, e: E, a: KnownEntid, v: V) -> Result<()> + where E: IntoThing>, + V: IntoThing> { + self.builder.retract(e, a, v) + } +} + +impl<'a, 'c> EntityBuilder> { + /// Build the terms from this builder and transact them against the current + /// `InProgress`. This method _always_ returns the `InProgress` -- failure doesn't + /// imply an automatic rollback. + pub fn transact(self) -> (InProgress<'a, 'c>, Result) { + self.finish().0.transact() + } + + /// Transact the contents of the builder and commit the `InProgress`. If any + /// step fails, roll back. Return the `TxReport`. + pub fn commit(self) -> Result { + self.finish().0.commit() + } +} + +// Can't implement Into for Rc. +pub trait IntoThing: Sized { + fn into_thing(self) -> T; +} + +pub trait FromThing { + fn from_thing(v: T) -> Self; +} + +impl FromThing for T { + fn from_thing(v: T) -> T { + v + } +} + +impl IntoThing for F where I: FromThing { + fn into_thing(self) -> I { + I::from_thing(self) + } +} + +impl<'a> FromThing<&'a TempIdHandle> for TypedValueOr { + fn from_thing(v: &'a TempIdHandle) -> Self { + Either::Right(v.clone()) + } +} + +impl FromThing for TypedValueOr { + fn from_thing(v: TempIdHandle) -> Self { + Either::Right(v) + } +} + +impl FromThing for TypedValueOr { + fn from_thing(v: TypedValue) -> Self { + Either::Left(v) + } +} + +impl FromThing for KnownEntidOr { + fn from_thing(v: TempIdHandle) -> Self { + Either::Right(v) + } +} + +impl<'a> FromThing<&'a KnownEntid> for KnownEntidOr { + fn from_thing(v: &'a KnownEntid) -> Self { + Either::Left(v.clone()) + } +} + +impl FromThing for KnownEntidOr { + fn from_thing(v: KnownEntid) -> Self { + Either::Left(v) + } +} + +impl FromThing for TypedValueOr { + fn from_thing(v: KnownEntid) -> Self { + Either::Left(v.into()) + } +} + +#[cfg(test)] +mod testing { + extern crate mentat_db; + + use mentat_core::{ + Entid, + HasSchema, + NamespacedKeyword, + TypedValue, + }; + + use errors::{ + Error, + }; + + use errors::ErrorKind::{ + DbError, + }; + + use mentat_db::TxReport; + + use mentat_db::ErrorKind::{ + UnrecognizedEntid, + }; + + use ::{ + Conn, + Queryable, + }; + + use super::*; + + // In reality we expect the store to hand these out safely. + fn fake_known_entid(e: Entid) -> KnownEntid { + KnownEntid(e) + } + + #[test] + fn test_entity_builder_bogus_entids() { + let mut builder = TermBuilder::new(); + let e = builder.named_tempid("x".into()); + let a1 = fake_known_entid(37); // :db/doc + let a2 = fake_known_entid(999); + let v = TypedValue::typed_string("Some attribute"); + let ve = fake_known_entid(12345); + + builder.add(e.clone(), a1, v).expect("add succeeded"); + builder.add(e.clone(), a2, e.clone()).expect("add succeeded, even though it's meaningless"); + builder.add(e.clone(), a2, ve).expect("add succeeded, even though it's meaningless"); + let (terms, tempids) = builder.build().expect("build succeeded"); + + assert_eq!(tempids.len(), 1); + assert_eq!(terms.len(), 3); // TODO: check the contents? + + // Now try to add them to a real store. + let mut sqlite = mentat_db::db::new_connection("").unwrap(); + let mut conn = Conn::connect(&mut sqlite).unwrap(); + let mut in_progress = conn.begin_transaction(&mut sqlite).expect("begun successfully"); + + // This should fail: unrecognized entid. + if let Err(Error(DbError(UnrecognizedEntid(e)), _)) = in_progress.transact_terms(terms, tempids) { + assert_eq!(e, 999); + } else { + panic!("Should have rejected the entid."); + } + } + + #[test] + fn test_entity_builder() { + let mut sqlite = mentat_db::db::new_connection("").unwrap(); + let mut conn = Conn::connect(&mut sqlite).unwrap(); + + let foo_one = NamespacedKeyword::new("foo", "one"); + let foo_many = NamespacedKeyword::new("foo", "many"); + let foo_ref = NamespacedKeyword::new("foo", "ref"); + let report: TxReport; + + // Give ourselves a schema to work with! + // Scoped borrow of conn. + { + conn.transact(&mut sqlite, r#"[ + [:db/add "o" :db/ident :foo/one] + [:db/add "o" :db/valueType :db.type/long] + [:db/add "o" :db/cardinality :db.cardinality/one] + [:db/add "m" :db/ident :foo/many] + [:db/add "m" :db/valueType :db.type/string] + [:db/add "m" :db/cardinality :db.cardinality/many] + [:db/add "r" :db/ident :foo/ref] + [:db/add "r" :db/valueType :db.type/ref] + [:db/add "r" :db/cardinality :db.cardinality/one] + ]"#).unwrap(); + + let mut in_progress = conn.begin_transaction(&mut sqlite).expect("begun successfully"); + + // Scoped borrow of in_progress. + { + let mut builder = TermBuilder::new(); + let e_x = builder.named_tempid("x".into()); + let e_y = builder.named_tempid("y".into()); + let a_ref = in_progress.get_entid(&foo_ref).expect(":foo/ref"); + let a_one = in_progress.get_entid(&foo_one).expect(":foo/one"); + let a_many = in_progress.get_entid(&foo_many).expect(":foo/many"); + let v_many_1 = TypedValue::typed_string("Some text"); + let v_many_2 = TypedValue::typed_string("Other text"); + let v_long: TypedValue = 123.into(); + + builder.add(e_x.clone(), a_many, v_many_1).expect("add succeeded"); + builder.add(e_x.clone(), a_many, v_many_2).expect("add succeeded"); + builder.add(e_y.clone(), a_ref, e_x.clone()).expect("add succeeded"); + builder.add(e_x.clone(), a_one, v_long).expect("add succeeded"); + + let (terms, tempids) = builder.build().expect("build succeeded"); + + assert_eq!(tempids.len(), 2); + assert_eq!(terms.len(), 4); + + report = in_progress.transact_terms(terms, tempids).expect("add succeeded"); + let x = report.tempids.get("x").expect("our tempid has an ID"); + let y = report.tempids.get("y").expect("our tempid has an ID"); + assert_eq!(in_progress.lookup_value_for_attribute(*y, &foo_ref).expect("lookup succeeded"), + Some(TypedValue::Ref(*x))); + assert_eq!(in_progress.lookup_value_for_attribute(*x, &foo_one).expect("lookup succeeded"), + Some(TypedValue::Long(123))); + } + + in_progress.commit().expect("commit succeeded"); + } + + // It's all still there after the commit. + let x = report.tempids.get("x").expect("our tempid has an ID"); + let y = report.tempids.get("y").expect("our tempid has an ID"); + assert_eq!(conn.lookup_value_for_attribute(&mut sqlite, *y, &foo_ref).expect("lookup succeeded"), + Some(TypedValue::Ref(*x))); + } +} diff --git a/src/errors.rs b/src/errors.rs index a7e8f3b9..642072eb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -15,7 +15,11 @@ use rusqlite; use std::collections::BTreeSet; use edn; +use mentat_core::{ + Attribute, +}; use mentat_db; +use mentat_query; use mentat_query_algebrizer; use mentat_query_parser; use mentat_query_projector; @@ -63,5 +67,10 @@ error_chain! { description("invalid vocabulary version") display("invalid vocabulary version") } + + ConflictingAttributeDefinitions(vocabulary: String, version: ::vocabulary::Version, attribute: String, current: Attribute, requested: Attribute) { + description("conflicting attribute definitions") + display("vocabulary {}/{} already has attribute {}, and the requested definition differs", vocabulary, version, attribute) + } } } diff --git a/src/lib.rs b/src/lib.rs index 17885bc5..048fd96c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,9 @@ #[macro_use] extern crate error_chain; +#[macro_use] +extern crate lazy_static; + extern crate rusqlite; extern crate edn; @@ -33,6 +36,7 @@ pub mod errors; pub mod ident; pub mod conn; pub mod query; +pub mod entity_builder; pub fn get_name() -> String { return String::from("mentat"); @@ -44,12 +48,16 @@ pub fn get_connection() -> Connection { } pub use mentat_core::{ + Attribute, + Entid, TypedValue, Uuid, ValueType, }; pub use mentat_db::{ + CORE_SCHEMA_VERSION, + DB_SCHEMA_CORE, new_connection, }; @@ -67,6 +75,7 @@ pub use query::{ pub use conn::{ Conn, + InProgress, Metadata, Queryable, };