Add an EntityBuilder abstraction. r=nalexander,emily
This includes two other changes: * Split transact to expose an interface for TermWithTempIds. * Return TxReport from each InProgress operation, not from commit.
This commit is contained in:
parent
3d28949add
commit
812f10b3e4
7 changed files with 575 additions and 48 deletions
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
64
db/src/tx.rs
64
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<I>(&mut self, entities: I) -> Result<TxReport> where I: IntoIterator<Item=Entity> {
|
||||
// TODO: push these into an internal transaction report?
|
||||
let mut tempids: BTreeMap<TempId, KnownEntid> = 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<I>(&mut self, terms: I, tempid_set: InternSet<TempId>) -> Result<TxReport> where I: IntoIterator<Item=TermWithTempIds> {
|
||||
// TODO: push these into an internal transaction report?
|
||||
let mut tempids: BTreeMap<TempId, KnownEntid> = 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,
|
||||
/// 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,
|
||||
entities: I) -> Result<(TxReport, PartitionMap, Option<Schema>)> where I: IntoIterator<Item=Entity> {
|
||||
|
||||
schema: &'a Schema) -> Result<Tx<'conn, 'a>> {
|
||||
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<Schema>)> {
|
||||
// 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<Schema>)>
|
||||
where I: IntoIterator<Item=Entity> {
|
||||
|
||||
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<TempId>) -> Result<(TxReport, PartitionMap, Option<Schema>)>
|
||||
where I: IntoIterator<Item=TermWithTempIds> {
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
73
src/conn.rs
73
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<TxReport>, // 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<I>(&mut self, entities: I) -> Result<()> where I: IntoIterator<Item=mentat_tx::entities::Entity> {
|
||||
pub fn builder(self) -> InProgressBuilder<'a, 'c> {
|
||||
InProgressBuilder::new(self)
|
||||
}
|
||||
|
||||
pub fn transact_terms<I>(&mut self, terms: I, tempid_set: InternSet<TempId>) -> Result<TxReport> where I: IntoIterator<Item=TermWithTempIds> {
|
||||
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<I>(&mut self, entities: I) -> Result<TxReport> where I: IntoIterator<Item=mentat_tx::entities::Entity> {
|
||||
// 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<TxReport> {
|
||||
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<Option<TxReport>> {
|
||||
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);
|
||||
|
|
455
src/entity_builder.rs
Normal file
455
src/entity_builder.rs
Normal file
|
@ -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<TermWithTempIds>, InternSet<TempId>);
|
||||
|
||||
pub struct TermBuilder {
|
||||
tempids: InternSet<TempId>,
|
||||
terms: Vec<TermWithTempIds>,
|
||||
}
|
||||
|
||||
pub struct EntityBuilder<T: BuildTerms + Sized> {
|
||||
builder: T,
|
||||
entity: KnownEntidOr<TempIdHandle>,
|
||||
}
|
||||
|
||||
pub trait BuildTerms where Self: Sized {
|
||||
fn describe_tempid(self, name: &str) -> EntityBuilder<Self>;
|
||||
fn describe<E>(self, entity: E) -> EntityBuilder<Self> where E: IntoThing<KnownEntidOr<TempIdHandle>>;
|
||||
fn add<E, V>(&mut self, e: E, a: KnownEntid, v: V) -> Result<()>
|
||||
where E: IntoThing<KnownEntidOr<TempIdHandle>>,
|
||||
V: IntoThing<TypedValueOr<TempIdHandle>>;
|
||||
fn retract<E, V>(&mut self, e: E, a: KnownEntid, v: V) -> Result<()>
|
||||
where E: IntoThing<KnownEntidOr<TempIdHandle>>,
|
||||
V: IntoThing<TypedValueOr<TempIdHandle>>;
|
||||
}
|
||||
|
||||
impl BuildTerms for TermBuilder {
|
||||
fn describe_tempid(mut self, name: &str) -> EntityBuilder<Self> {
|
||||
let e = self.named_tempid(name.into());
|
||||
self.describe(e)
|
||||
}
|
||||
|
||||
fn describe<E>(self, entity: E) -> EntityBuilder<Self> where E: IntoThing<KnownEntidOr<TempIdHandle>> {
|
||||
EntityBuilder {
|
||||
builder: self,
|
||||
entity: entity.into_thing(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add<E, V>(&mut self, e: E, a: KnownEntid, v: V) -> Result<()>
|
||||
where E: IntoThing<KnownEntidOr<TempIdHandle>>,
|
||||
V: IntoThing<TypedValueOr<TempIdHandle>> {
|
||||
let e = e.into_thing();
|
||||
let v = v.into_thing();
|
||||
self.terms.push(Term::AddOrRetract(OpType::Add, e, a.into(), v));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn retract<E, V>(&mut self, e: E, a: KnownEntid, v: V) -> Result<()>
|
||||
where E: IntoThing<KnownEntidOr<TempIdHandle>>,
|
||||
V: IntoThing<TypedValueOr<TempIdHandle>> {
|
||||
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<Terms> {
|
||||
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<T> EntityBuilder<T> where T: BuildTerms {
|
||||
pub fn finish(self) -> (T, KnownEntidOr<TempIdHandle>) {
|
||||
(self.builder, self.entity)
|
||||
}
|
||||
|
||||
pub fn add<V>(&mut self, a: KnownEntid, v: V) -> Result<()>
|
||||
where V: IntoThing<TypedValueOr<TempIdHandle>> {
|
||||
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<TxReport>) {
|
||||
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<TxReport> {
|
||||
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<InProgressBuilder<'a, 'c>> {
|
||||
let e = self.builder.named_tempid(name.into());
|
||||
self.describe(e)
|
||||
}
|
||||
|
||||
fn describe<E>(self, entity: E) -> EntityBuilder<InProgressBuilder<'a, 'c>> where E: IntoThing<KnownEntidOr<TempIdHandle>> {
|
||||
EntityBuilder {
|
||||
builder: self,
|
||||
entity: entity.into_thing(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add<E, V>(&mut self, e: E, a: KnownEntid, v: V) -> Result<()>
|
||||
where E: IntoThing<KnownEntidOr<TempIdHandle>>,
|
||||
V: IntoThing<TypedValueOr<TempIdHandle>> {
|
||||
self.builder.add(e, a, v)
|
||||
}
|
||||
|
||||
fn retract<E, V>(&mut self, e: E, a: KnownEntid, v: V) -> Result<()>
|
||||
where E: IntoThing<KnownEntidOr<TempIdHandle>>,
|
||||
V: IntoThing<TypedValueOr<TempIdHandle>> {
|
||||
self.builder.retract(e, a, v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'c> EntityBuilder<InProgressBuilder<'a, 'c>> {
|
||||
/// 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<TxReport>) {
|
||||
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<TxReport> {
|
||||
self.finish().0.commit()
|
||||
}
|
||||
}
|
||||
|
||||
// Can't implement Into for Rc<T>.
|
||||
pub trait IntoThing<T>: Sized {
|
||||
fn into_thing(self) -> T;
|
||||
}
|
||||
|
||||
pub trait FromThing<T> {
|
||||
fn from_thing(v: T) -> Self;
|
||||
}
|
||||
|
||||
impl<T> FromThing<T> for T {
|
||||
fn from_thing(v: T) -> T {
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, F> IntoThing<I> for F where I: FromThing<F> {
|
||||
fn into_thing(self) -> I {
|
||||
I::from_thing(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromThing<&'a TempIdHandle> for TypedValueOr<TempIdHandle> {
|
||||
fn from_thing(v: &'a TempIdHandle) -> Self {
|
||||
Either::Right(v.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromThing<TempIdHandle> for TypedValueOr<TempIdHandle> {
|
||||
fn from_thing(v: TempIdHandle) -> Self {
|
||||
Either::Right(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromThing<TypedValue> for TypedValueOr<TempIdHandle> {
|
||||
fn from_thing(v: TypedValue) -> Self {
|
||||
Either::Left(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromThing<TempIdHandle> for KnownEntidOr<TempIdHandle> {
|
||||
fn from_thing(v: TempIdHandle) -> Self {
|
||||
Either::Right(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromThing<&'a KnownEntid> for KnownEntidOr<TempIdHandle> {
|
||||
fn from_thing(v: &'a KnownEntid) -> Self {
|
||||
Either::Left(v.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromThing<KnownEntid> for KnownEntidOr<TempIdHandle> {
|
||||
fn from_thing(v: KnownEntid) -> Self {
|
||||
Either::Left(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromThing<KnownEntid> for TypedValueOr<TempIdHandle> {
|
||||
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)));
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue