From 6ed5413cd42e2c4bba4d38dc57063168c32112dd Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Thu, 11 Jan 2018 21:44:05 -0800 Subject: [PATCH] Implement simple vocabulary management. (#504) r=emily,nalexander Bump version to 0.5.1 to reflect this change. --- Cargo.toml | 2 +- src/conn.rs | 4 + src/errors.rs | 15 ++ src/lib.rs | 1 + src/vocabulary.rs | 605 ++++++++++++++++++++++++++++++++++++++++++++ tests/vocabulary.rs | 270 ++++++++++++++++++++ 6 files changed, 896 insertions(+), 1 deletion(-) create mode 100644 src/vocabulary.rs create mode 100644 tests/vocabulary.rs diff --git a/Cargo.toml b/Cargo.toml index 56b97c0d..f55e3ba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ authors = [ "Emily Toop ", ] name = "mentat" -version = "0.5.0" +version = "0.5.1" build = "build/version.rs" [workspace] diff --git a/src/conn.rs b/src/conn.rs index 31352b42..4d5893e9 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -298,6 +298,10 @@ impl<'a, 'c> InProgress<'a, 'c> { if self.schema != *(metadata.schema) { metadata.schema = Arc::new(self.schema); + + // TODO: rebuild vocabularies and notify consumers that they've changed -- it's possible + // that a change has arrived over the wire and invalidated some local module. + // TODO: consider making vocabulary lookup lazy -- we won't need it much of the time. } Ok(()) diff --git a/src/errors.rs b/src/errors.rs index 642072eb..fc8902cb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -72,5 +72,20 @@ error_chain! { description("conflicting attribute definitions") display("vocabulary {}/{} already has attribute {}, and the requested definition differs", vocabulary, version, attribute) } + + ExistingVocabularyTooNew(name: String, existing: ::vocabulary::Version, ours: ::vocabulary::Version) { + description("existing vocabulary too new") + display("existing vocabulary too new: wanted {}, got {}", ours, existing) + } + + UnexpectedCoreSchema(version: Option<::vocabulary::Version>) { + description("unexpected core schema version") + display("core schema: wanted {}, got {:?}", mentat_db::CORE_SCHEMA_VERSION, version) + } + + MissingCoreVocabulary(kw: mentat_query::NamespacedKeyword) { + description("missing core vocabulary") + display("missing core attribute {}", kw) + } } } diff --git a/src/lib.rs b/src/lib.rs index 048fd96c..d9da5348 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ use rusqlite::Connection; pub mod errors; pub mod ident; +pub mod vocabulary; pub mod conn; pub mod query; pub mod entity_builder; diff --git a/src/vocabulary.rs b/src/vocabulary.rs new file mode 100644 index 00000000..c03628d6 --- /dev/null +++ b/src/vocabulary.rs @@ -0,0 +1,605 @@ +// 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. + + +//! This module exposes an interface for programmatic management of vocabularies. A vocabulary +//! is defined as a name, a version number, and a collection of attribute definitions. In the +//! future, this input will be augmented with specifications of migrations between versions. +//! +//! A Mentat store exposes, via the `HasSchema` trait, operations to read vocabularies by name +//! or in bulk. +//! +//! An in-progress transaction (`InProgress`) further exposes a trait, `VersionedStore`, which +//! allows for a vocabulary definition to be checked for existence in the store, and transacted +//! if needed. +//! +//! Typical use is the following: +//! +//! ``` +//! extern crate mentat; +//! extern crate mentat_db; // So we can use SQLite connection utilities. +//! +//! use mentat::{ +//! Conn, +//! NamespacedKeyword, +//! ValueType, +//! }; +//! +//! use mentat::vocabulary; +//! use mentat::vocabulary::{ +//! Definition, +//! HasVocabularies, +//! VersionedStore, +//! VocabularyOutcome, +//! }; +//! +//! fn main() { +//! let mut sqlite = mentat_db::db::new_connection("").expect("SQLite connected"); +//! let mut conn = Conn::connect(&mut sqlite).expect("connected"); +//! +//! { +//! // Read the list of installed vocabularies. +//! let reader = conn.begin_read(&mut sqlite).expect("began read"); +//! let vocabularies = reader.read_vocabularies().expect("read"); +//! for (name, vocabulary) in vocabularies.iter() { +//! println!("Vocab {} is at version {}.", name, vocabulary.version); +//! for &(ref name, ref attr) in vocabulary.attributes().iter() { +//! println!(" >> {} ({})", name, attr.value_type); +//! } +//! } +//! } +//! +//! { +//! let mut in_progress = conn.begin_transaction(&mut sqlite).expect("began transaction"); +//! +//! // Make sure the core vocabulary exists. +//! in_progress.verify_core_schema().expect("verified"); +//! +//! // Make sure our vocabulary is installed, and install if necessary. +//! in_progress.ensure_vocabulary(&Definition { +//! name: NamespacedKeyword::new("example", "links"), +//! version: 1, +//! attributes: vec![ +//! (NamespacedKeyword::new("link", "title"), +//! vocabulary::AttributeBuilder::default() +//! .value_type(ValueType::String) +//! .multival(false) +//! .fulltext(true) +//! .build()), +//! ], +//! }).expect("ensured"); +//! +//! // Now we can do stuff. +//! in_progress.transact("[{:link/title \"Title\"}]").expect("transacts"); +//! in_progress.commit().expect("commits"); +//! } +//! } +//! ``` + +use std::collections::BTreeMap; + +use mentat_core::{ + Attribute, + Entid, + HasSchema, + KnownEntid, + NamespacedKeyword, + TypedValue, + ValueType, +}; + +pub use mentat_core::attribute; +use mentat_core::attribute::Unique; + +use ::{ + CORE_SCHEMA_VERSION, + IntoResult, +}; + +use ::conn::{ + InProgress, + Queryable, +}; + +use ::errors::{ + ErrorKind, + Result, +}; + +use ::entity_builder::{ + BuildTerms, + TermBuilder, + Terms, +}; + +pub use mentat_db::AttributeBuilder; + +pub type Version = u32; +pub type Datom = (Entid, Entid, TypedValue); + +/// `Attribute` instances not only aren't named, but don't even have entids. +/// We need two kinds of structure here: an abstract definition of a vocabulary in terms of names, +/// and a concrete instance of a vocabulary in a particular store. +/// Note that, because it's possible to 'flesh out' a vocabulary with attributes without bumping +/// its version number, we need to know the attributes that the application cares about -- it's +/// not enough to know the name and version. Indeed, we even care about the details of each attribute, +/// because that's how we'll detect errors. +#[derive(Debug)] +pub struct Definition { + pub name: NamespacedKeyword, + pub version: Version, + pub attributes: Vec<(NamespacedKeyword, Attribute)>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Vocabulary { + pub entity: Entid, + pub version: Version, + attributes: Vec<(Entid, Attribute)>, +} + +impl Vocabulary { + pub fn attributes(&self) -> &Vec<(Entid, Attribute)> { + &self.attributes + } +} + +#[derive(Debug, Default, Clone)] +pub struct Vocabularies(pub BTreeMap); // N.B., this has a copy of the attributes in Schema! + +impl Vocabularies { + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn get(&self, name: &NamespacedKeyword) -> Option<&Vocabulary> { + self.0.get(name) + } + + pub fn iter(&self) -> ::std::collections::btree_map::Iter { + self.0.iter() + } +} + +lazy_static! { + static ref DB_SCHEMA_CORE: NamespacedKeyword = { + NamespacedKeyword::new("db.schema", "core") + }; + static ref DB_SCHEMA_ATTRIBUTE: NamespacedKeyword = { + NamespacedKeyword::new("db.schema", "attribute") + }; + static ref DB_SCHEMA_VERSION: NamespacedKeyword = { + NamespacedKeyword::new("db.schema", "version") + }; + static ref DB_IDENT: NamespacedKeyword = { + NamespacedKeyword::new("db", "ident") + }; + static ref DB_UNIQUE: NamespacedKeyword = { + NamespacedKeyword::new("db", "unique") + }; + static ref DB_UNIQUE_VALUE: NamespacedKeyword = { + NamespacedKeyword::new("db.unique", "value") + }; + static ref DB_UNIQUE_IDENTITY: NamespacedKeyword = { + NamespacedKeyword::new("db.unique", "identity") + }; + static ref DB_IS_COMPONENT: NamespacedKeyword = { + NamespacedKeyword::new("db", "isComponent") + }; + static ref DB_VALUE_TYPE: NamespacedKeyword = { + NamespacedKeyword::new("db", "valueType") + }; + static ref DB_INDEX: NamespacedKeyword = { + NamespacedKeyword::new("db", "index") + }; + static ref DB_FULLTEXT: NamespacedKeyword = { + NamespacedKeyword::new("db", "fulltext") + }; + static ref DB_CARDINALITY: NamespacedKeyword = { + NamespacedKeyword::new("db", "cardinality") + }; + static ref DB_CARDINALITY_ONE: NamespacedKeyword = { + NamespacedKeyword::new("db.cardinality", "one") + }; + static ref DB_CARDINALITY_MANY: NamespacedKeyword = { + NamespacedKeyword::new("db.cardinality", "many") + }; + + // Not yet supported. + // static ref DB_NO_HISTORY: NamespacedKeyword = { + // NamespacedKeyword::new("db", "noHistory") + // }; +} + +trait HasCoreSchema { + /// Return the entity ID for a type. On failure, return `MissingCoreVocabulary`. + fn core_type(&self, t: ValueType) -> Result; + + /// Return the entity ID for an ident. On failure, return `MissingCoreVocabulary`. + fn core_entid(&self, ident: &NamespacedKeyword) -> Result; + + /// Return the entity ID for an attribute's keyword. On failure, return + /// `MissingCoreVocabulary`. + fn core_attribute(&self, ident: &NamespacedKeyword) -> Result; +} + +impl HasCoreSchema for T where T: HasSchema { + fn core_type(&self, t: ValueType) -> Result { + self.entid_for_type(t) + .ok_or_else(|| ErrorKind::MissingCoreVocabulary(DB_SCHEMA_VERSION.clone()).into()) + } + + fn core_entid(&self, ident: &NamespacedKeyword) -> Result { + self.get_entid(ident) + .ok_or_else(|| ErrorKind::MissingCoreVocabulary(DB_SCHEMA_VERSION.clone()).into()) + } + + fn core_attribute(&self, ident: &NamespacedKeyword) -> Result { + self.attribute_for_ident(ident) + .ok_or_else(|| ErrorKind::MissingCoreVocabulary(DB_SCHEMA_VERSION.clone()).into()) + .map(|(_, e)| e) + } +} + +impl Definition { + fn description_for_attributes<'s, T, R>(&'s self, attributes: &[R], via: &T) -> Result + where T: HasCoreSchema, + R: ::std::borrow::Borrow<(NamespacedKeyword, Attribute)> { + + // The attributes we'll need to describe this vocabulary. + let a_version = via.core_attribute(&DB_SCHEMA_VERSION)?; + let a_ident = via.core_attribute(&DB_IDENT)?; + let a_attr = via.core_attribute(&DB_SCHEMA_ATTRIBUTE)?; + + let a_cardinality = via.core_attribute(&DB_CARDINALITY)?; + let a_fulltext = via.core_attribute(&DB_FULLTEXT)?; + let a_index = via.core_attribute(&DB_INDEX)?; + let a_is_component = via.core_attribute(&DB_IS_COMPONENT)?; + let a_value_type = via.core_attribute(&DB_VALUE_TYPE)?; + let a_unique = via.core_attribute(&DB_UNIQUE)?; + + // Not yet supported. + // let a_no_history = via.core_attribute(&DB_NO_HISTORY)?; + + let v_cardinality_many = via.core_entid(&DB_CARDINALITY_MANY)?; + let v_cardinality_one = via.core_entid(&DB_CARDINALITY_ONE)?; + let v_unique_identity = via.core_entid(&DB_UNIQUE_IDENTITY)?; + let v_unique_value = via.core_entid(&DB_UNIQUE_VALUE)?; + + // The properties of the vocabulary itself. + let name: TypedValue = self.name.clone().into(); + let version: TypedValue = TypedValue::Long(self.version as i64); + + // Describe the vocabulary. + let mut entity = TermBuilder::new().describe_tempid("s"); + entity.add(a_version, version)?; + entity.add(a_ident, name)?; + let (mut builder, schema) = entity.finish(); + + // Describe each of its attributes. + // This is a lot like Schema::to_edn_value; at some point we should tidy this up. + for ref r in attributes.iter() { + let &(ref name, ref attr) = r.borrow(); + + // Note that we allow tempid resolution to find an existing entity, if it + // exists. We don't yet support upgrades, which will involve producing + // alteration statements. + let tempid = builder.named_tempid(name.to_string()); + let name: TypedValue = name.clone().into(); + builder.add(tempid.clone(), a_ident, name)?; + builder.add(schema.clone(), a_attr, tempid.clone())?; + + let value_type = via.core_type(attr.value_type)?; + builder.add(tempid.clone(), a_value_type, value_type)?; + + let c = if attr.multival { + v_cardinality_many + } else { + v_cardinality_one + }; + builder.add(tempid.clone(), a_cardinality, c)?; + + if attr.index { + builder.add(tempid.clone(), a_index, TypedValue::Boolean(true))?; + } + if attr.fulltext { + builder.add(tempid.clone(), a_fulltext, TypedValue::Boolean(true))?; + } + if attr.component { + builder.add(tempid.clone(), a_is_component, TypedValue::Boolean(true))?; + } + + if let Some(u) = attr.unique { + let uu = match u { + Unique::Identity => v_unique_identity, + Unique::Value => v_unique_value, + }; + builder.add(tempid.clone(), a_unique, uu)?; + } + } + + builder.build() + } + + /// Return a sequence of terms that describes this vocabulary definition and its attributes. + fn description(&self, via: &T) -> Result where T: HasSchema { + self.description_for_attributes(self.attributes.as_slice(), via) + } +} + +#[derive(Debug, Eq, PartialEq)] +pub enum VocabularyCheck<'definition> { + NotPresent, + Present, + PresentButNeedsUpdate { older_version: Vocabulary }, + PresentButTooNew { newer_version: Vocabulary }, + PresentButMissingAttributes { attributes: Vec<&'definition (NamespacedKeyword, Attribute)> }, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum VocabularyOutcome { + /// The vocabulary was absent and has been installed. + Installed, + + /// The vocabulary was present with this version, but some attributes were absent. + /// They have been installed. + InstalledMissingAttributes, + + /// The vocabulary was present, at the correct version, and all attributes were present. + Existed, + + /// The vocabulary was present, at an older version, and it has been upgraded. Any + /// missing attributes were installed. + Upgraded, +} + +pub trait HasVocabularies { + fn read_vocabularies(&self) -> Result; + fn read_vocabulary_named(&self, name: &NamespacedKeyword) -> Result>; +} + +pub trait VersionedStore { + /// Check whether the vocabulary described by the provided metadata is present in the store. + fn check_vocabulary<'definition>(&self, definition: &'definition Definition) -> Result>; + + /// Check whether the provided vocabulary is present in the store. If it isn't, make it so. + fn ensure_vocabulary(&mut self, definition: &Definition) -> Result; + + /// Make sure that our expectations of the core vocabulary -- basic types and attributes -- are met. + fn verify_core_schema(&self) -> Result<()>; +} + +trait VocabularyMechanics { + fn install_vocabulary(&mut self, definition: &Definition) -> Result; + fn install_attributes_for<'definition>(&mut self, definition: &'definition Definition, attributes: Vec<&'definition (NamespacedKeyword, Attribute)>) -> Result; + fn upgrade_vocabulary(&mut self, definition: &Definition, from_version: Vocabulary) -> Result; +} + +impl Vocabulary { + // TODO: don't do linear search! + fn find(&self, entid: T) -> Option<&Attribute> where T: Into { + let to_find = entid.into(); + self.attributes.iter().find(|&&(e, _)| e == to_find).map(|&(_, ref a)| a) + } +} + +impl<'a, 'c> VersionedStore for InProgress<'a, 'c> { + fn verify_core_schema(&self) -> Result<()> { + if let Some(core) = self.read_vocabulary_named(&DB_SCHEMA_CORE)? { + if core.version != CORE_SCHEMA_VERSION { + bail!(ErrorKind::UnexpectedCoreSchema(Some(core.version))); + } + + // TODO: check things other than the version. + } else { + // This would be seriously messed up. + bail!(ErrorKind::UnexpectedCoreSchema(None)); + } + Ok(()) + } + + fn check_vocabulary<'definition>(&self, definition: &'definition Definition) -> Result> { + if let Some(vocabulary) = self.read_vocabulary_named(&definition.name)? { + // The name is present. + // Check the version. + if vocabulary.version == definition.version { + // Same version. Check that all of our attributes are present. + let mut missing: Vec<&'definition (NamespacedKeyword, Attribute)> = vec![]; + for pair in definition.attributes.iter() { + if let Some(entid) = self.get_entid(&pair.0) { + if let Some(existing) = vocabulary.find(entid) { + if *existing == pair.1 { + // Same. Phew. + continue; + } else { + // We have two vocabularies with the same name, same version, and + // different definitions for an attribute. That's a coding error. + // We can't accept this vocabulary. + bail!(ErrorKind::ConflictingAttributeDefinitions( + definition.name.to_string(), + definition.version, + pair.0.to_string(), + existing.clone(), + pair.1.clone()) + ); + } + } + } + // It's missing. Collect it. + missing.push(pair); + } + if missing.is_empty() { + Ok(VocabularyCheck::Present) + } else { + Ok(VocabularyCheck::PresentButMissingAttributes { attributes: missing }) + } + } else if vocabulary.version < definition.version { + // Ours is newer. Upgrade. + Ok(VocabularyCheck::PresentButNeedsUpdate { older_version: vocabulary }) + } else { + // The vocabulary in the store is newer. We are outdated. + Ok(VocabularyCheck::PresentButTooNew { newer_version: vocabulary }) + } + } else { + // The vocabulary isn't present in the store. Install it. + Ok(VocabularyCheck::NotPresent) + } + } + + fn ensure_vocabulary(&mut self, definition: &Definition) -> Result { + match self.check_vocabulary(definition)? { + VocabularyCheck::Present => Ok(VocabularyOutcome::Existed), + VocabularyCheck::NotPresent => self.install_vocabulary(definition), + VocabularyCheck::PresentButNeedsUpdate { older_version } => self.upgrade_vocabulary(definition, older_version), + VocabularyCheck::PresentButMissingAttributes { attributes } => self.install_attributes_for(definition, attributes), + VocabularyCheck::PresentButTooNew { newer_version } => Err(ErrorKind::ExistingVocabularyTooNew(definition.name.to_string(), newer_version.version, definition.version).into()), + } + } +} + +impl<'a, 'c> VocabularyMechanics for InProgress<'a, 'c> { + /// Turn the vocabulary into datoms, transact them, and on success return the outcome. + fn install_vocabulary(&mut self, definition: &Definition) -> Result { + let (terms, tempids) = definition.description(self)?; + self.transact_terms(terms, tempids)?; + Ok(VocabularyOutcome::Installed) + } + + fn install_attributes_for<'definition>(&mut self, definition: &'definition Definition, attributes: Vec<&'definition (NamespacedKeyword, Attribute)>) -> Result { + let (terms, tempids) = definition.description_for_attributes(&attributes, self)?; + self.transact_terms(terms, tempids)?; + Ok(VocabularyOutcome::InstalledMissingAttributes) + } + + /// Turn the declarative parts of the vocabulary into alterations. Run the 'pre' steps. + /// Transact the changes. Run the 'post' steps. Return the result and the new `InProgress`! + fn upgrade_vocabulary(&mut self, _definition: &Definition, _from_version: Vocabulary) -> Result { + unimplemented!(); + // TODO + // Ok(VocabularyOutcome::Installed) + } +} + +impl HasVocabularies for T where T: HasSchema + Queryable { + fn read_vocabulary_named(&self, name: &NamespacedKeyword) -> Result> { + if let Some(entid) = self.get_entid(name) { + match self.lookup_value_for_attribute(entid, &DB_SCHEMA_VERSION)? { + None => Ok(None), + Some(TypedValue::Long(version)) + if version > 0 && (version < u32::max_value() as i64) => { + let version = version as u32; + let attributes = self.lookup_values_for_attribute(entid, &DB_SCHEMA_ATTRIBUTE)? + .into_iter() + .filter_map(|a| { + if let TypedValue::Ref(a) = a { + self.attribute_for_entid(a) + .cloned() + .map(|attr| (a, attr)) + } else { + None + } + }) + .collect(); + Ok(Some(Vocabulary { + entity: entid.into(), + version: version, + attributes: attributes, + })) + }, + Some(_) => bail!(ErrorKind::InvalidVocabularyVersion), + } + } else { + Ok(None) + } + } + + fn read_vocabularies(&self) -> Result { + // This would be way easier with pull expressions. #110. + let versions: BTreeMap = + self.q_once(r#"[:find ?vocab ?version + :where [?vocab :db.schema/version ?version]]"#, None) + .into_rel_result()? + .into_iter() + .filter_map(|v| + match (&v[0], &v[1]) { + (&TypedValue::Ref(vocab), &TypedValue::Long(version)) + if version > 0 && (version < u32::max_value() as i64) => Some((vocab, version as u32)), + (_, _) => None, + }) + .collect(); + + let mut attributes = BTreeMap::>::new(); + let pairs = + self.q_once("[:find ?vocab ?attr :where [?vocab :db.schema/attribute ?attr]]", None) + .into_rel_result()? + .into_iter() + .filter_map(|v| { + match (&v[0], &v[1]) { + (&TypedValue::Ref(vocab), &TypedValue::Ref(attr)) => Some((vocab, attr)), + (_, _) => None, + } + }); + + // TODO: validate that attributes.keys is a subset of versions.keys. + for (vocab, attr) in pairs { + if let Some(attribute) = self.attribute_for_entid(attr).cloned() { + attributes.entry(vocab).or_insert(Vec::new()).push((attr, attribute)); + } + } + + // TODO: return more errors? + + // We walk versions first in order to support vocabularies with no attributes. + Ok(Vocabularies(versions.into_iter().filter_map(|(vocab, version)| { + // Get the name. + self.get_ident(vocab).cloned() + .map(|name| { + let attrs = attributes.remove(&vocab).unwrap_or(vec![]); + (name.clone(), Vocabulary { + entity: vocab, + version: version, + attributes: attrs, + }) + }) + }).collect())) + } +} + +#[cfg(test)] +mod tests { + use ::{ + NamespacedKeyword, + Conn, + new_connection, + }; + + use super::HasVocabularies; + + #[test] + fn test_read_vocabularies() { + let mut sqlite = new_connection("").expect("could open conn"); + let mut conn = Conn::connect(&mut sqlite).expect("could open store"); + let vocabularies = conn.begin_read(&mut sqlite).expect("in progress") + .read_vocabularies().expect("OK"); + assert_eq!(vocabularies.len(), 1); + let core = vocabularies.get(&NamespacedKeyword::new("db.schema", "core")).expect("exists"); + assert_eq!(core.version, 1); + } + + #[test] + fn test_core_schema() { + let mut c = new_connection("").expect("could open conn"); + let mut conn = Conn::connect(&mut c).expect("could open store"); + let in_progress = conn.begin_transaction(&mut c).expect("in progress"); + let vocab = in_progress.read_vocabularies().expect("vocabulary"); + assert_eq!(1, vocab.len()); + assert_eq!(1, vocab.get(&NamespacedKeyword::new("db.schema", "core")).expect("core vocab").version); + } +} diff --git a/tests/vocabulary.rs b/tests/vocabulary.rs new file mode 100644 index 00000000..33825586 --- /dev/null +++ b/tests/vocabulary.rs @@ -0,0 +1,270 @@ +// 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. + +#[macro_use] +extern crate lazy_static; + +extern crate mentat; +extern crate mentat_core; +extern crate mentat_db; +extern crate rusqlite; + +use mentat::vocabulary; + +use mentat::vocabulary::{ + VersionedStore, + VocabularyCheck, + VocabularyOutcome, +}; + +use mentat::query::IntoResult; + +use mentat_core::{ + HasSchema, +}; + +use mentat::{ + Conn, + NamespacedKeyword, + Queryable, + TypedValue, + ValueType, +}; + +use mentat::entity_builder::BuildTerms; + +use mentat::errors::{ + Error, + ErrorKind, +}; + +lazy_static! { + static ref FOO_NAME: NamespacedKeyword = { + NamespacedKeyword::new("foo", "name") + }; + + static ref FOO_MOMENT: NamespacedKeyword = { + NamespacedKeyword::new("foo", "moment") + }; + + static ref FOO_VOCAB: vocabulary::Definition = { + vocabulary::Definition { + name: NamespacedKeyword::new("org.mozilla", "foo"), + version: 1, + attributes: vec![ + (FOO_NAME.clone(), + vocabulary::AttributeBuilder::default() + .value_type(ValueType::String) + .multival(false) + .unique(vocabulary::attribute::Unique::Identity) + .build()), + (FOO_MOMENT.clone(), + vocabulary::AttributeBuilder::default() + .value_type(ValueType::Instant) + .multival(false) + .index(true) + .build()), + ] + } + }; +} + +// Do some work with the appropriate level of paranoia for a shared system. +fn be_paranoid(conn: &mut Conn, sqlite: &mut rusqlite::Connection, name: TypedValue, moment: TypedValue) { + let mut in_progress = conn.begin_transaction(sqlite).expect("begun successfully"); + assert!(in_progress.verify_core_schema().is_ok()); + assert!(in_progress.ensure_vocabulary(&FOO_VOCAB).is_ok()); + + let a_moment = in_progress.attribute_for_ident(&FOO_MOMENT).expect("exists").1; + let a_name = in_progress.attribute_for_ident(&FOO_NAME).expect("exists").1; + + let builder = in_progress.builder(); + let mut entity = builder.describe_tempid("s"); + entity.add(a_name, name).expect("added"); + entity.add(a_moment, moment).expect("added"); + assert!(entity.commit().is_ok()); // Discard the TxReport. +} + +#[test] +fn test_real_world() { + let mut sqlite = mentat_db::db::new_connection("").unwrap(); + let mut conn = Conn::connect(&mut sqlite).unwrap(); + + let alice: TypedValue = TypedValue::typed_string("Alice"); + let barbara: TypedValue = TypedValue::typed_string("Barbara"); + let now: TypedValue = TypedValue::current_instant(); + + be_paranoid(&mut conn, &mut sqlite, alice.clone(), now.clone()); + be_paranoid(&mut conn, &mut sqlite, barbara.clone(), now.clone()); + + let results = conn.q_once(&mut sqlite, r#"[:find ?name ?when + :order (asc ?name) + :where [?x :foo/name ?name] + [?x :foo/moment ?when] + ]"#, + None) + .into_rel_result() + .expect("query succeeded"); + assert_eq!(results, + vec![vec![alice, now.clone()], vec![barbara, now.clone()]]); +} + +#[test] +fn test_add_vocab() { + let bar = vocabulary::AttributeBuilder::default() + .value_type(ValueType::Instant) + .multival(false) + .index(true) + .build(); + let baz = vocabulary::AttributeBuilder::default() + .value_type(ValueType::String) + .multival(true) + .fulltext(true) + .build(); + let bar_only = vec![ + (NamespacedKeyword::new("foo", "bar"), bar.clone()), + ]; + let baz_only = vec![ + (NamespacedKeyword::new("foo", "baz"), baz.clone()), + ]; + let bar_and_baz = vec![ + (NamespacedKeyword::new("foo", "bar"), bar.clone()), + (NamespacedKeyword::new("foo", "baz"), baz.clone()), + ]; + + let foo_v1_a = vocabulary::Definition { + name: NamespacedKeyword::new("org.mozilla", "foo"), + version: 1, + attributes: bar_only.clone(), + }; + + let foo_v1_b = vocabulary::Definition { + name: NamespacedKeyword::new("org.mozilla", "foo"), + version: 1, + attributes: bar_and_baz.clone(), + }; + + let mut sqlite = mentat_db::db::new_connection("").unwrap(); + let mut conn = Conn::connect(&mut sqlite).unwrap(); + + let foo_version_query = r#"[:find [?version ?aa] + :where + [:org.mozilla/foo :db.schema/version ?version] + [:org.mozilla/foo :db.schema/attribute ?a] + [?a :db/ident ?aa]]"#; + let foo_attributes_query = r#"[:find [?aa ...] + :where + [:org.mozilla/foo :db.schema/attribute ?a] + [?a :db/ident ?aa]]"#; + + // Scoped borrow of `conn`. + { + let mut in_progress = conn.begin_transaction(&mut sqlite).expect("begun successfully"); + + assert!(in_progress.verify_core_schema().is_ok()); + assert_eq!(VocabularyCheck::NotPresent, in_progress.check_vocabulary(&foo_v1_a).expect("check completed")); + assert_eq!(VocabularyCheck::NotPresent, in_progress.check_vocabulary(&foo_v1_b).expect("check completed")); + + // If we install v1.a, then it will succeed. + assert_eq!(VocabularyOutcome::Installed, in_progress.ensure_vocabulary(&foo_v1_a).expect("ensure succeeded")); + + // Now we can query to get the vocab. + let ver_attr = + in_progress.q_once(foo_version_query, None) + .into_tuple_result() + .expect("query returns") + .expect("a result"); + assert_eq!(ver_attr[0], TypedValue::Long(1)); + assert_eq!(ver_attr[1], TypedValue::typed_ns_keyword("foo", "bar")); + + // If we commit, it'll stick around. + in_progress.commit().expect("commit succeeded"); + } + + // It's still there. + let ver_attr = + conn.q_once(&mut sqlite, + foo_version_query, + None) + .into_tuple_result() + .expect("query returns") + .expect("a result"); + assert_eq!(ver_attr[0], TypedValue::Long(1)); + assert_eq!(ver_attr[1], TypedValue::typed_ns_keyword("foo", "bar")); + + // Scoped borrow of `conn`. + { + let mut in_progress = conn.begin_transaction(&mut sqlite).expect("begun successfully"); + + // Subsequently ensuring v1.a again will succeed with no work done. + assert_eq!(VocabularyCheck::Present, in_progress.check_vocabulary(&foo_v1_a).expect("check completed")); + + // Checking for v1.b will say that we have work to do. + assert_eq!(VocabularyCheck::PresentButMissingAttributes { + attributes: vec![&baz_only[0]], + }, in_progress.check_vocabulary(&foo_v1_b).expect("check completed")); + + // Ensuring v1.b will succeed. + assert_eq!(VocabularyOutcome::InstalledMissingAttributes, + in_progress.ensure_vocabulary(&foo_v1_b).expect("ensure succeeded")); + + // Checking v1.a or v1.b again will still succeed with no work done. + assert_eq!(VocabularyCheck::Present, in_progress.check_vocabulary(&foo_v1_a).expect("check completed")); + assert_eq!(VocabularyCheck::Present, in_progress.check_vocabulary(&foo_v1_b).expect("check completed")); + + // Ensuring again does nothing. + assert_eq!(VocabularyOutcome::Existed, in_progress.ensure_vocabulary(&foo_v1_b).expect("ensure succeeded")); + in_progress.commit().expect("commit succeeded"); + } + + // We have both attributes. + let actual_attributes = + conn.q_once(&mut sqlite, + foo_attributes_query, + None) + .into_coll_result() + .expect("query returns"); + assert_eq!(actual_attributes, + vec![ + TypedValue::typed_ns_keyword("foo", "bar"), + TypedValue::typed_ns_keyword("foo", "baz"), + ]); + + // Now let's modify our vocabulary without bumping the version. This is invalid and will result + // in an error. + let malformed_baz = vocabulary::AttributeBuilder::default() + .value_type(ValueType::Instant) + .multival(true) + .build(); + let bar_and_malformed_baz = vec![ + (NamespacedKeyword::new("foo", "bar"), bar), + (NamespacedKeyword::new("foo", "baz"), malformed_baz.clone()), + ]; + let foo_v1_malformed = vocabulary::Definition { + name: NamespacedKeyword::new("org.mozilla", "foo"), + version: 1, + attributes: bar_and_malformed_baz.clone(), + }; + + // Scoped borrow of `conn`. + { + let mut in_progress = conn.begin_transaction(&mut sqlite).expect("begun successfully"); + match in_progress.ensure_vocabulary(&foo_v1_malformed) { + Result::Err(Error(ErrorKind::ConflictingAttributeDefinitions(vocab, version, attr, theirs, ours), _)) => { + assert_eq!(vocab.as_str(), ":org.mozilla/foo"); + assert_eq!(attr.as_str(), ":foo/baz"); + assert_eq!(version, 1); + assert_eq!(&theirs, &baz); + assert_eq!(&ours, &malformed_baz); + }, + _ => panic!(), + } + } +}