mentat/tests/vocabulary.rs
2018-04-03 15:21:02 -07:00

514 lines
19 KiB
Rust

// 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;
#[macro_use]
extern crate mentat;
extern crate mentat_core;
extern crate mentat_db;
extern crate rusqlite;
use mentat::vocabulary;
use mentat::vocabulary::{
VersionedStore,
VocabularyCheck,
VocabularyOutcome,
VocabularyProvider,
};
use mentat::query::IntoResult;
use mentat_core::{
HasSchema,
};
// To check our working.
use mentat_db::AttributeValidation;
use mentat::{
Conn,
NamespacedKeyword,
Queryable,
Store,
TypedValue,
ValueType,
};
use mentat::entity_builder::BuildTerms;
use mentat::errors::{
Error,
ErrorKind,
};
lazy_static! {
static ref FOO_NAME: NamespacedKeyword = {
kw!(:foo/name)
};
static ref FOO_MOMENT: NamespacedKeyword = {
kw!(:foo/moment)
};
static ref FOO_VOCAB: vocabulary::Definition = {
vocabulary::Definition {
name: kw!(:org.mozilla/foo),
version: 1,
attributes: vec![
(FOO_NAME.clone(),
vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::String)
.multival(false)
.unique(vocabulary::attribute::Unique::Identity)
.build()),
(FOO_MOMENT.clone(),
vocabulary::AttributeBuilder::helpful()
.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_default_attributebuilder_complains() {
// ::new is helpful. ::default is not.
assert!(vocabulary::AttributeBuilder::default()
.value_type(ValueType::String)
.multival(true)
.fulltext(true)
.build()
.validate(|| "Foo".to_string())
.is_err());
assert!(vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::String)
.multival(true)
.fulltext(true)
.build()
.validate(|| "Foo".to_string())
.is_ok());
}
#[test]
fn test_add_vocab() {
let bar = vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::Instant)
.multival(false)
.index(true)
.build();
let baz = vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::String)
.multival(true)
.fulltext(true)
.build();
let bar_only = vec![
(kw!(:foo/bar), bar.clone()),
];
let baz_only = vec![
(kw!(:foo/baz), baz.clone()),
];
let bar_and_baz = vec![
(kw!(:foo/bar), bar.clone()),
(kw!(:foo/baz), baz.clone()),
];
let foo_v1_a = vocabulary::Definition {
name: kw!(:org.mozilla/foo),
version: 1,
attributes: bar_only.clone(),
};
let foo_v1_b = vocabulary::Definition {
name: kw!(: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![
(kw!(:foo/bar), bar),
(kw!(:foo/baz), malformed_baz.clone()),
];
let foo_v1_malformed = vocabulary::Definition {
name: kw!(: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!(),
}
}
// Some alterations -- cardinality/one to cardinality/many, unique to weaker unique or
// no unique, unindexed to indexed -- can be applied automatically, so long as you
// bump the version number.
let multival_bar = vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::Instant)
.multival(true)
.index(true)
.build();
let multival_bar_and_baz = vec![
(kw!(:foo/bar), multival_bar),
(kw!(:foo/baz), baz.clone()),
];
let altered_vocabulary = vocabulary::Definition {
name: kw!(:org.mozilla/foo),
version: 2,
attributes: multival_bar_and_baz,
};
// foo/bar starts single-valued.
assert_eq!(false, conn.current_schema().attribute_for_ident(&kw!(:foo/bar)).expect("attribute").0.multival);
// Scoped borrow of `conn`.
{
let mut in_progress = conn.begin_transaction(&mut sqlite).expect("begun successfully");
assert_eq!(in_progress.ensure_vocabulary(&altered_vocabulary).expect("success"),
VocabularyOutcome::Upgraded);
in_progress.commit().expect("commit succeeded");
}
// Now it's multi-valued.
assert_eq!(true, conn.current_schema().attribute_for_ident(&kw!(:foo/bar)).expect("attribute").0.multival);
}
// This is a real-world-style test that evolves a schema with data changes.
// We start with a basic vocabulary in three parts:
//
// Part 1 describes foods by name.
// Part 2 describes movies by title.
// Part 3 describes people: their names and heights, and their likes.
//
// We simulate three common migrations:
// - We made a trivial modeling error: movie names should not be unique.
// - We made a less trivial modeling error, one that can fail: food names should be unique so that
// we can more easily refer to them during writes.
// In order for this migration to succeed, we need to merge duplicates, then alter the schema --
// which we will do by introducing a new property in the same vocabulary, deprecating the old one
// -- then transact the transformed data.
// - We need to normalize some non-unique data: we recorded heights in inches when they should be
// in centimeters.
// - We need to normalize some unique data: food names should all be lowercase. Again, that can fail
// because of a uniqueness constraint. (We might know that it can't fail thanks to application
// restrictions, in which case we can treat this as we did the height alteration.)
// - We made a more significant modeling error: we used 'like' to identify both movies and foods,
// and we have decided that food preferences and movie preferences should be different attributes.
// We wish to split these up and deprecate the old attribute. In order to do so we need to retract
// all of the datoms that use the old attribute, transact new attributes _in both movies and foods_,
// then re-assert the data.
#[test]
fn test_upgrade_with_functions() {
let mut store = Store::open("").expect("open");
let food_v1 = vocabulary::Definition {
name: kw!(:org.mozilla/food),
version: 1,
attributes: vec![
(kw!(:food/name),
vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::String)
.multival(false)
.build()),
],
};
let movies_v1 = vocabulary::Definition {
name: kw!(:org.mozilla/movies),
version: 1,
attributes: vec![
(kw!(:movie/year),
vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::Long) // No need for Instant here.
.multival(false)
.build()),
(kw!(:movie/title),
vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::String)
.multival(false)
.unique(vocabulary::attribute::Unique::Identity)
.index(true)
.build()),
],
};
let people_v1 = vocabulary::Definition {
name: kw!(:org.mozilla/people),
version: 1,
attributes: vec![
(kw!(:person/name),
vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::String)
.multival(false)
.unique(vocabulary::attribute::Unique::Identity)
.index(true)
.build()),
(kw!(:person/height),
vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::Long)
.multival(false)
.build()),
(kw!(:person/likes),
vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::Ref)
.multival(true)
.build()),
],
};
// Apply v1 of each.
let v1_provider = VocabularyProvider {
pre: |_ip| Ok(()),
definitions: vec![
food_v1.clone(),
movies_v1.clone(),
people_v1.clone(),
],
post: |_ip| Ok(()),
};
// Mutable borrow of store.
{
let mut in_progress = store.begin_transaction().expect("began");
in_progress.ensure_vocabularies(&v1_provider).expect("success");
// Also add some data. We do this in one transaction 'cos -- thanks to the modeling errors
// we are about to fix! -- it's a little awkward to make references to entities without
// unique attributes.
in_progress.transact(r#"[
{:movie/title "John Wick"
:movie/year 2014
:db/id "mjw"}
{:movie/title "Terminator 2: Judgment Day"
:movie/year 1991
:db/id "mt2"}
{:movie/title "Dune"
:db/id "md"
:movie/year 1984}
{:movie/title "Upstream Color"
:movie/year 2013
:db/id "muc"}
{:movie/title "Primer"
:db/id "mp"
:movie/year 2004}
;; No year: not yet released.
{:movie/title "The Modern Ocean"
:db/id "mtmo"}
{:food/name "Carrots" :db/id "fc"}
{:food/name "Weird blue worms" :db/id "fwbw"}
{:food/name "Spice" :db/id "fS"}
{:food/name "spice" :db/id "fs"}
;; Sam likes action movies, carrots, and lowercase spice.
{:person/name "Sam"
:person/height 64
:person/likes ["mjw", "mt2", "fc", "fs"]}
;; Beth likes thoughtful and weird movies, weird blue worms, and Spice.
{:person/name "Beth"
:person/height 68
:person/likes ["muc", "mp", "md", "fwbw", "fS"]}
]"#).expect("transacted");
in_progress.commit().expect("commit succeeded");
}
// Mutable borrow of store.
{
// Crap, there are several movies named Dune. We need to de-uniqify that attribute.
let movies_v2 = vocabulary::Definition {
name: kw!(:org.mozilla/movies),
version: 2,
attributes: vec![
(kw!(:movie/title),
vocabulary::AttributeBuilder::helpful()
.value_type(ValueType::String)
.multival(false)
.non_unique()
.index(true)
.build()),
],
};
let mut in_progress = store.begin_transaction().expect("began");
in_progress.ensure_vocabulary(&movies_v2).expect("success");
// We can now add another Dune movie: Denis Villeneuve's 2019 version.
// (Let's just pretend that it's been released, here in 2018!)
in_progress.transact(r#"[
{:movie/title "Dune"
:movie/year 2019}
]"#).expect("transact succeeded");
// And we can query both.
let years =
in_progress.q_once(r#"[:find [?year ...]
:where [?movie :movie/title "Dune"]
[?movie :movie/year ?year]
:order (asc ?year)]"#, None)
.into_coll_result()
.expect("coll");
assert_eq!(years, vec![TypedValue::Long(1984), TypedValue::Long(2019)]);
in_progress.commit().expect("commit succeeded");
}
}