Validate attributes installed after open. (#538) r=emily
Make AttributeBuilder optionally helpful, fix tests.
This commit is contained in:
parent
2614f498be
commit
37a7c9ea48
9 changed files with 100 additions and 17 deletions
18
db/src/db.rs
18
db/src/db.rs
|
@ -1421,6 +1421,24 @@ mod tests {
|
||||||
[200 :db.schema/attribute 101]]");
|
[200 :db.schema/attribute 101]]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unique is required!
|
||||||
|
#[test]
|
||||||
|
fn test_upsert_issue_538() {
|
||||||
|
let mut conn = TestConn::default();
|
||||||
|
assert_transact!(conn, "
|
||||||
|
[{:db/ident :person/name
|
||||||
|
:db/valueType :db.type/string
|
||||||
|
:db/cardinality :db.cardinality/many}
|
||||||
|
{:db/ident :person/age
|
||||||
|
:db/valueType :db.type/long
|
||||||
|
:db/cardinality :db.cardinality/one}
|
||||||
|
{:db/ident :person/email
|
||||||
|
:db/valueType :db.type/string
|
||||||
|
:db/unique :db.unique/identity
|
||||||
|
:db/cardinality :db.cardinality/many}]",
|
||||||
|
Err("bad schema assertion: :db/unique :db/unique_identity without :db/index true for entid: 65538"));
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: don't use :db/ident to test upserts!
|
// TODO: don't use :db/ident to test upserts!
|
||||||
#[test]
|
#[test]
|
||||||
fn test_upsert_vector() {
|
fn test_upsert_vector() {
|
||||||
|
|
|
@ -53,7 +53,10 @@ pub use bootstrap::{
|
||||||
USER0,
|
USER0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use schema::AttributeBuilder;
|
pub use schema::{
|
||||||
|
AttributeBuilder,
|
||||||
|
AttributeValidation,
|
||||||
|
};
|
||||||
|
|
||||||
pub use bootstrap::{
|
pub use bootstrap::{
|
||||||
CORE_SCHEMA_VERSION,
|
CORE_SCHEMA_VERSION,
|
||||||
|
|
|
@ -47,8 +47,10 @@ use mentat_core::{
|
||||||
TypedValue,
|
TypedValue,
|
||||||
ValueType,
|
ValueType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use schema::{
|
use schema::{
|
||||||
AttributeBuilder,
|
AttributeBuilder,
|
||||||
|
AttributeValidation,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An alteration to an attribute.
|
/// An alteration to an attribute.
|
||||||
|
@ -181,14 +183,20 @@ pub fn update_attribute_map_from_entid_triples<U>(attribute_map: &mut AttributeM
|
||||||
for (entid, builder) in builders.into_iter() {
|
for (entid, builder) in builders.into_iter() {
|
||||||
match attribute_map.entry(entid) {
|
match attribute_map.entry(entid) {
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
|
// Validate once…
|
||||||
builder.validate_install_attribute()
|
builder.validate_install_attribute()
|
||||||
.chain_err(|| ErrorKind::BadSchemaAssertion(format!("Schema alteration for new attribute with entid {} is not valid", entid)))?;
|
.chain_err(|| ErrorKind::BadSchemaAssertion(format!("Schema alteration for new attribute with entid {} is not valid", entid)))?;
|
||||||
|
|
||||||
|
// … and twice, now we have the Attribute.
|
||||||
|
let a = builder.build();
|
||||||
|
a.validate(|| entid.to_string())?;
|
||||||
entry.insert(builder.build());
|
entry.insert(builder.build());
|
||||||
attributes_installed.insert(entid);
|
attributes_installed.insert(entid);
|
||||||
},
|
},
|
||||||
|
|
||||||
Entry::Occupied(mut entry) => {
|
Entry::Occupied(mut entry) => {
|
||||||
builder.validate_alter_attribute()
|
builder.validate_alter_attribute()
|
||||||
.chain_err(|| ErrorKind::BadSchemaAssertion(format!("Schema alteration for existing attribute with entid {} is not valid", entid)))?;
|
.chain_err(|| ErrorKind::BadSchemaAssertion(format!("Schema alteration for existing attribute with entid {} is not valid", entid)))?;
|
||||||
let mutations = builder.mutate(entry.get_mut());
|
let mutations = builder.mutate(entry.get_mut());
|
||||||
attributes_altered.insert(entid, mutations);
|
attributes_altered.insert(entid, mutations);
|
||||||
},
|
},
|
||||||
|
|
|
@ -32,35 +32,48 @@ use metadata::{
|
||||||
AttributeAlteration,
|
AttributeAlteration,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Return `Ok(())` if `attribute_map` defines a valid Mentat schema.
|
pub trait AttributeValidation {
|
||||||
fn validate_attribute_map(entid_map: &EntidMap, attribute_map: &AttributeMap) -> Result<()> {
|
fn validate<F>(&self, ident: F) -> Result<()> where F: Fn() -> String;
|
||||||
for (entid, attribute) in attribute_map {
|
}
|
||||||
let ident = || entid_map.get(entid).map(|ident| ident.to_string()).unwrap_or(entid.to_string());
|
|
||||||
if attribute.unique == Some(attribute::Unique::Value) && !attribute.index {
|
impl AttributeValidation for Attribute {
|
||||||
|
fn validate<F>(&self, ident: F) -> Result<()> where F: Fn() -> String {
|
||||||
|
if self.unique == Some(attribute::Unique::Value) && !self.index {
|
||||||
bail!(ErrorKind::BadSchemaAssertion(format!(":db/unique :db/unique_value without :db/index true for entid: {}", ident())))
|
bail!(ErrorKind::BadSchemaAssertion(format!(":db/unique :db/unique_value without :db/index true for entid: {}", ident())))
|
||||||
}
|
}
|
||||||
if attribute.unique == Some(attribute::Unique::Identity) && !attribute.index {
|
if self.unique == Some(attribute::Unique::Identity) && !self.index {
|
||||||
|
println!("Unique identity without index. Bailing.");
|
||||||
bail!(ErrorKind::BadSchemaAssertion(format!(":db/unique :db/unique_identity without :db/index true for entid: {}", ident())))
|
bail!(ErrorKind::BadSchemaAssertion(format!(":db/unique :db/unique_identity without :db/index true for entid: {}", ident())))
|
||||||
}
|
}
|
||||||
if attribute.fulltext && attribute.value_type != ValueType::String {
|
if self.fulltext && self.value_type != ValueType::String {
|
||||||
bail!(ErrorKind::BadSchemaAssertion(format!(":db/fulltext true without :db/valueType :db.type/string for entid: {}", ident())))
|
bail!(ErrorKind::BadSchemaAssertion(format!(":db/fulltext true without :db/valueType :db.type/string for entid: {}", ident())))
|
||||||
}
|
}
|
||||||
if attribute.fulltext && !attribute.index {
|
if self.fulltext && !self.index {
|
||||||
bail!(ErrorKind::BadSchemaAssertion(format!(":db/fulltext true without :db/index true for entid: {}", ident())))
|
bail!(ErrorKind::BadSchemaAssertion(format!(":db/fulltext true without :db/index true for entid: {}", ident())))
|
||||||
}
|
}
|
||||||
if attribute.component && attribute.value_type != ValueType::Ref {
|
if self.component && self.value_type != ValueType::Ref {
|
||||||
bail!(ErrorKind::BadSchemaAssertion(format!(":db/isComponent true without :db/valueType :db.type/ref for entid: {}", ident())))
|
bail!(ErrorKind::BadSchemaAssertion(format!(":db/isComponent true without :db/valueType :db.type/ref for entid: {}", ident())))
|
||||||
}
|
}
|
||||||
// TODO: consider warning if we have :db/index true for :db/valueType :db.type/string,
|
// TODO: consider warning if we have :db/index true for :db/valueType :db.type/string,
|
||||||
// since this may be inefficient. More generally, we should try to drive complex
|
// since this may be inefficient. More generally, we should try to drive complex
|
||||||
// :db/valueType (string, uri, json in the future) users to opt-in to some hash-indexing
|
// :db/valueType (string, uri, json in the future) users to opt-in to some hash-indexing
|
||||||
// scheme, as discussed in https://github.com/mozilla/mentat/issues/69.
|
// scheme, as discussed in https://github.com/mozilla/mentat/issues/69.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `Ok(())` if `attribute_map` defines a valid Mentat schema.
|
||||||
|
fn validate_attribute_map(entid_map: &EntidMap, attribute_map: &AttributeMap) -> Result<()> {
|
||||||
|
for (entid, attribute) in attribute_map {
|
||||||
|
let ident = || entid_map.get(entid).map(|ident| ident.to_string()).unwrap_or(entid.to_string());
|
||||||
|
attribute.validate(ident)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone,Debug,Default,Eq,Hash,Ord,PartialOrd,PartialEq)]
|
#[derive(Clone,Debug,Default,Eq,Hash,Ord,PartialOrd,PartialEq)]
|
||||||
pub struct AttributeBuilder {
|
pub struct AttributeBuilder {
|
||||||
|
helpful: bool,
|
||||||
value_type: Option<ValueType>,
|
value_type: Option<ValueType>,
|
||||||
multival: Option<bool>,
|
multival: Option<bool>,
|
||||||
unique: Option<Option<attribute::Unique>>,
|
unique: Option<Option<attribute::Unique>>,
|
||||||
|
@ -70,6 +83,15 @@ pub struct AttributeBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributeBuilder {
|
impl AttributeBuilder {
|
||||||
|
/// Make a new AttributeBuilder for human consumption: it will help you
|
||||||
|
/// by flipping relevant flags.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
AttributeBuilder {
|
||||||
|
helpful: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn value_type<'a>(&'a mut self, value_type: ValueType) -> &'a mut Self {
|
pub fn value_type<'a>(&'a mut self, value_type: ValueType) -> &'a mut Self {
|
||||||
self.value_type = Some(value_type);
|
self.value_type = Some(value_type);
|
||||||
self
|
self
|
||||||
|
@ -81,6 +103,9 @@ impl AttributeBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unique<'a>(&'a mut self, unique: attribute::Unique) -> &'a mut Self {
|
pub fn unique<'a>(&'a mut self, unique: attribute::Unique) -> &'a mut Self {
|
||||||
|
if self.helpful && unique == attribute::Unique::Identity {
|
||||||
|
self.index = Some(true);
|
||||||
|
}
|
||||||
self.unique = Some(Some(unique));
|
self.unique = Some(Some(unique));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -92,6 +117,9 @@ impl AttributeBuilder {
|
||||||
|
|
||||||
pub fn fulltext<'a>(&'a mut self, fulltext: bool) -> &'a mut Self {
|
pub fn fulltext<'a>(&'a mut self, fulltext: bool) -> &'a mut Self {
|
||||||
self.fulltext = Some(fulltext);
|
self.fulltext = Some(fulltext);
|
||||||
|
if self.helpful && fulltext {
|
||||||
|
self.index = Some(true);
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -754,6 +754,7 @@ mod testing {
|
||||||
associate_ident(&mut schema, NamespacedKeyword::new("foo", "bar"), 99);
|
associate_ident(&mut schema, NamespacedKeyword::new("foo", "bar"), 99);
|
||||||
add_attribute(&mut schema, 99, Attribute {
|
add_attribute(&mut schema, 99, Attribute {
|
||||||
value_type: ValueType::String,
|
value_type: ValueType::String,
|
||||||
|
index: true,
|
||||||
fulltext: true,
|
fulltext: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,6 +45,7 @@ fn prepopulated_schema() -> Schema {
|
||||||
});
|
});
|
||||||
add_attribute(&mut schema, 66, Attribute {
|
add_attribute(&mut schema, 66, Attribute {
|
||||||
value_type: ValueType::String,
|
value_type: ValueType::String,
|
||||||
|
index: true,
|
||||||
fulltext: true,
|
fulltext: true,
|
||||||
multival: true,
|
multival: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
//! version: 1,
|
//! version: 1,
|
||||||
//! attributes: vec![
|
//! attributes: vec![
|
||||||
//! (kw!(:link/title),
|
//! (kw!(:link/title),
|
||||||
//! vocabulary::AttributeBuilder::default()
|
//! vocabulary::AttributeBuilder::new()
|
||||||
//! .value_type(ValueType::String)
|
//! .value_type(ValueType::String)
|
||||||
//! .multival(false)
|
//! .multival(false)
|
||||||
//! .fulltext(true)
|
//! .fulltext(true)
|
||||||
|
|
|
@ -339,6 +339,7 @@ fn test_fulltext() {
|
||||||
[:db/add "s" :db/ident :foo/fts]
|
[:db/add "s" :db/ident :foo/fts]
|
||||||
[:db/add "s" :db/valueType :db.type/string]
|
[:db/add "s" :db/valueType :db.type/string]
|
||||||
[:db/add "s" :db/fulltext true]
|
[:db/add "s" :db/fulltext true]
|
||||||
|
[:db/add "s" :db/index true]
|
||||||
[:db/add "s" :db/cardinality :db.cardinality/many]
|
[:db/add "s" :db/cardinality :db.cardinality/many]
|
||||||
]"#).unwrap();
|
]"#).unwrap();
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,9 @@ use mentat_core::{
|
||||||
HasSchema,
|
HasSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// To check our working.
|
||||||
|
use mentat_db::AttributeValidation;
|
||||||
|
|
||||||
use mentat::{
|
use mentat::{
|
||||||
Conn,
|
Conn,
|
||||||
NamespacedKeyword,
|
NamespacedKeyword,
|
||||||
|
@ -61,13 +64,13 @@ lazy_static! {
|
||||||
version: 1,
|
version: 1,
|
||||||
attributes: vec![
|
attributes: vec![
|
||||||
(FOO_NAME.clone(),
|
(FOO_NAME.clone(),
|
||||||
vocabulary::AttributeBuilder::default()
|
vocabulary::AttributeBuilder::new()
|
||||||
.value_type(ValueType::String)
|
.value_type(ValueType::String)
|
||||||
.multival(false)
|
.multival(false)
|
||||||
.unique(vocabulary::attribute::Unique::Identity)
|
.unique(vocabulary::attribute::Unique::Identity)
|
||||||
.build()),
|
.build()),
|
||||||
(FOO_MOMENT.clone(),
|
(FOO_MOMENT.clone(),
|
||||||
vocabulary::AttributeBuilder::default()
|
vocabulary::AttributeBuilder::new()
|
||||||
.value_type(ValueType::Instant)
|
.value_type(ValueType::Instant)
|
||||||
.multival(false)
|
.multival(false)
|
||||||
.index(true)
|
.index(true)
|
||||||
|
@ -117,14 +120,34 @@ fn test_real_world() {
|
||||||
vec![vec![alice, now.clone()], vec![barbara, now.clone()]]);
|
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::new()
|
||||||
|
.value_type(ValueType::String)
|
||||||
|
.multival(true)
|
||||||
|
.fulltext(true)
|
||||||
|
.build()
|
||||||
|
.validate(|| "Foo".to_string())
|
||||||
|
.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_add_vocab() {
|
fn test_add_vocab() {
|
||||||
let bar = vocabulary::AttributeBuilder::default()
|
let bar = vocabulary::AttributeBuilder::new()
|
||||||
.value_type(ValueType::Instant)
|
.value_type(ValueType::Instant)
|
||||||
.multival(false)
|
.multival(false)
|
||||||
.index(true)
|
.index(true)
|
||||||
.build();
|
.build();
|
||||||
let baz = vocabulary::AttributeBuilder::default()
|
let baz = vocabulary::AttributeBuilder::new()
|
||||||
.value_type(ValueType::String)
|
.value_type(ValueType::String)
|
||||||
.multival(true)
|
.multival(true)
|
||||||
.fulltext(true)
|
.fulltext(true)
|
||||||
|
|
Loading…
Reference in a new issue