diff --git a/db/src/db.rs b/db/src/db.rs index a199cb3c..c9cf8336 100644 --- a/db/src/db.rs +++ b/db/src/db.rs @@ -743,6 +743,10 @@ impl MentatStoring for rusqlite::Connection { value_type_tag0 SMALLINT NOT NULL, added0 TINYINT NOT NULL, flags0 TINYINT NOT NULL)"#, + + // We create this unique index so that it's impossible to violate a cardinality constraint + // within a transaction. + r#"CREATE UNIQUE INDEX IF NOT EXISTS temp.inexact_searches_unique ON inexact_searches (e0, a0) WHERE added0 = 1"#, r#"DROP TABLE IF EXISTS temp.search_results"#, // TODO: don't encode search_type as a STRING. This is explicit and much easier to read // than another flag, so we'll do it to start, and optimize later. @@ -824,6 +828,7 @@ impl MentatStoring for rusqlite::Connection { let s: String = if search_type == SearchType::Exact { format!("INSERT INTO temp.exact_searches (e0, a0, v0, value_type_tag0, added0, flags0) VALUES {}", values) } else { + // This will err for duplicates within the tx. format!("INSERT INTO temp.inexact_searches (e0, a0, v0, value_type_tag0, added0, flags0) VALUES {}", values) }; @@ -2271,4 +2276,42 @@ mod tests { "[{:test/_dangling 1.23}]", Err("EDN value \'1.23\' is not the expected Mentat value type Ref")); } + + #[test] + fn test_cardinality_one_violation_existing_entity() { + let mut conn = TestConn::default(); + + // Start by installing a few attributes. + assert_transact!(conn, r#"[ + [:db/add 111 :db/ident :test/one] + [:db/add 111 :db/valueType :db.type/long] + [:db/add 111 :db/cardinality :db.cardinality/one] + [:db/add 112 :db/ident :test/unique] + [:db/add 112 :db/index true] + [:db/add 112 :db/valueType :db.type/string] + [:db/add 112 :db/cardinality :db.cardinality/one] + [:db/add 112 :db/unique :db.unique/identity] + ]"#); + + assert_transact!(conn, r#"[ + [:db/add "foo" :test/unique "x"] + ]"#); + + // You can try to assert two values for the same entity and attribute, + // but you'll get an error. + assert_transact!(conn, r#"[ + [:db/add "foo" :test/unique "x"] + [:db/add "foo" :test/one 123] + [:db/add "bar" :test/unique "x"] + [:db/add "bar" :test/one 124] + ]"#, + Err("Could not insert non-fts one statements into temporary search table!")); + + // It also fails for map notation. + assert_transact!(conn, r#"[ + {:test/unique "x", :test/one 123} + {:test/unique "x", :test/one 124} + ]"#, + Err("Could not insert non-fts one statements into temporary search table!")); + } } diff --git a/tx/src/entities.rs b/tx/src/entities.rs index d6e078b3..dddf5a5a 100644 --- a/tx/src/entities.rs +++ b/tx/src/entities.rs @@ -104,7 +104,7 @@ pub enum EntidOrLookupRefOrTempId { TempId(TempId), } -#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)] pub enum OpType { Add, Retract,