3389 lines
139 KiB
Rust
3389 lines
139 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.
|
|
|
|
#![allow(dead_code)]
|
|
|
|
use failure::ResultExt;
|
|
|
|
use std::collections::hash_map::Entry;
|
|
use std::collections::HashMap;
|
|
use std::iter::{once, repeat};
|
|
use std::ops::Range;
|
|
use std::path::Path;
|
|
|
|
use itertools;
|
|
use itertools::Itertools;
|
|
use rusqlite;
|
|
use rusqlite::limits::Limit;
|
|
use rusqlite::types::{ToSql, ToSqlOutput};
|
|
use rusqlite::TransactionBehavior;
|
|
|
|
use bootstrap;
|
|
use {repeat_values, to_namespaced_keyword};
|
|
|
|
use edn::{DateTime, Utc, Uuid, Value};
|
|
|
|
use entids;
|
|
|
|
use core_traits::{attribute, Attribute, AttributeBitFlags, Entid, TypedValue, ValueType};
|
|
|
|
use mentat_core::{AttributeMap, FromMicros, IdentMap, Schema, ToMicros, ValueRc};
|
|
|
|
use db_traits::errors::{DbErrorKind, Result};
|
|
|
|
use metadata;
|
|
use schema::SchemaBuilding;
|
|
use tx::transact;
|
|
use types::{AVMap, AVPair, Partition, PartitionMap, DB};
|
|
|
|
use std::convert::TryInto;
|
|
use watcher::NullWatcher;
|
|
|
|
// In PRAGMA foo='bar', `'bar'` must be a constant string (it cannot be a
|
|
// bound parameter), so we need to escape manually. According to
|
|
// https://www.sqlite.org/faq.html, the only character that must be escaped is
|
|
// the single quote, which is escaped by placing two single quotes in a row.
|
|
fn escape_string_for_pragma(s: &str) -> String {
|
|
s.replace("'", "''")
|
|
}
|
|
|
|
fn make_connection(
|
|
uri: &Path,
|
|
maybe_encryption_key: Option<&str>,
|
|
) -> rusqlite::Result<rusqlite::Connection> {
|
|
let conn = match uri.to_string_lossy().len() {
|
|
0 => rusqlite::Connection::open_in_memory()?,
|
|
_ => rusqlite::Connection::open(uri)?,
|
|
};
|
|
|
|
let page_size = 32768;
|
|
|
|
let initial_pragmas = if let Some(encryption_key) = maybe_encryption_key {
|
|
assert!(
|
|
cfg!(feature = "sqlcipher"),
|
|
"This function shouldn't be called with a key unless we have sqlcipher support"
|
|
);
|
|
// Important: The `cipher_page_size` cannot be changed without breaking
|
|
// the ability to open databases that were written when using a
|
|
// different `cipher_page_size`. Additionally, it (AFAICT) must be a
|
|
// positive multiple of `page_size`. We use the same value for both here.
|
|
format!(
|
|
"
|
|
PRAGMA key='{}';
|
|
PRAGMA cipher_page_size={};
|
|
",
|
|
escape_string_for_pragma(encryption_key),
|
|
page_size
|
|
)
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
// See https://github.com/mozilla/mentat/issues/505 for details on temp_store
|
|
// pragma and how it might interact together with consumers such as Firefox.
|
|
// temp_store=2 is currently present to force SQLite to store temp files in memory.
|
|
// Some of the platforms we support do not have a tmp partition (e.g. Android)
|
|
// necessary to store temp files on disk. Ideally, consumers should be able to
|
|
// override this behaviour (see issue 505).
|
|
conn.execute_batch(&format!(
|
|
"
|
|
{}
|
|
PRAGMA journal_mode=wal;
|
|
PRAGMA wal_autocheckpoint=32;
|
|
PRAGMA journal_size_limit=3145728;
|
|
PRAGMA foreign_keys=ON;
|
|
PRAGMA temp_store=2;
|
|
",
|
|
initial_pragmas
|
|
))?;
|
|
|
|
Ok(conn)
|
|
}
|
|
|
|
pub fn new_connection<T>(uri: T) -> rusqlite::Result<rusqlite::Connection>
|
|
where
|
|
T: AsRef<Path>,
|
|
{
|
|
make_connection(uri.as_ref(), None)
|
|
}
|
|
|
|
#[cfg(feature = "sqlcipher")]
|
|
pub fn new_connection_with_key<P, S>(
|
|
uri: P,
|
|
encryption_key: S,
|
|
) -> rusqlite::Result<rusqlite::Connection>
|
|
where
|
|
P: AsRef<Path>,
|
|
S: AsRef<str>,
|
|
{
|
|
make_connection(uri.as_ref(), Some(encryption_key.as_ref()))
|
|
}
|
|
|
|
#[cfg(feature = "sqlcipher")]
|
|
pub fn change_encryption_key<S>(
|
|
conn: &rusqlite::Connection,
|
|
encryption_key: S,
|
|
) -> rusqlite::Result<()>
|
|
where
|
|
S: AsRef<str>,
|
|
{
|
|
let escaped = escape_string_for_pragma(encryption_key.as_ref());
|
|
// `conn.execute` complains that this returns a result, and using a query
|
|
// for it requires more boilerplate.
|
|
conn.execute_batch(&format!("PRAGMA rekey = '{}';", escaped))
|
|
}
|
|
|
|
/// Version history:
|
|
///
|
|
/// 1: initial Rust Mentat schema.
|
|
pub const CURRENT_VERSION: i32 = 1;
|
|
|
|
/// MIN_SQLITE_VERSION should be changed when there's a new minimum version of sqlite required
|
|
/// for the project to work.
|
|
const MIN_SQLITE_VERSION: i32 = 3008000;
|
|
|
|
const TRUE: &'static bool = &true;
|
|
const FALSE: &'static bool = &false;
|
|
|
|
/// Turn an owned bool into a static reference to a bool.
|
|
///
|
|
/// `rusqlite` is designed around references to values; this lets us use computed bools easily.
|
|
#[inline(always)]
|
|
fn to_bool_ref(x: bool) -> &'static bool {
|
|
if x {
|
|
TRUE
|
|
} else {
|
|
FALSE
|
|
}
|
|
}
|
|
|
|
lazy_static! {
|
|
/// SQL statements to be executed, in order, to create the Mentat SQL schema (version 1).
|
|
#[cfg_attr(rustfmt, rustfmt_skip)]
|
|
static ref V1_STATEMENTS: Vec<&'static str> = { vec![
|
|
r#"CREATE TABLE datoms (e INTEGER NOT NULL, a SMALLINT NOT NULL, v BLOB NOT NULL, tx INTEGER NOT NULL,
|
|
value_type_tag SMALLINT NOT NULL,
|
|
index_avet TINYINT NOT NULL DEFAULT 0, index_vaet TINYINT NOT NULL DEFAULT 0,
|
|
index_fulltext TINYINT NOT NULL DEFAULT 0,
|
|
unique_value TINYINT NOT NULL DEFAULT 0)"#,
|
|
r#"CREATE UNIQUE INDEX idx_datoms_eavt ON datoms (e, a, value_type_tag, v)"#,
|
|
r#"CREATE UNIQUE INDEX idx_datoms_aevt ON datoms (a, e, value_type_tag, v)"#,
|
|
|
|
// Opt-in index: only if a has :db/index true.
|
|
r#"CREATE UNIQUE INDEX idx_datoms_avet ON datoms (a, value_type_tag, v, e) WHERE index_avet IS NOT 0"#,
|
|
|
|
// Opt-in index: only if a has :db/valueType :db.type/ref. No need for tag here since all
|
|
// indexed elements are refs.
|
|
r#"CREATE UNIQUE INDEX idx_datoms_vaet ON datoms (v, a, e) WHERE index_vaet IS NOT 0"#,
|
|
|
|
// Opt-in index: only if a has :db/fulltext true; thus, it has :db/valueType :db.type/string,
|
|
// which is not :db/valueType :db.type/ref. That is, index_vaet and index_fulltext are mutually
|
|
// exclusive.
|
|
r#"CREATE INDEX idx_datoms_fulltext ON datoms (value_type_tag, v, a, e) WHERE index_fulltext IS NOT 0"#,
|
|
|
|
// TODO: possibly remove this index. :db.unique/{value,identity} should be asserted by the
|
|
// transactor in all cases, but the index may speed up some of SQLite's query planning. For now,
|
|
// it serves to validate the transactor implementation. Note that tag is needed here to
|
|
// differentiate, e.g., keywords and strings.
|
|
r#"CREATE UNIQUE INDEX idx_datoms_unique_value ON datoms (a, value_type_tag, v) WHERE unique_value IS NOT 0"#,
|
|
|
|
r#"CREATE TABLE timelined_transactions (e INTEGER NOT NULL, a SMALLINT NOT NULL, v BLOB NOT NULL, tx INTEGER NOT NULL, added TINYINT NOT NULL DEFAULT 1, value_type_tag SMALLINT NOT NULL, timeline TINYINT NOT NULL DEFAULT 0)"#,
|
|
r#"CREATE INDEX idx_timelined_transactions_timeline ON timelined_transactions (timeline)"#,
|
|
r#"CREATE VIEW transactions AS SELECT e, a, v, value_type_tag, tx, added FROM timelined_transactions WHERE timeline IS 0"#,
|
|
|
|
// Fulltext indexing.
|
|
// A fulltext indexed value v is an integer rowid referencing fulltext_values.
|
|
|
|
// Optional settings:
|
|
// tokenize="porter"#,
|
|
// prefix='2,3'
|
|
// By default we use Unicode-aware tokenizing (particularly for case folding), but preserve
|
|
// diacritics.
|
|
r#"CREATE VIRTUAL TABLE fulltext_values
|
|
USING FTS4 (text NOT NULL, searchid INT, tokenize=unicode61 "remove_diacritics=0")"#,
|
|
|
|
// This combination of view and triggers allows you to transparently
|
|
// update-or-insert into FTS. Just INSERT INTO fulltext_values_view (text, searchid).
|
|
r#"CREATE VIEW fulltext_values_view AS SELECT * FROM fulltext_values"#,
|
|
r#"CREATE TRIGGER replace_fulltext_searchid
|
|
INSTEAD OF INSERT ON fulltext_values_view
|
|
WHEN EXISTS (SELECT 1 FROM fulltext_values WHERE text = new.text)
|
|
BEGIN
|
|
UPDATE fulltext_values SET searchid = new.searchid WHERE text = new.text;
|
|
END"#,
|
|
r#"CREATE TRIGGER insert_fulltext_searchid
|
|
INSTEAD OF INSERT ON fulltext_values_view
|
|
WHEN NOT EXISTS (SELECT 1 FROM fulltext_values WHERE text = new.text)
|
|
BEGIN
|
|
INSERT INTO fulltext_values (text, searchid) VALUES (new.text, new.searchid);
|
|
END"#,
|
|
|
|
// A view transparently interpolating fulltext indexed values into the datom structure.
|
|
r#"CREATE VIEW fulltext_datoms AS
|
|
SELECT e, a, fulltext_values.text AS v, tx, value_type_tag, index_avet, index_vaet, index_fulltext, unique_value
|
|
FROM datoms, fulltext_values
|
|
WHERE datoms.index_fulltext IS NOT 0 AND datoms.v = fulltext_values.rowid"#,
|
|
|
|
// A view transparently interpolating all entities (fulltext and non-fulltext) into the datom structure.
|
|
r#"CREATE VIEW all_datoms AS
|
|
SELECT e, a, v, tx, value_type_tag, index_avet, index_vaet, index_fulltext, unique_value
|
|
FROM datoms
|
|
WHERE index_fulltext IS 0
|
|
UNION ALL
|
|
SELECT e, a, v, tx, value_type_tag, index_avet, index_vaet, index_fulltext, unique_value
|
|
FROM fulltext_datoms"#,
|
|
|
|
// Materialized views of the metadata.
|
|
r#"CREATE TABLE idents (e INTEGER NOT NULL, a SMALLINT NOT NULL, v BLOB NOT NULL, value_type_tag SMALLINT NOT NULL)"#,
|
|
r#"CREATE INDEX idx_idents_unique ON idents (e, a, v, value_type_tag)"#,
|
|
r#"CREATE TABLE schema (e INTEGER NOT NULL, a SMALLINT NOT NULL, v BLOB NOT NULL, value_type_tag SMALLINT NOT NULL)"#,
|
|
r#"CREATE INDEX idx_schema_unique ON schema (e, a, v, value_type_tag)"#,
|
|
|
|
// TODO: store entid instead of ident for partition name.
|
|
r#"CREATE TABLE known_parts (part TEXT NOT NULL PRIMARY KEY, start INTEGER NOT NULL, end INTEGER NOT NULL, allow_excision SMALLINT NOT NULL)"#,
|
|
]
|
|
};
|
|
}
|
|
|
|
/// Set the SQLite user version.
|
|
///
|
|
/// Mentat manages its own SQL schema version using the user version. See the [SQLite
|
|
/// documentation](https://www.sqlite.org/pragma.html#pragma_user_version).
|
|
fn set_user_version(conn: &rusqlite::Connection, version: i32) -> Result<()> {
|
|
conn.execute(
|
|
&format!("PRAGMA user_version = {}", version),
|
|
rusqlite::params![],
|
|
)
|
|
.context(DbErrorKind::CouldNotSetVersionPragma)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the SQLite user version.
|
|
///
|
|
/// Mentat manages its own SQL schema version using the user version. See the [SQLite
|
|
/// documentation](https://www.sqlite.org/pragma.html#pragma_user_version).
|
|
fn get_user_version(conn: &rusqlite::Connection) -> Result<i32> {
|
|
let v = conn
|
|
.query_row("PRAGMA user_version", rusqlite::params![], |row| row.get(0))
|
|
.context(DbErrorKind::CouldNotGetVersionPragma)?;
|
|
Ok(v)
|
|
}
|
|
|
|
/// Do just enough work that either `create_current_version` or sync can populate the DB.
|
|
pub fn create_empty_current_version(
|
|
conn: &mut rusqlite::Connection,
|
|
) -> Result<(rusqlite::Transaction, DB)> {
|
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
|
|
|
for statement in (&V1_STATEMENTS).iter() {
|
|
tx.execute(statement, rusqlite::params![])?;
|
|
}
|
|
|
|
set_user_version(&tx, CURRENT_VERSION)?;
|
|
|
|
let bootstrap_schema = bootstrap::bootstrap_schema();
|
|
let bootstrap_partition_map = bootstrap::bootstrap_partition_map();
|
|
|
|
Ok((tx, DB::new(bootstrap_partition_map, bootstrap_schema)))
|
|
}
|
|
|
|
/// Creates a partition map view for the main timeline based on partitions
|
|
/// defined in 'known_parts'.
|
|
fn create_current_partition_view(conn: &rusqlite::Connection) -> Result<()> {
|
|
let mut stmt = conn.prepare("SELECT part, end FROM known_parts ORDER BY end ASC")?;
|
|
let known_parts: Result<Vec<(String, i64)>> = stmt
|
|
.query_and_then(rusqlite::params![], |row| Ok((row.get(0)?, row.get(1)?)))?
|
|
.collect();
|
|
|
|
let mut case = vec![];
|
|
for &(ref part, ref end) in known_parts?.iter() {
|
|
case.push(format!(r#"WHEN e <= {} THEN "{}""#, end, part));
|
|
}
|
|
|
|
let view_stmt = format!(
|
|
"CREATE VIEW parts AS
|
|
SELECT
|
|
CASE {} END AS part,
|
|
min(e) AS start,
|
|
max(e) + 1 AS idx
|
|
FROM timelined_transactions WHERE timeline = {} GROUP BY part",
|
|
case.join(" "),
|
|
::TIMELINE_MAIN
|
|
);
|
|
|
|
conn.execute(&view_stmt, rusqlite::params![])?;
|
|
Ok(())
|
|
}
|
|
|
|
// TODO: rename "SQL" functions to align with "datoms" functions.
|
|
pub fn create_current_version(conn: &mut rusqlite::Connection) -> Result<DB> {
|
|
let (tx, mut db) = create_empty_current_version(conn)?;
|
|
|
|
// TODO: think more carefully about allocating new parts and bit-masking part ranges.
|
|
// TODO: install these using bootstrap assertions. It's tricky because the part ranges are implicit.
|
|
// TODO: one insert, chunk into 999/3 sections, for safety.
|
|
// This is necessary: `transact` will only UPDATE parts, not INSERT them if they're missing.
|
|
for (part, partition) in db.partition_map.iter() {
|
|
// TODO: Convert "keyword" part to SQL using Value conversion.
|
|
tx.execute(
|
|
"INSERT INTO known_parts (part, start, end, allow_excision) VALUES (?, ?, ?, ?)",
|
|
&[
|
|
part,
|
|
&partition.start.to_string(),
|
|
&partition.end.to_string(),
|
|
&(partition.allow_excision as i8).to_string(),
|
|
],
|
|
)?;
|
|
}
|
|
|
|
create_current_partition_view(&tx)?;
|
|
|
|
// TODO: return to transact_internal to self-manage the encompassing SQLite transaction.
|
|
let bootstrap_schema_for_mutation = Schema::default(); // The bootstrap transaction will populate this schema.
|
|
|
|
let (_report, next_partition_map, next_schema, _watcher) = transact(
|
|
&tx,
|
|
db.partition_map,
|
|
&bootstrap_schema_for_mutation,
|
|
&db.schema,
|
|
NullWatcher(),
|
|
bootstrap::bootstrap_entities(),
|
|
)?;
|
|
|
|
// TODO: validate metadata mutations that aren't schema related, like additional partitions.
|
|
if let Some(next_schema) = next_schema {
|
|
if next_schema != db.schema {
|
|
bail!(DbErrorKind::NotYetImplemented(format!(
|
|
"Initial bootstrap transaction did not produce expected bootstrap schema"
|
|
)));
|
|
}
|
|
}
|
|
|
|
// TODO: use the drop semantics to do this automagically?
|
|
tx.commit()?;
|
|
|
|
db.partition_map = next_partition_map;
|
|
Ok(db)
|
|
}
|
|
|
|
pub fn ensure_current_version(conn: &mut rusqlite::Connection) -> Result<DB> {
|
|
if rusqlite::version_number() < MIN_SQLITE_VERSION {
|
|
panic!("Mentat requires at least sqlite {}", MIN_SQLITE_VERSION);
|
|
}
|
|
|
|
let user_version = get_user_version(&conn)?;
|
|
match user_version {
|
|
0 => create_current_version(conn),
|
|
CURRENT_VERSION => read_db(conn),
|
|
|
|
// TODO: support updating an existing store.
|
|
v => bail!(DbErrorKind::NotYetImplemented(format!(
|
|
"Opening databases with Mentat version: {}",
|
|
v
|
|
))),
|
|
}
|
|
}
|
|
|
|
pub trait TypedSQLValue {
|
|
fn from_sql_value_pair(
|
|
value: rusqlite::types::Value,
|
|
value_type_tag: i32,
|
|
) -> Result<TypedValue>;
|
|
fn to_sql_value_pair<'a>(&'a self) -> (ToSqlOutput<'a>, i32);
|
|
fn from_edn_value(value: &Value) -> Option<TypedValue>;
|
|
fn to_edn_value_pair(&self) -> (Value, ValueType);
|
|
}
|
|
|
|
impl TypedSQLValue for TypedValue {
|
|
/// Given a SQLite `value` and a `value_type_tag`, return the corresponding `TypedValue`.
|
|
fn from_sql_value_pair(
|
|
value: rusqlite::types::Value,
|
|
value_type_tag: i32,
|
|
) -> Result<TypedValue> {
|
|
match (value_type_tag, value) {
|
|
(0, rusqlite::types::Value::Integer(x)) => Ok(TypedValue::Ref(x)),
|
|
(1, rusqlite::types::Value::Integer(x)) => Ok(TypedValue::Boolean(0 != x)),
|
|
|
|
// Negative integers are simply times before 1970.
|
|
(4, rusqlite::types::Value::Integer(x)) => {
|
|
Ok(TypedValue::Instant(DateTime::<Utc>::from_micros(x)))
|
|
}
|
|
|
|
// SQLite distinguishes integral from decimal types, allowing long and double to
|
|
// share a tag.
|
|
(5, rusqlite::types::Value::Integer(x)) => Ok(TypedValue::Long(x)),
|
|
(5, rusqlite::types::Value::Real(x)) => Ok(TypedValue::Double(x.into())),
|
|
(10, rusqlite::types::Value::Text(x)) => Ok(x.into()),
|
|
(11, rusqlite::types::Value::Blob(x)) => {
|
|
let u = Uuid::from_bytes(x.as_slice().try_into().unwrap());
|
|
if u.is_nil() {
|
|
// Rather than exposing Uuid's ParseError…
|
|
bail!(DbErrorKind::BadSQLValuePair(
|
|
rusqlite::types::Value::Blob(x),
|
|
value_type_tag
|
|
));
|
|
}
|
|
Ok(TypedValue::Uuid(u))
|
|
}
|
|
(13, rusqlite::types::Value::Text(x)) => to_namespaced_keyword(&x).map(|k| k.into()),
|
|
(_, value) => bail!(DbErrorKind::BadSQLValuePair(value, value_type_tag)),
|
|
}
|
|
}
|
|
|
|
/// Given an EDN `value`, return a corresponding Mentat `TypedValue`.
|
|
///
|
|
/// An EDN `Value` does not encode a unique Mentat `ValueType`, so the composition
|
|
/// `from_edn_value(first(to_edn_value_pair(...)))` loses information. Additionally, there are
|
|
/// EDN values which are not Mentat typed values.
|
|
///
|
|
/// This function is deterministic.
|
|
fn from_edn_value(value: &Value) -> Option<TypedValue> {
|
|
match value {
|
|
&Value::Boolean(x) => Some(TypedValue::Boolean(x)),
|
|
&Value::Instant(x) => Some(TypedValue::Instant(x)),
|
|
&Value::Integer(x) => Some(TypedValue::Long(x)),
|
|
&Value::Uuid(x) => Some(TypedValue::Uuid(x)),
|
|
&Value::Float(ref x) => Some(TypedValue::Double(x.clone())),
|
|
&Value::Text(ref x) => Some(x.clone().into()),
|
|
&Value::Keyword(ref x) => Some(x.clone().into()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Return the corresponding SQLite `value` and `value_type_tag` pair.
|
|
fn to_sql_value_pair<'a>(&'a self) -> (ToSqlOutput<'a>, i32) {
|
|
match self {
|
|
&TypedValue::Ref(x) => (x.into(), 0),
|
|
&TypedValue::Boolean(x) => ((if x { 1 } else { 0 }).into(), 1),
|
|
&TypedValue::Instant(x) => (x.to_micros().into(), 4),
|
|
// SQLite distinguishes integral from decimal types, allowing long and double to share a tag.
|
|
&TypedValue::Long(x) => (x.into(), 5),
|
|
&TypedValue::Double(x) => (x.into_inner().into(), 5),
|
|
&TypedValue::String(ref x) => (x.as_str().into(), 10),
|
|
&TypedValue::Uuid(ref u) => (u.as_bytes().to_vec().into(), 11),
|
|
&TypedValue::Keyword(ref x) => (x.to_string().into(), 13),
|
|
}
|
|
}
|
|
|
|
/// Return the corresponding EDN `value` and `value_type` pair.
|
|
fn to_edn_value_pair(&self) -> (Value, ValueType) {
|
|
match self {
|
|
&TypedValue::Ref(x) => (Value::Integer(x), ValueType::Ref),
|
|
&TypedValue::Boolean(x) => (Value::Boolean(x), ValueType::Boolean),
|
|
&TypedValue::Instant(x) => (Value::Instant(x), ValueType::Instant),
|
|
&TypedValue::Long(x) => (Value::Integer(x), ValueType::Long),
|
|
&TypedValue::Double(x) => (Value::Float(x), ValueType::Double),
|
|
&TypedValue::String(ref x) => (Value::Text(x.as_ref().clone()), ValueType::String),
|
|
&TypedValue::Uuid(ref u) => (Value::Uuid(u.clone()), ValueType::Uuid),
|
|
&TypedValue::Keyword(ref x) => (Value::Keyword(x.as_ref().clone()), ValueType::Keyword),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read an arbitrary [e a v value_type_tag] materialized view from the given table in the SQL
|
|
/// store.
|
|
pub(crate) fn read_materialized_view(
|
|
conn: &rusqlite::Connection,
|
|
table: &str,
|
|
) -> Result<Vec<(Entid, Entid, TypedValue)>> {
|
|
let mut stmt: rusqlite::Statement =
|
|
conn.prepare(format!("SELECT e, a, v, value_type_tag FROM {}", table).as_str())?;
|
|
let m: Result<Vec<_>> = stmt
|
|
.query_and_then(rusqlite::params![], row_to_datom_assertion)?
|
|
.collect();
|
|
m
|
|
}
|
|
|
|
/// Read the partition map materialized view from the given SQL store.
|
|
pub fn read_partition_map(conn: &rusqlite::Connection) -> Result<PartitionMap> {
|
|
// An obviously expensive query, but we use it infrequently:
|
|
// - on first start,
|
|
// - while moving timelines,
|
|
// - during sync.
|
|
// First part of the union sprinkles 'allow_excision' into the 'parts' view.
|
|
// Second part of the union takes care of partitions which are known
|
|
// but don't have any transactions.
|
|
let mut stmt: rusqlite::Statement = conn.prepare(
|
|
"
|
|
SELECT
|
|
known_parts.part,
|
|
known_parts.start,
|
|
known_parts.end,
|
|
parts.idx,
|
|
known_parts.allow_excision
|
|
FROM
|
|
parts
|
|
INNER JOIN
|
|
known_parts
|
|
ON parts.part = known_parts.part
|
|
|
|
UNION
|
|
|
|
SELECT
|
|
part,
|
|
start,
|
|
end,
|
|
start,
|
|
allow_excision
|
|
FROM
|
|
known_parts
|
|
WHERE
|
|
part NOT IN (SELECT part FROM parts)",
|
|
)?;
|
|
let m = stmt
|
|
.query_and_then(rusqlite::params![], |row| -> Result<(String, Partition)> {
|
|
Ok((
|
|
row.get(0)?,
|
|
Partition::new(row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?),
|
|
))
|
|
})?
|
|
.collect();
|
|
m
|
|
}
|
|
|
|
/// Read the ident map materialized view from the given SQL store.
|
|
pub(crate) fn read_ident_map(conn: &rusqlite::Connection) -> Result<IdentMap> {
|
|
let v = read_materialized_view(conn, "idents")?;
|
|
v.into_iter().map(|(e, a, typed_value)| {
|
|
if a != entids::DB_IDENT {
|
|
bail!(DbErrorKind::NotYetImplemented(format!("bad idents materialized view: expected :db/ident but got {}", a)));
|
|
}
|
|
if let TypedValue::Keyword(keyword) = typed_value {
|
|
Ok((keyword.as_ref().clone(), e))
|
|
} else {
|
|
bail!(DbErrorKind::NotYetImplemented(format!("bad idents materialized view: expected [entid :db/ident keyword] but got [entid :db/ident {:?}]", typed_value)));
|
|
}
|
|
}).collect()
|
|
}
|
|
|
|
/// Read the schema materialized view from the given SQL store.
|
|
pub(crate) fn read_attribute_map(conn: &rusqlite::Connection) -> Result<AttributeMap> {
|
|
let entid_triples = read_materialized_view(conn, "schema")?;
|
|
let mut attribute_map = AttributeMap::default();
|
|
metadata::update_attribute_map_from_entid_triples(&mut attribute_map, entid_triples, vec![])?;
|
|
Ok(attribute_map)
|
|
}
|
|
|
|
/// Read the materialized views from the given SQL store and return a Mentat `DB` for querying and
|
|
/// applying transactions.
|
|
pub(crate) fn read_db(conn: &rusqlite::Connection) -> Result<DB> {
|
|
let partition_map = read_partition_map(conn)?;
|
|
let ident_map = read_ident_map(conn)?;
|
|
let attribute_map = read_attribute_map(conn)?;
|
|
let schema = Schema::from_ident_map_and_attribute_map(ident_map, attribute_map)?;
|
|
Ok(DB::new(partition_map, schema))
|
|
}
|
|
|
|
/// Internal representation of an [e a v added] datom, ready to be transacted against the store.
|
|
pub type ReducedEntity<'a> = (Entid, Entid, &'a Attribute, TypedValue, bool);
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)]
|
|
pub enum SearchType {
|
|
Exact,
|
|
Inexact,
|
|
}
|
|
|
|
/// `MentatStoring` will be the trait that encapsulates the storage layer. It is consumed by the
|
|
/// transaction processing layer.
|
|
///
|
|
/// Right now, the only implementation of `MentatStoring` is the SQLite-specific SQL schema. In the
|
|
/// future, we might consider other SQL engines (perhaps with different fulltext indexing), or
|
|
/// entirely different data stores, say ones shaped like key-value stores.
|
|
pub trait MentatStoring {
|
|
/// Given a slice of [a v] lookup-refs, look up the corresponding [e a v] triples.
|
|
///
|
|
/// It is assumed that the attribute `a` in each lookup-ref is `:db/unique`, so that at most one
|
|
/// matching [e a v] triple exists. (If this is not true, some matching entid `e` will be
|
|
/// chosen non-deterministically, if one exists.)
|
|
///
|
|
/// Returns a map &(a, v) -> e, to avoid cloning potentially large values. The keys of the map
|
|
/// are exactly those (a, v) pairs that have an assertion [e a v] in the store.
|
|
fn resolve_avs<'a>(&self, avs: &'a [&'a AVPair]) -> Result<AVMap<'a>>;
|
|
|
|
/// Begin (or prepare) the underlying storage layer for a new Mentat transaction.
|
|
///
|
|
/// Use this to create temporary tables, prepare indices, set pragmas, etc, before the initial
|
|
/// `insert_non_fts_searches` invocation.
|
|
fn begin_tx_application(&self) -> Result<()>;
|
|
|
|
// TODO: this is not a reasonable abstraction, but I don't want to really consider non-SQL storage just yet.
|
|
fn insert_non_fts_searches<'a>(
|
|
&self,
|
|
entities: &'a [ReducedEntity],
|
|
search_type: SearchType,
|
|
) -> Result<()>;
|
|
fn insert_fts_searches<'a>(
|
|
&self,
|
|
entities: &'a [ReducedEntity],
|
|
search_type: SearchType,
|
|
) -> Result<()>;
|
|
|
|
/// Prepare the underlying storage layer for finalization after a Mentat transaction.
|
|
///
|
|
/// Use this to finalize temporary tables, complete indices, revert pragmas, etc, after the
|
|
/// final `insert_non_fts_searches` invocation.
|
|
fn materialize_mentat_transaction(&self, tx_id: Entid) -> Result<()>;
|
|
|
|
/// Finalize the underlying storage layer after a Mentat transaction.
|
|
///
|
|
/// This is a final step in performing a transaction.
|
|
fn commit_mentat_transaction(&self, tx_id: Entid) -> Result<()>;
|
|
|
|
/// Extract metadata-related [e a typed_value added] datoms resolved in the last
|
|
/// materialized transaction.
|
|
fn resolved_metadata_assertions(&self) -> Result<Vec<(Entid, Entid, TypedValue, bool)>>;
|
|
}
|
|
|
|
/// Take search rows and complete `temp.search_results`.
|
|
///
|
|
/// See https://github.com/mozilla/mentat/wiki/Transacting:-entity-to-SQL-translation.
|
|
fn search(conn: &rusqlite::Connection) -> Result<()> {
|
|
// First is fast, only one table walk: lookup by exact eav.
|
|
// Second is slower, but still only one table walk: lookup old value by ea.
|
|
let s = r#"
|
|
INSERT INTO temp.search_results
|
|
SELECT t.e0, t.a0, t.v0, t.value_type_tag0, t.added0, t.flags0, ':db.cardinality/many', d.rowid, d.v
|
|
FROM temp.exact_searches AS t
|
|
LEFT JOIN datoms AS d
|
|
ON t.e0 = d.e AND
|
|
t.a0 = d.a AND
|
|
t.value_type_tag0 = d.value_type_tag AND
|
|
t.v0 = d.v
|
|
|
|
UNION ALL
|
|
|
|
SELECT t.e0, t.a0, t.v0, t.value_type_tag0, t.added0, t.flags0, ':db.cardinality/one', d.rowid, d.v
|
|
FROM temp.inexact_searches AS t
|
|
LEFT JOIN datoms AS d
|
|
ON t.e0 = d.e AND
|
|
t.a0 = d.a"#;
|
|
|
|
let mut stmt = conn.prepare_cached(s)?;
|
|
stmt.execute(rusqlite::params![])
|
|
.context(DbErrorKind::CouldNotSearch)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Insert the new transaction into the `transactions` table.
|
|
///
|
|
/// This turns the contents of `search_results` into a new transaction.
|
|
///
|
|
/// See https://github.com/mozilla/mentat/wiki/Transacting:-entity-to-SQL-translation.
|
|
fn insert_transaction(conn: &rusqlite::Connection, tx: Entid) -> Result<()> {
|
|
// Mentat follows Datomic and treats its input as a set. That means it is okay to transact the
|
|
// same [e a v] twice in one transaction. However, we don't want to represent the transacted
|
|
// datom twice. Therefore, the transactor unifies repeated datoms, and in addition we add
|
|
// indices to the search inputs and search results to ensure that we don't see repeated datoms
|
|
// at this point.
|
|
|
|
let s = r#"
|
|
INSERT INTO timelined_transactions (e, a, v, tx, added, value_type_tag)
|
|
SELECT e0, a0, v0, ?, 1, value_type_tag0
|
|
FROM temp.search_results
|
|
WHERE added0 IS 1 AND ((rid IS NULL) OR ((rid IS NOT NULL) AND (v0 IS NOT v)))"#;
|
|
|
|
let mut stmt = conn.prepare_cached(s)?;
|
|
stmt.execute(&[&tx])
|
|
.context(DbErrorKind::TxInsertFailedToAddMissingDatoms)?;
|
|
|
|
let s = r#"
|
|
INSERT INTO timelined_transactions (e, a, v, tx, added, value_type_tag)
|
|
SELECT DISTINCT e0, a0, v, ?, 0, value_type_tag0
|
|
FROM temp.search_results
|
|
WHERE rid IS NOT NULL AND
|
|
((added0 IS 0) OR
|
|
(added0 IS 1 AND search_type IS ':db.cardinality/one' AND v0 IS NOT v))"#;
|
|
|
|
let mut stmt = conn.prepare_cached(s)?;
|
|
stmt.execute(&[&tx])
|
|
.context(DbErrorKind::TxInsertFailedToRetractDatoms)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Update the contents of the `datoms` materialized view with the new transaction.
|
|
///
|
|
/// This applies the contents of `search_results` to the `datoms` table (in place).
|
|
///
|
|
/// See https://github.com/mozilla/mentat/wiki/Transacting:-entity-to-SQL-translation.
|
|
fn update_datoms(conn: &rusqlite::Connection, tx: Entid) -> Result<()> {
|
|
// Delete datoms that were retracted, or those that were :db.cardinality/one and will be
|
|
// replaced.
|
|
let s = r#"
|
|
WITH ids AS (SELECT rid
|
|
FROM temp.search_results
|
|
WHERE rid IS NOT NULL AND
|
|
((added0 IS 0) OR
|
|
(added0 IS 1 AND search_type IS ':db.cardinality/one' AND v0 IS NOT v)))
|
|
DELETE FROM datoms WHERE rowid IN ids"#;
|
|
|
|
let mut stmt = conn.prepare_cached(s)?;
|
|
stmt.execute(rusqlite::params![])
|
|
.context(DbErrorKind::DatomsUpdateFailedToRetract)?;
|
|
|
|
// Insert datoms that were added and not already present. We also must expand our bitfield into
|
|
// flags. Since Mentat follows Datomic and treats its input as a set, it is okay to transact
|
|
// the same [e a v] twice in one transaction, but we don't want to represent the transacted
|
|
// datom twice in datoms. The transactor unifies repeated datoms, and in addition we add
|
|
// indices to the search inputs and search results to ensure that we don't see repeated datoms
|
|
// at this point.
|
|
let s = format!(
|
|
r#"
|
|
INSERT INTO datoms (e, a, v, tx, value_type_tag, index_avet, index_vaet, index_fulltext, unique_value)
|
|
SELECT e0, a0, v0, ?, value_type_tag0,
|
|
flags0 & {} IS NOT 0,
|
|
flags0 & {} IS NOT 0,
|
|
flags0 & {} IS NOT 0,
|
|
flags0 & {} IS NOT 0
|
|
FROM temp.search_results
|
|
WHERE added0 IS 1 AND ((rid IS NULL) OR ((rid IS NOT NULL) AND (v0 IS NOT v)))"#,
|
|
AttributeBitFlags::IndexAVET as u8,
|
|
AttributeBitFlags::IndexVAET as u8,
|
|
AttributeBitFlags::IndexFulltext as u8,
|
|
AttributeBitFlags::UniqueValue as u8
|
|
);
|
|
|
|
let mut stmt = conn.prepare_cached(&s)?;
|
|
stmt.execute(&[&tx])
|
|
.context(DbErrorKind::DatomsUpdateFailedToAdd)?;
|
|
Ok(())
|
|
}
|
|
|
|
impl MentatStoring for rusqlite::Connection {
|
|
fn resolve_avs<'a>(&self, avs: &'a [&'a AVPair]) -> Result<AVMap<'a>> {
|
|
// Start search_id's at some identifiable number.
|
|
let initial_search_id = 2000;
|
|
let bindings_per_statement = 4;
|
|
|
|
// We map [a v] -> numeric search_id -> e, and then we use the search_id lookups to finally
|
|
// produce the map [a v] -> e.
|
|
//
|
|
// TODO: `collect` into a HashSet so that any (a, v) is resolved at most once.
|
|
let max_vars = self.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize;
|
|
let chunks: itertools::IntoChunks<_> = avs.into_iter().enumerate().chunks(max_vars / 4);
|
|
|
|
// We'd like to `flat_map` here, but it's not obvious how to `flat_map` across `Result`.
|
|
// Alternatively, this is a `fold`, and it might be wise to express it as such.
|
|
let results: Result<Vec<Vec<_>>> = chunks.into_iter().map(|chunk| -> Result<Vec<_>> {
|
|
let mut count = 0;
|
|
|
|
// We must keep these computed values somewhere to reference them later, so we can't
|
|
// combine this `map` and the subsequent `flat_map`.
|
|
let block: Vec<(i64, i64, ToSqlOutput<'a>, i32)> = chunk.map(|(index, &&(a, ref v))| {
|
|
count += 1;
|
|
let search_id: i64 = initial_search_id + index as i64;
|
|
let (value, value_type_tag) = v.to_sql_value_pair();
|
|
(search_id, a, value, value_type_tag)
|
|
}).collect();
|
|
|
|
// `params` reference computed values in `block`.
|
|
let params: Vec<&dyn ToSql> = block.iter().flat_map(|&(ref searchid, ref a, ref value, ref value_type_tag)| {
|
|
// Avoid inner heap allocation.
|
|
once(searchid as &dyn ToSql)
|
|
.chain(once(a as &dyn ToSql)
|
|
.chain(once(value as &dyn ToSql)
|
|
.chain(once(value_type_tag as &dyn ToSql))))
|
|
}).collect();
|
|
|
|
// TODO: cache these statements for selected values of `count`.
|
|
// TODO: query against `datoms` and UNION ALL with `fulltext_datoms` rather than
|
|
// querying against `all_datoms`. We know all the attributes, and in the common case,
|
|
// where most unique attributes will not be fulltext-indexed, we'll be querying just
|
|
// `datoms`, which will be much faster.ˇ
|
|
assert!(bindings_per_statement * count < max_vars, "Too many values: {} * {} >= {}", bindings_per_statement, count, max_vars);
|
|
|
|
let values: String = repeat_values(bindings_per_statement, count);
|
|
let s: String = format!("WITH t(search_id, a, v, value_type_tag) AS (VALUES {}) SELECT t.search_id, d.e \
|
|
FROM t, all_datoms AS d \
|
|
WHERE d.index_avet IS NOT 0 AND d.a = t.a AND d.value_type_tag = t.value_type_tag AND d.v = t.v",
|
|
values);
|
|
let mut stmt: rusqlite::Statement = self.prepare(s.as_str())?;
|
|
|
|
let m: Result<Vec<(i64, Entid)>> = stmt.query_and_then(¶ms, |row| -> Result<(i64, Entid)> {
|
|
Ok((row.get(0)?, row.get(1)?))
|
|
})?.collect();
|
|
m
|
|
}).collect::<Result<Vec<Vec<(i64, Entid)>>>>();
|
|
|
|
// Flatten.
|
|
let results: Vec<(i64, Entid)> = results?.as_slice().concat();
|
|
|
|
// Create map [a v] -> e.
|
|
let m: HashMap<&'a AVPair, Entid> = results
|
|
.into_iter()
|
|
.map(|(search_id, entid)| {
|
|
let index: usize = (search_id - initial_search_id) as usize;
|
|
(avs[index], entid)
|
|
})
|
|
.collect();
|
|
Ok(m)
|
|
}
|
|
|
|
/// Create empty temporary tables for search parameters and search results.
|
|
fn begin_tx_application(&self) -> Result<()> {
|
|
// We can't do this in one shot, since we can't prepare a batch statement.
|
|
let statements = [
|
|
r#"DROP TABLE IF EXISTS temp.exact_searches"#,
|
|
// Note that `flags0` is a bitfield of several flags compressed via
|
|
// `AttributeBitFlags.flags()` in the temporary search tables, later
|
|
// expanded in the `datoms` insertion.
|
|
r#"CREATE TABLE temp.exact_searches (
|
|
e0 INTEGER NOT NULL,
|
|
a0 SMALLINT NOT NULL,
|
|
v0 BLOB NOT NULL,
|
|
value_type_tag0 SMALLINT NOT NULL,
|
|
added0 TINYINT NOT NULL,
|
|
flags0 TINYINT NOT NULL)"#,
|
|
// There's no real need to split exact and inexact searches, so long as we keep things
|
|
// in the correct place and performant. Splitting has the advantage of being explicit
|
|
// and slightly easier to read, so we'll do that to start.
|
|
r#"DROP TABLE IF EXISTS temp.inexact_searches"#,
|
|
r#"CREATE TABLE temp.inexact_searches (
|
|
e0 INTEGER NOT NULL,
|
|
a0 SMALLINT NOT NULL,
|
|
v0 BLOB NOT NULL,
|
|
value_type_tag0 SMALLINT NOT NULL,
|
|
added0 TINYINT NOT NULL,
|
|
flags0 TINYINT NOT NULL)"#,
|
|
// It is fine to transact the same [e a v] twice in one transaction, but the transaction
|
|
// processor should unify such repeated datoms. This index will cause insertion to fail
|
|
// if the transaction processor incorrectly tries to assert the same (cardinality one)
|
|
// datom twice. (Sadly, the failure is opaque.)
|
|
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.
|
|
r#"CREATE TABLE temp.search_results (
|
|
e0 INTEGER NOT NULL,
|
|
a0 SMALLINT NOT NULL,
|
|
v0 BLOB NOT NULL,
|
|
value_type_tag0 SMALLINT NOT NULL,
|
|
added0 TINYINT NOT NULL,
|
|
flags0 TINYINT NOT NULL,
|
|
search_type STRING NOT NULL,
|
|
rid INTEGER,
|
|
v BLOB)"#,
|
|
// It is fine to transact the same [e a v] twice in one transaction, but the transaction
|
|
// processor should identify those datoms. This index will cause insertion to fail if
|
|
// the internals of the database searching code incorrectly find the same datom twice.
|
|
// (Sadly, the failure is opaque.)
|
|
//
|
|
// N.b.: temp goes on index name, not table name. See http://stackoverflow.com/a/22308016.
|
|
r#"CREATE UNIQUE INDEX IF NOT EXISTS temp.search_results_unique ON search_results (e0, a0, v0, value_type_tag0)"#,
|
|
];
|
|
|
|
for statement in &statements {
|
|
let mut stmt = self.prepare_cached(statement)?;
|
|
stmt.execute(rusqlite::params![])
|
|
.context(DbErrorKind::FailedToCreateTempTables)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Insert search rows into temporary search tables.
|
|
///
|
|
/// Eventually, the details of this approach will be captured in
|
|
/// https://github.com/mozilla/mentat/wiki/Transacting:-entity-to-SQL-translation.
|
|
fn insert_non_fts_searches<'a>(
|
|
&self,
|
|
entities: &'a [ReducedEntity<'a>],
|
|
search_type: SearchType,
|
|
) -> Result<()> {
|
|
let bindings_per_statement = 6;
|
|
|
|
let max_vars = self.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize;
|
|
let chunks: itertools::IntoChunks<_> = entities
|
|
.into_iter()
|
|
.chunks(max_vars / bindings_per_statement);
|
|
|
|
// We'd like to flat_map here, but it's not obvious how to flat_map across Result.
|
|
let results: Result<Vec<()>> = chunks.into_iter().map(|chunk| -> Result<()> {
|
|
let mut count = 0;
|
|
|
|
// We must keep these computed values somewhere to reference them later, so we can't
|
|
// combine this map and the subsequent flat_map.
|
|
// (e0, a0, v0, value_type_tag0, added0, flags0)
|
|
let block: Result<Vec<(i64 /* e */,
|
|
i64 /* a */,
|
|
ToSqlOutput<'a> /* value */,
|
|
i32 /* value_type_tag */,
|
|
bool, /* added0 */
|
|
u8 /* flags0 */)>> = chunk.map(|&(e, a, ref attribute, ref typed_value, added)| {
|
|
count += 1;
|
|
|
|
// Now we can represent the typed value as an SQL value.
|
|
let (value, value_type_tag): (ToSqlOutput, i32) = typed_value.to_sql_value_pair();
|
|
|
|
Ok((e, a, value, value_type_tag, added, attribute.flags()))
|
|
}).collect();
|
|
let block = block?;
|
|
|
|
// `params` reference computed values in `block`.
|
|
let params: Vec<&dyn ToSql> = block.iter().flat_map(|&(ref e, ref a, ref value, ref value_type_tag, added, ref flags)| {
|
|
// Avoid inner heap allocation.
|
|
// TODO: extract some finite length iterator to make this less indented!
|
|
once(e as &dyn ToSql)
|
|
.chain(once(a as &dyn ToSql)
|
|
.chain(once(value as &dyn ToSql)
|
|
.chain(once(value_type_tag as &dyn ToSql)
|
|
.chain(once(to_bool_ref(added) as &dyn ToSql)
|
|
.chain(once(flags as &dyn ToSql))))))
|
|
}).collect();
|
|
|
|
// TODO: cache this for selected values of count.
|
|
assert!(bindings_per_statement * count < max_vars, "Too many values: {} * {} >= {}", bindings_per_statement, count, max_vars);
|
|
let values: String = repeat_values(bindings_per_statement, count);
|
|
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)
|
|
};
|
|
|
|
// TODO: consider ensuring we inserted the expected number of rows.
|
|
let mut stmt = self.prepare_cached(s.as_str())?;
|
|
stmt.execute(¶ms)
|
|
.context(DbErrorKind::NonFtsInsertionIntoTempSearchTableFailed)
|
|
.map_err(|e| e.into())
|
|
.map(|_c| ())
|
|
}).collect::<Result<Vec<()>>>();
|
|
|
|
results.map(|_| ())
|
|
}
|
|
|
|
/// Insert search rows into temporary search tables.
|
|
///
|
|
/// Eventually, the details of this approach will be captured in
|
|
/// https://github.com/mozilla/mentat/wiki/Transacting:-entity-to-SQL-translation.
|
|
fn insert_fts_searches<'a>(
|
|
&self,
|
|
entities: &'a [ReducedEntity<'a>],
|
|
search_type: SearchType,
|
|
) -> Result<()> {
|
|
let max_vars = self.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize;
|
|
let bindings_per_statement = 6;
|
|
|
|
let mut outer_searchid = 2000;
|
|
|
|
let chunks: itertools::IntoChunks<_> = entities
|
|
.into_iter()
|
|
.chunks(max_vars / bindings_per_statement);
|
|
|
|
// From string to (searchid, value_type_tag).
|
|
let mut seen: HashMap<ValueRc<String>, (i64, i32)> = HashMap::with_capacity(entities.len());
|
|
|
|
// We'd like to flat_map here, but it's not obvious how to flat_map across Result.
|
|
let results: Result<Vec<()>> = chunks.into_iter().map(|chunk| -> Result<()> {
|
|
let mut datom_count = 0;
|
|
let mut string_count = 0;
|
|
|
|
// We must keep these computed values somewhere to reference them later, so we can't
|
|
// combine this map and the subsequent flat_map.
|
|
// (e0, a0, v0, value_type_tag0, added0, flags0)
|
|
let block: Result<Vec<(i64 /* e */,
|
|
i64 /* a */,
|
|
Option<ToSqlOutput<'a>> /* value */,
|
|
i32 /* value_type_tag */,
|
|
bool /* added0 */,
|
|
u8 /* flags0 */,
|
|
i64 /* searchid */)>> = chunk.map(|&(e, a, ref attribute, ref typed_value, added)| {
|
|
match typed_value {
|
|
&TypedValue::String(ref rc) => {
|
|
datom_count += 1;
|
|
let entry = seen.entry(rc.clone());
|
|
match entry {
|
|
Entry::Occupied(entry) => {
|
|
let &(searchid, value_type_tag) = entry.get();
|
|
Ok((e, a, None, value_type_tag, added, attribute.flags(), searchid))
|
|
},
|
|
Entry::Vacant(entry) => {
|
|
outer_searchid += 1;
|
|
string_count += 1;
|
|
|
|
// Now we can represent the typed value as an SQL value.
|
|
let (value, value_type_tag): (ToSqlOutput, i32) = typed_value.to_sql_value_pair();
|
|
entry.insert((outer_searchid, value_type_tag));
|
|
|
|
Ok((e, a, Some(value), value_type_tag, added, attribute.flags(), outer_searchid))
|
|
}
|
|
}
|
|
},
|
|
_ => {
|
|
bail!(DbErrorKind::WrongTypeValueForFtsAssertion);
|
|
},
|
|
}
|
|
|
|
|
|
}).collect();
|
|
let block = block?;
|
|
|
|
// First, insert all fulltext string values.
|
|
// `fts_params` reference computed values in `block`.
|
|
let fts_params: Vec<&dyn ToSql> =
|
|
block.iter()
|
|
.filter(|&&(ref _e, ref _a, ref value, ref _value_type_tag, _added, ref _flags, ref _searchid)| {
|
|
value.is_some()
|
|
})
|
|
.flat_map(|&(ref _e, ref _a, ref value, ref _value_type_tag, _added, ref _flags, ref searchid)| {
|
|
// Avoid inner heap allocation.
|
|
once(value as &dyn ToSql)
|
|
.chain(once(searchid as &dyn ToSql))
|
|
}).collect();
|
|
|
|
// TODO: make this maximally efficient. It's not terribly inefficient right now.
|
|
let fts_values: String = repeat_values(2, string_count);
|
|
let fts_s: String = format!("INSERT INTO fulltext_values_view (text, searchid) VALUES {}", fts_values);
|
|
|
|
// TODO: consider ensuring we inserted the expected number of rows.
|
|
let mut stmt = self.prepare_cached(fts_s.as_str())?;
|
|
stmt.execute(&fts_params).context(DbErrorKind::FtsInsertionFailed)?;
|
|
|
|
// Second, insert searches.
|
|
// `params` reference computed values in `block`.
|
|
let params: Vec<&dyn ToSql> = block.iter().flat_map(|&(ref e, ref a, ref _value, ref value_type_tag, added, ref flags, ref searchid)| {
|
|
// Avoid inner heap allocation.
|
|
// TODO: extract some finite length iterator to make this less indented!
|
|
once(e as &dyn ToSql)
|
|
.chain(once(a as &dyn ToSql)
|
|
.chain(once(searchid as &dyn ToSql)
|
|
.chain(once(value_type_tag as &dyn ToSql)
|
|
.chain(once(to_bool_ref(added) as &dyn ToSql)
|
|
.chain(once(flags as &dyn ToSql))))))
|
|
}).collect();
|
|
|
|
// TODO: cache this for selected values of count.
|
|
assert!(bindings_per_statement * datom_count < max_vars, "Too many values: {} * {} >= {}", bindings_per_statement, datom_count, max_vars);
|
|
let inner = "(?, ?, (SELECT rowid FROM fulltext_values WHERE searchid = ?), ?, ?, ?)".to_string();
|
|
// Like "(?, ?, (SELECT rowid FROM fulltext_values WHERE searchid = ?), ?, ?, ?), (?, ?, (SELECT rowid FROM fulltext_values WHERE searchid = ?), ?, ?, ?)".
|
|
let fts_values: String = repeat(inner).take(datom_count).join(", ");
|
|
let s: String = if search_type == SearchType::Exact {
|
|
format!("INSERT INTO temp.exact_searches (e0, a0, v0, value_type_tag0, added0, flags0) VALUES {}", fts_values)
|
|
} else {
|
|
format!("INSERT INTO temp.inexact_searches (e0, a0, v0, value_type_tag0, added0, flags0) VALUES {}", fts_values)
|
|
};
|
|
|
|
// TODO: consider ensuring we inserted the expected number of rows.
|
|
let mut stmt = self.prepare_cached(s.as_str())?;
|
|
stmt.execute(¶ms).context(DbErrorKind::FtsInsertionIntoTempSearchTableFailed)
|
|
.map_err(|e| e.into())
|
|
.map(|_c| ())
|
|
}).collect::<Result<Vec<()>>>();
|
|
|
|
// Finally, clean up temporary searchids.
|
|
let mut stmt = self.prepare_cached(
|
|
"UPDATE fulltext_values SET searchid = NULL WHERE searchid IS NOT NULL",
|
|
)?;
|
|
stmt.execute(rusqlite::params![])
|
|
.context(DbErrorKind::FtsFailedToDropSearchIds)?;
|
|
results.map(|_| ())
|
|
}
|
|
|
|
fn commit_mentat_transaction(&self, tx_id: Entid) -> Result<()> {
|
|
insert_transaction(&self, tx_id)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn materialize_mentat_transaction(&self, tx_id: Entid) -> Result<()> {
|
|
search(&self)?;
|
|
update_datoms(&self, tx_id)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn resolved_metadata_assertions(&self) -> Result<Vec<(Entid, Entid, TypedValue, bool)>> {
|
|
let sql_stmt = format!(
|
|
r#"
|
|
SELECT e, a, v, value_type_tag, added FROM
|
|
(
|
|
SELECT e0 as e, a0 as a, v0 as v, value_type_tag0 as value_type_tag, 1 as added
|
|
FROM temp.search_results
|
|
WHERE a0 IN {} AND added0 IS 1 AND ((rid IS NULL) OR
|
|
((rid IS NOT NULL) AND (v0 IS NOT v)))
|
|
|
|
UNION
|
|
|
|
SELECT e0 as e, a0 as a, v, value_type_tag0 as value_type_tag, 0 as added
|
|
FROM temp.search_results
|
|
WHERE a0 in {} AND rid IS NOT NULL AND
|
|
((added0 IS 0) OR
|
|
(added0 IS 1 AND search_type IS ':db.cardinality/one' AND v0 IS NOT v))
|
|
|
|
) ORDER BY e, a, v, value_type_tag, added"#,
|
|
entids::METADATA_SQL_LIST.as_str(),
|
|
entids::METADATA_SQL_LIST.as_str()
|
|
);
|
|
|
|
let mut stmt = self.prepare_cached(&sql_stmt)?;
|
|
let m: Result<Vec<_>> = stmt
|
|
.query_and_then(rusqlite::params![], row_to_transaction_assertion)?
|
|
.collect();
|
|
m
|
|
}
|
|
}
|
|
|
|
/// Extract metadata-related [e a typed_value added] datoms committed in the given transaction.
|
|
pub fn committed_metadata_assertions(
|
|
conn: &rusqlite::Connection,
|
|
tx_id: Entid,
|
|
) -> Result<Vec<(Entid, Entid, TypedValue, bool)>> {
|
|
let sql_stmt = format!(
|
|
r#"
|
|
SELECT e, a, v, value_type_tag, added
|
|
FROM transactions
|
|
WHERE tx = ? AND a IN {}
|
|
ORDER BY e, a, v, value_type_tag, added"#,
|
|
entids::METADATA_SQL_LIST.as_str()
|
|
);
|
|
|
|
let mut stmt = conn.prepare_cached(&sql_stmt)?;
|
|
let m: Result<Vec<_>> = stmt
|
|
.query_and_then(&[&tx_id as &dyn ToSql], row_to_transaction_assertion)?
|
|
.collect();
|
|
m
|
|
}
|
|
|
|
/// Takes a row, produces a transaction quadruple.
|
|
fn row_to_transaction_assertion(row: &rusqlite::Row) -> Result<(Entid, Entid, TypedValue, bool)> {
|
|
Ok((
|
|
row.get(0)?,
|
|
row.get(1)?,
|
|
TypedValue::from_sql_value_pair(row.get(2)?, row.get(3)?)?,
|
|
row.get(4)?,
|
|
))
|
|
}
|
|
|
|
/// Takes a row, produces a datom quadruple.
|
|
fn row_to_datom_assertion(row: &rusqlite::Row) -> Result<(Entid, Entid, TypedValue)> {
|
|
Ok((
|
|
row.get(0)?,
|
|
row.get(1)?,
|
|
TypedValue::from_sql_value_pair(row.get(2)?, row.get(3)?)?,
|
|
))
|
|
}
|
|
|
|
/// Update the metadata materialized views based on the given metadata report.
|
|
///
|
|
/// This updates the "entids", "idents", and "schema" materialized views, copying directly from the
|
|
/// "datoms" and "transactions" table as appropriate.
|
|
pub fn update_metadata(
|
|
conn: &rusqlite::Connection,
|
|
_old_schema: &Schema,
|
|
new_schema: &Schema,
|
|
metadata_report: &metadata::MetadataReport,
|
|
) -> Result<()> {
|
|
use metadata::AttributeAlteration::*;
|
|
|
|
// Populate the materialized view directly from datoms (and, potentially in the future,
|
|
// transactions). This might generalize nicely as we expand the set of materialized views.
|
|
// TODO: consider doing this in fewer SQLite execute() invocations.
|
|
// TODO: use concat! to avoid creating String instances.
|
|
if !metadata_report.idents_altered.is_empty() {
|
|
// Idents is the materialized view of the [entid :db/ident ident] slice of datoms.
|
|
conn.execute(format!("DELETE FROM idents").as_str(), rusqlite::params![])?;
|
|
conn.execute(
|
|
format!(
|
|
"INSERT INTO idents SELECT e, a, v, value_type_tag FROM datoms WHERE a IN {}",
|
|
entids::IDENTS_SQL_LIST.as_str()
|
|
)
|
|
.as_str(),
|
|
rusqlite::params![],
|
|
)?;
|
|
}
|
|
|
|
// Populate the materialized view directly from datoms.
|
|
// It's possible that an "ident" was removed, along with its attributes.
|
|
// That's not counted as an "alteration" of attributes, so we explicitly check
|
|
// for non-emptiness of 'idents_altered'.
|
|
|
|
// TODO expand metadata report to allow for better signaling for the above.
|
|
|
|
if !metadata_report.attributes_installed.is_empty()
|
|
|| !metadata_report.attributes_altered.is_empty()
|
|
|| !metadata_report.idents_altered.is_empty()
|
|
{
|
|
conn.execute(format!("DELETE FROM schema").as_str(), rusqlite::params![])?;
|
|
// NB: we're using :db/valueType as a placeholder for the entire schema-defining set.
|
|
let s = format!(
|
|
r#"
|
|
WITH s(e) AS (SELECT e FROM datoms WHERE a = {})
|
|
INSERT INTO schema
|
|
SELECT s.e, a, v, value_type_tag
|
|
FROM datoms, s
|
|
WHERE s.e = datoms.e AND a IN {}
|
|
"#,
|
|
entids::DB_VALUE_TYPE,
|
|
entids::SCHEMA_SQL_LIST.as_str()
|
|
);
|
|
conn.execute(&s, rusqlite::params![])?;
|
|
}
|
|
|
|
let mut index_stmt = conn.prepare("UPDATE datoms SET index_avet = ? WHERE a = ?")?;
|
|
let mut unique_value_stmt = conn.prepare("UPDATE datoms SET unique_value = ? WHERE a = ?")?;
|
|
let mut cardinality_stmt = conn.prepare(
|
|
r#"
|
|
SELECT EXISTS
|
|
(SELECT 1
|
|
FROM datoms AS left, datoms AS right
|
|
WHERE left.a = ? AND
|
|
left.a = right.a AND
|
|
left.e = right.e AND
|
|
left.v <> right.v)"#,
|
|
)?;
|
|
|
|
for (&entid, alterations) in &metadata_report.attributes_altered {
|
|
let attribute = new_schema.require_attribute_for_entid(entid)?;
|
|
|
|
for alteration in alterations {
|
|
match alteration {
|
|
&Index => {
|
|
// This should always succeed.
|
|
index_stmt.execute(&[&attribute.index, &entid as &dyn ToSql])?;
|
|
}
|
|
&Unique => {
|
|
// TODO: This can fail if there are conflicting values; give a more helpful
|
|
// error message in this case.
|
|
if unique_value_stmt
|
|
.execute(&[
|
|
to_bool_ref(attribute.unique.is_some()),
|
|
&entid as &dyn ToSql,
|
|
])
|
|
.is_err()
|
|
{
|
|
match attribute.unique {
|
|
Some(attribute::Unique::Value) => {
|
|
bail!(DbErrorKind::SchemaAlterationFailed(format!(
|
|
"Cannot alter schema attribute {} to be :db.unique/value",
|
|
entid
|
|
)))
|
|
}
|
|
Some(attribute::Unique::Identity) => {
|
|
bail!(DbErrorKind::SchemaAlterationFailed(format!(
|
|
"Cannot alter schema attribute {} to be :db.unique/identity",
|
|
entid
|
|
)))
|
|
}
|
|
None => unreachable!(), // This shouldn't happen, even after we support removing :db/unique.
|
|
}
|
|
}
|
|
}
|
|
&Cardinality => {
|
|
// We can always go from :db.cardinality/one to :db.cardinality many. It's
|
|
// :db.cardinality/many to :db.cardinality/one that can fail.
|
|
//
|
|
// TODO: improve the failure message. Perhaps try to mimic what Datomic says in
|
|
// this case?
|
|
if !attribute.multival {
|
|
let mut rows = cardinality_stmt.query(&[&entid as &dyn ToSql])?;
|
|
if rows.next()?.is_some() {
|
|
bail!(DbErrorKind::SchemaAlterationFailed(format!(
|
|
"Cannot alter schema attribute {} to be :db.cardinality/one",
|
|
entid
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
&NoHistory | &IsComponent => {
|
|
// There's no on disk change required for either of these.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
impl PartitionMap {
|
|
/// Allocate a single fresh entid in the given `partition`.
|
|
pub(crate) fn allocate_entid(&mut self, partition: &str) -> i64 {
|
|
self.allocate_entids(partition, 1).start
|
|
}
|
|
|
|
/// Allocate `n` fresh entids in the given `partition`.
|
|
pub(crate) fn allocate_entids(&mut self, partition: &str, n: usize) -> Range<i64> {
|
|
match self.get_mut(partition) {
|
|
Some(partition) => partition.allocate_entids(n),
|
|
None => panic!(
|
|
"Cannot allocate entid from unknown partition: {}",
|
|
partition
|
|
),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn contains_entid(&self, entid: Entid) -> bool {
|
|
self.values()
|
|
.any(|partition| partition.contains_entid(entid))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
extern crate env_logger;
|
|
|
|
use std::borrow::Borrow;
|
|
|
|
use super::*;
|
|
use core_traits::{attribute, KnownEntid};
|
|
use db_traits::errors;
|
|
use debug::{tempids, TestConn};
|
|
use edn::entities::OpType;
|
|
use edn::{self, InternSet};
|
|
use internal_types::Term;
|
|
use mentat_core::util::Either::*;
|
|
use mentat_core::{HasSchema, Keyword};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn run_test_add(mut conn: TestConn) {
|
|
// Test inserting :db.cardinality/one elements.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db.schema/version 1]
|
|
[:db/add 101 :db.schema/version 2]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db.schema/version 1 ?tx true]
|
|
[101 :db.schema/version 2 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db.schema/version 1]
|
|
[101 :db.schema/version 2]]"
|
|
);
|
|
|
|
// Test inserting :db.cardinality/many elements.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 200 :db.schema/attribute 100]
|
|
[:db/add 200 :db.schema/attribute 101]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[200 :db.schema/attribute 100 ?tx true]
|
|
[200 :db.schema/attribute 101 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db.schema/version 1]
|
|
[101 :db.schema/version 2]
|
|
[200 :db.schema/attribute 100]
|
|
[200 :db.schema/attribute 101]]"
|
|
);
|
|
|
|
// Test replacing existing :db.cardinality/one elements.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db.schema/version 11]
|
|
[:db/add 101 :db.schema/version 22]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db.schema/version 1 ?tx false]
|
|
[100 :db.schema/version 11 ?tx true]
|
|
[101 :db.schema/version 2 ?tx false]
|
|
[101 :db.schema/version 22 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db.schema/version 11]
|
|
[101 :db.schema/version 22]
|
|
[200 :db.schema/attribute 100]
|
|
[200 :db.schema/attribute 101]]"
|
|
);
|
|
|
|
// Test that asserting existing :db.cardinality/one elements doesn't change the store.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db.schema/version 11]
|
|
[:db/add 101 :db.schema/version 22]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db.schema/version 11]
|
|
[101 :db.schema/version 22]
|
|
[200 :db.schema/attribute 100]
|
|
[200 :db.schema/attribute 101]]"
|
|
);
|
|
|
|
// Test that asserting existing :db.cardinality/many elements doesn't change the store.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 200 :db.schema/attribute 100]
|
|
[:db/add 200 :db.schema/attribute 101]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db.schema/version 11]
|
|
[101 :db.schema/version 22]
|
|
[200 :db.schema/attribute 100]
|
|
[200 :db.schema/attribute 101]]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_add() {
|
|
run_test_add(TestConn::default());
|
|
}
|
|
|
|
#[test]
|
|
fn test_tx_assertions() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Test that txInstant can be asserted.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add (transaction-tx) :db/txInstant #inst \"2017-06-16T00:56:41.257Z\"]
|
|
[:db/add 100 :db/ident :name/Ivan]
|
|
[:db/add 101 :db/ident :name/Petr]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db/ident :name/Ivan ?tx true]
|
|
[101 :db/ident :name/Petr ?tx true]
|
|
[?tx :db/txInstant #inst \"2017-06-16T00:56:41.257Z\" ?tx true]]"
|
|
);
|
|
|
|
// Test multiple txInstant with different values should fail.
|
|
assert_transact!(conn, "[[:db/add (transaction-tx) :db/txInstant #inst \"2017-06-16T00:59:11.257Z\"]
|
|
[:db/add (transaction-tx) :db/txInstant #inst \"2017-06-16T00:59:11.752Z\"]
|
|
[:db/add 102 :db/ident :name/Vlad]]",
|
|
Err("schema constraint violation: cardinality conflicts:\n CardinalityOneAddConflict { e: 268435458, a: 3, vs: {Instant(2017-06-16T00:59:11.257Z), Instant(2017-06-16T00:59:11.752Z)} }\n"));
|
|
|
|
// Test multiple txInstants with the same value.
|
|
assert_transact!(conn, "[[:db/add (transaction-tx) :db/txInstant #inst \"2017-06-16T00:59:11.257Z\"]
|
|
[:db/add (transaction-tx) :db/txInstant #inst \"2017-06-16T00:59:11.257Z\"]
|
|
[:db/add 103 :db/ident :name/Dimitri]
|
|
[:db/add 104 :db/ident :name/Anton]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[103 :db/ident :name/Dimitri ?tx true]
|
|
[104 :db/ident :name/Anton ?tx true]
|
|
[?tx :db/txInstant #inst \"2017-06-16T00:59:11.257Z\" ?tx true]]"
|
|
);
|
|
|
|
// We need a few attributes to work with.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 111 :db/ident :test/str]
|
|
[:db/add 111 :db/valueType :db.type/string]
|
|
[:db/add 222 :db/ident :test/ref]
|
|
[:db/add 222 :db/valueType :db.type/ref]]"
|
|
);
|
|
|
|
// Test that we can assert metadata about the current transaction.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add (transaction-tx) :test/str \"We want metadata!\"]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?tx :db/txInstant ?ms ?tx true]
|
|
[?tx :test/str \"We want metadata!\" ?tx true]]"
|
|
);
|
|
|
|
// Test that we can use (transaction-tx) as a value.
|
|
assert_transact!(conn, "[[:db/add 333 :test/ref (transaction-tx)]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[333 :test/ref ?tx ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
// Test that we type-check properly. In the value position, (transaction-tx) yields a ref;
|
|
// :db/ident expects a keyword.
|
|
assert_transact!(conn, "[[:db/add 444 :db/ident (transaction-tx)]]",
|
|
Err("not yet implemented: Transaction function transaction-tx produced value of type :db.type/ref but expected type :db.type/keyword"));
|
|
|
|
// Test that we can assert metadata about the current transaction.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add (transaction-tx) :test/ref (transaction-tx)]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?tx :db/txInstant ?ms ?tx true]
|
|
[?tx :test/ref ?tx ?tx true]]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_retract() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Insert a few :db.cardinality/one elements.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db.schema/version 1]
|
|
[:db/add 101 :db.schema/version 2]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db.schema/version 1 ?tx true]
|
|
[101 :db.schema/version 2 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db.schema/version 1]
|
|
[101 :db.schema/version 2]]"
|
|
);
|
|
|
|
// And a few :db.cardinality/many elements.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 200 :db.schema/attribute 100]
|
|
[:db/add 200 :db.schema/attribute 101]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[200 :db.schema/attribute 100 ?tx true]
|
|
[200 :db.schema/attribute 101 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db.schema/version 1]
|
|
[101 :db.schema/version 2]
|
|
[200 :db.schema/attribute 100]
|
|
[200 :db.schema/attribute 101]]"
|
|
);
|
|
|
|
// Test that we can retract :db.cardinality/one elements.
|
|
assert_transact!(conn, "[[:db/retract 100 :db.schema/version 1]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db.schema/version 1 ?tx false]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[101 :db.schema/version 2]
|
|
[200 :db.schema/attribute 100]
|
|
[200 :db.schema/attribute 101]]"
|
|
);
|
|
|
|
// Test that we can retract :db.cardinality/many elements.
|
|
assert_transact!(conn, "[[:db/retract 200 :db.schema/attribute 100]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[200 :db.schema/attribute 100 ?tx false]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[101 :db.schema/version 2]
|
|
[200 :db.schema/attribute 101]]"
|
|
);
|
|
|
|
// Verify that retracting :db.cardinality/{one,many} elements that are not present doesn't
|
|
// change the store.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/retract 100 :db.schema/version 1]
|
|
[:db/retract 200 :db.schema/attribute 100]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[101 :db.schema/version 2]
|
|
[200 :db.schema/attribute 101]]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_db_doc_is_not_schema() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Neither transaction below is defining a new attribute. That is, it's fine to use :db/doc
|
|
// to describe any entity in the system, not just attributes. And in particular, including
|
|
// :db/doc shouldn't make the transactor consider the entity a schema attribute.
|
|
assert_transact!(
|
|
conn,
|
|
r#"
|
|
[{:db/doc "test"}]
|
|
"#
|
|
);
|
|
|
|
assert_transact!(
|
|
conn,
|
|
r#"
|
|
[{:db/ident :test/id :db/doc "test"}]
|
|
"#
|
|
);
|
|
}
|
|
|
|
// 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!
|
|
#[test]
|
|
fn test_upsert_vector() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Insert some :db.unique/identity elements.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db/ident :name/Ivan]
|
|
[:db/add 101 :db/ident :name/Petr]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db/ident :name/Ivan ?tx true]
|
|
[101 :db/ident :name/Petr ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db/ident :name/Ivan]
|
|
[101 :db/ident :name/Petr]]"
|
|
);
|
|
|
|
// Upserting two tempids to the same entid works.
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[[:db/add \"t1\" :db/ident :name/Ivan]
|
|
[:db/add \"t1\" :db.schema/attribute 100]
|
|
[:db/add \"t2\" :db/ident :name/Petr]
|
|
[:db/add \"t2\" :db.schema/attribute 101]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db.schema/attribute :name/Ivan ?tx true]
|
|
[101 :db.schema/attribute :name/Petr ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db/ident :name/Ivan]
|
|
[100 :db.schema/attribute :name/Ivan]
|
|
[101 :db/ident :name/Petr]
|
|
[101 :db.schema/attribute :name/Petr]]"
|
|
);
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"t1\" 100
|
|
\"t2\" 101}"
|
|
);
|
|
|
|
// Upserting a tempid works. The ref doesn't have to exist (at this time), but we can't
|
|
// reuse an existing ref due to :db/unique :db.unique/value.
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[[:db/add \"t1\" :db/ident :name/Ivan]
|
|
[:db/add \"t1\" :db.schema/attribute 102]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db.schema/attribute 102 ?tx true]
|
|
[?true :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db/ident :name/Ivan]
|
|
[100 :db.schema/attribute :name/Ivan]
|
|
[100 :db.schema/attribute 102]
|
|
[101 :db/ident :name/Petr]
|
|
[101 :db.schema/attribute :name/Petr]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{\"t1\" 100}");
|
|
|
|
// A single complex upsert allocates a new entid.
|
|
let report = assert_transact!(conn, "[[:db/add \"t1\" :db.schema/attribute \"t2\"]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[65536 :db.schema/attribute 65537 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"t1\" 65536
|
|
\"t2\" 65537}"
|
|
);
|
|
|
|
// Conflicting upserts fail.
|
|
assert_transact!(conn, "[[:db/add \"t1\" :db/ident :name/Ivan]
|
|
[:db/add \"t1\" :db/ident :name/Petr]]",
|
|
Err("schema constraint violation: conflicting upserts:\n tempid External(\"t1\") upserts to {KnownEntid(100), KnownEntid(101)}\n"));
|
|
|
|
// The error messages of conflicting upserts gives information about all failing upserts (in a particular generation).
|
|
assert_transact!(conn, "[[:db/add \"t2\" :db/ident :name/Grigory]
|
|
[:db/add \"t2\" :db/ident :name/Petr]
|
|
[:db/add \"t2\" :db/ident :name/Ivan]
|
|
[:db/add \"t1\" :db/ident :name/Ivan]
|
|
[:db/add \"t1\" :db/ident :name/Petr]]",
|
|
Err("schema constraint violation: conflicting upserts:\n tempid External(\"t1\") upserts to {KnownEntid(100), KnownEntid(101)}\n tempid External(\"t2\") upserts to {KnownEntid(100), KnownEntid(101)}\n"));
|
|
|
|
// tempids in :db/retract that don't upsert fail.
|
|
assert_transact!(conn, "[[:db/retract \"t1\" :db/ident :name/Anonymous]]",
|
|
Err("not yet implemented: [:db/retract ...] entity referenced tempid that did not upsert: t1"));
|
|
|
|
// tempids in :db/retract that do upsert are retracted. The ref given doesn't exist, so the
|
|
// assertion will be ignored.
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[[:db/add \"t1\" :db/ident :name/Ivan]
|
|
[:db/retract \"t1\" :db.schema/attribute 103]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{\"t1\" 100}");
|
|
|
|
// A multistep upsert. The upsert algorithm will first try to resolve "t1", fail, and then
|
|
// allocate both "t1" and "t2".
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[[:db/add \"t1\" :db/ident :name/Josef]
|
|
[:db/add \"t2\" :db.schema/attribute \"t1\"]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[65538 :db/ident :name/Josef ?tx true]
|
|
[65539 :db.schema/attribute :name/Josef ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"t1\" 65538
|
|
\"t2\" 65539}"
|
|
);
|
|
|
|
// A multistep insert. This time, we can resolve both, but we have to try "t1", succeed,
|
|
// and then resolve "t2".
|
|
// TODO: We can't quite test this without more schema elements.
|
|
// conn.transact("[[:db/add \"t1\" :db/ident :name/Josef]
|
|
// [:db/add \"t2\" :db/ident \"t1\"]]");
|
|
// assert_matches!(conn.last_transaction(),
|
|
// "[[65538 :db/ident :name/Josef]
|
|
// [65538 :db/ident :name/Karl]
|
|
// [?tx :db/txInstant ?ms ?tx true]]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolved_upserts() {
|
|
let mut conn = TestConn::default();
|
|
assert_transact!(
|
|
conn,
|
|
"[
|
|
{:db/ident :test/id
|
|
:db/valueType :db.type/string
|
|
:db/unique :db.unique/identity
|
|
:db/index true
|
|
:db/cardinality :db.cardinality/one}
|
|
{:db/ident :test/ref
|
|
:db/valueType :db.type/ref
|
|
:db/unique :db.unique/identity
|
|
:db/index true
|
|
:db/cardinality :db.cardinality/one}
|
|
]"
|
|
);
|
|
|
|
// Partial data for :test/id, links via :test/ref.
|
|
assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/add 100 :test/id "0"]
|
|
[:db/add 101 :test/ref 100]
|
|
[:db/add 102 :test/ref 101]
|
|
[:db/add 103 :test/ref 102]
|
|
]"#
|
|
);
|
|
|
|
// Fill in the rest of the data for :test/id, using the links of :test/ref.
|
|
let report = assert_transact!(
|
|
conn,
|
|
r#"[
|
|
{:db/id "a" :test/id "0"}
|
|
{:db/id "b" :test/id "1" :test/ref "a"}
|
|
{:db/id "c" :test/id "2" :test/ref "b"}
|
|
{:db/id "d" :test/id "3" :test/ref "c"}
|
|
]"#
|
|
);
|
|
|
|
assert_matches!(
|
|
tempids(&report),
|
|
r#"{
|
|
"a" 100
|
|
"b" 101
|
|
"c" 102
|
|
"d" 103
|
|
}"#
|
|
);
|
|
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
r#"[
|
|
[101 :test/id "1" ?tx true]
|
|
[102 :test/id "2" ?tx true]
|
|
[103 :test/id "3" ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]
|
|
]"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sqlite_limit() {
|
|
let conn = new_connection("").expect("Couldn't open in-memory db");
|
|
let initial = conn.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER);
|
|
// Sanity check.
|
|
assert!(initial > 500);
|
|
|
|
// Make sure setting works.
|
|
conn.set_limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER, 222);
|
|
assert_eq!(222, conn.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER));
|
|
}
|
|
|
|
#[test]
|
|
fn test_db_install() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// We're missing some tests here, since our implementation is incomplete.
|
|
// See https://github.com/mozilla/mentat/issues/797
|
|
|
|
// We can assert a new schema attribute.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db/ident :test/ident]
|
|
[:db/add 100 :db/valueType :db.type/long]
|
|
[:db/add 100 :db/cardinality :db.cardinality/many]]"
|
|
);
|
|
|
|
assert_eq!(
|
|
conn.schema.entid_map.get(&100).cloned().unwrap(),
|
|
to_namespaced_keyword(":test/ident").unwrap()
|
|
);
|
|
assert_eq!(
|
|
conn.schema
|
|
.ident_map
|
|
.get(&to_namespaced_keyword(":test/ident").unwrap())
|
|
.cloned()
|
|
.unwrap(),
|
|
100
|
|
);
|
|
let attribute = conn.schema.attribute_for_entid(100).unwrap().clone();
|
|
assert_eq!(attribute.value_type, ValueType::Long);
|
|
assert_eq!(attribute.multival, true);
|
|
assert_eq!(attribute.fulltext, false);
|
|
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db/ident :test/ident ?tx true]
|
|
[100 :db/valueType :db.type/long ?tx true]
|
|
[100 :db/cardinality :db.cardinality/many ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db/ident :test/ident]
|
|
[100 :db/valueType :db.type/long]
|
|
[100 :db/cardinality :db.cardinality/many]]"
|
|
);
|
|
|
|
// Let's check we actually have the schema characteristics we expect.
|
|
let attribute = conn.schema.attribute_for_entid(100).unwrap().clone();
|
|
assert_eq!(attribute.value_type, ValueType::Long);
|
|
assert_eq!(attribute.multival, true);
|
|
assert_eq!(attribute.fulltext, false);
|
|
|
|
// Let's check that we can use the freshly installed attribute.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 101 100 -10]
|
|
[:db/add 101 :test/ident -9]]"
|
|
);
|
|
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[101 :test/ident -10 ?tx true]
|
|
[101 :test/ident -9 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
// Cannot retract a single characteristic of an installed attribute.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/retract 100 :db/cardinality :db.cardinality/many]]",
|
|
Err("bad schema assertion: Retracting attribute 8 for entity 100 not permitted.")
|
|
);
|
|
|
|
// Cannot retract a single characteristic of an installed attribute.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/retract 100 :db/valueType :db.type/long]]",
|
|
Err("bad schema assertion: Retracting attribute 7 for entity 100 not permitted.")
|
|
);
|
|
|
|
// Cannot retract a non-defining set of characteristics of an installed attribute.
|
|
assert_transact!(conn,
|
|
"[[:db/retract 100 :db/valueType :db.type/long]
|
|
[:db/retract 100 :db/cardinality :db.cardinality/many]]",
|
|
Err("bad schema assertion: Retracting defining attributes of a schema without retracting its :db/ident is not permitted."));
|
|
|
|
// See https://github.com/mozilla/mentat/issues/796.
|
|
// assert_transact!(conn,
|
|
// "[[:db/retract 100 :db/ident :test/ident]]",
|
|
// Err("bad schema assertion: Retracting :db/ident of a schema without retracting its defining attributes is not permitted."));
|
|
|
|
// Can retract all of characterists of an installed attribute in one go.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/retract 100 :db/cardinality :db.cardinality/many]
|
|
[:db/retract 100 :db/valueType :db.type/long]
|
|
[:db/retract 100 :db/ident :test/ident]]"
|
|
);
|
|
|
|
// Trying to install an attribute without a :db/ident is allowed.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 101 :db/valueType :db.type/long]
|
|
[:db/add 101 :db/cardinality :db.cardinality/many]]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_db_alter() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a :db.cardinality/one attribute.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db/ident :test/ident]
|
|
[:db/add 100 :db/valueType :db.type/keyword]
|
|
[:db/add 100 :db/cardinality :db.cardinality/one]]"
|
|
);
|
|
|
|
// Trying to alter the :db/valueType will fail.
|
|
assert_transact!(conn, "[[:db/add 100 :db/valueType :db.type/long]]",
|
|
Err("bad schema assertion: Schema alteration for existing attribute with entid 100 is not valid"));
|
|
|
|
// But we can alter the cardinality.
|
|
assert_transact!(conn, "[[:db/add 100 :db/cardinality :db.cardinality/many]]");
|
|
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db/cardinality :db.cardinality/one ?tx false]
|
|
[100 :db/cardinality :db.cardinality/many ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db/ident :test/ident]
|
|
[100 :db/valueType :db.type/keyword]
|
|
[100 :db/cardinality :db.cardinality/many]]"
|
|
);
|
|
|
|
// Let's check we actually have the schema characteristics we expect.
|
|
let attribute = conn.schema.attribute_for_entid(100).unwrap().clone();
|
|
assert_eq!(attribute.value_type, ValueType::Keyword);
|
|
assert_eq!(attribute.multival, true);
|
|
assert_eq!(attribute.fulltext, false);
|
|
|
|
// Let's check that we can use the freshly altered attribute's new characteristic.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 101 100 :test/value1]
|
|
[:db/add 101 :test/ident :test/value2]]"
|
|
);
|
|
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[101 :test/ident :test/value1 ?tx true]
|
|
[101 :test/ident :test/value2 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_db_ident() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// We can assert a new :db/ident.
|
|
assert_transact!(conn, "[[:db/add 100 :db/ident :name/Ivan]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db/ident :name/Ivan ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(conn.datoms(), "[[100 :db/ident :name/Ivan]]");
|
|
assert_eq!(
|
|
conn.schema.entid_map.get(&100).cloned().unwrap(),
|
|
to_namespaced_keyword(":name/Ivan").unwrap()
|
|
);
|
|
assert_eq!(
|
|
conn.schema
|
|
.ident_map
|
|
.get(&to_namespaced_keyword(":name/Ivan").unwrap())
|
|
.cloned()
|
|
.unwrap(),
|
|
100
|
|
);
|
|
|
|
// We can re-assert an existing :db/ident.
|
|
assert_transact!(conn, "[[:db/add 100 :db/ident :name/Ivan]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(conn.datoms(), "[[100 :db/ident :name/Ivan]]");
|
|
assert_eq!(
|
|
conn.schema.entid_map.get(&100).cloned().unwrap(),
|
|
to_namespaced_keyword(":name/Ivan").unwrap()
|
|
);
|
|
assert_eq!(
|
|
conn.schema
|
|
.ident_map
|
|
.get(&to_namespaced_keyword(":name/Ivan").unwrap())
|
|
.cloned()
|
|
.unwrap(),
|
|
100
|
|
);
|
|
|
|
// We can alter an existing :db/ident to have a new keyword.
|
|
assert_transact!(conn, "[[:db/add :name/Ivan :db/ident :name/Petr]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[100 :db/ident :name/Ivan ?tx false]
|
|
[100 :db/ident :name/Petr ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(conn.datoms(), "[[100 :db/ident :name/Petr]]");
|
|
// Entid map is updated.
|
|
assert_eq!(
|
|
conn.schema.entid_map.get(&100).cloned().unwrap(),
|
|
to_namespaced_keyword(":name/Petr").unwrap()
|
|
);
|
|
// Ident map contains the new ident.
|
|
assert_eq!(
|
|
conn.schema
|
|
.ident_map
|
|
.get(&to_namespaced_keyword(":name/Petr").unwrap())
|
|
.cloned()
|
|
.unwrap(),
|
|
100
|
|
);
|
|
// Ident map no longer contains the old ident.
|
|
assert!(conn
|
|
.schema
|
|
.ident_map
|
|
.get(&to_namespaced_keyword(":name/Ivan").unwrap())
|
|
.is_none());
|
|
|
|
// We can re-purpose an old ident.
|
|
assert_transact!(conn, "[[:db/add 101 :db/ident :name/Ivan]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[101 :db/ident :name/Ivan ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db/ident :name/Petr]
|
|
[101 :db/ident :name/Ivan]]"
|
|
);
|
|
// Entid map contains both entids.
|
|
assert_eq!(
|
|
conn.schema.entid_map.get(&100).cloned().unwrap(),
|
|
to_namespaced_keyword(":name/Petr").unwrap()
|
|
);
|
|
assert_eq!(
|
|
conn.schema.entid_map.get(&101).cloned().unwrap(),
|
|
to_namespaced_keyword(":name/Ivan").unwrap()
|
|
);
|
|
// Ident map contains the new ident.
|
|
assert_eq!(
|
|
conn.schema
|
|
.ident_map
|
|
.get(&to_namespaced_keyword(":name/Petr").unwrap())
|
|
.cloned()
|
|
.unwrap(),
|
|
100
|
|
);
|
|
// Ident map contains the old ident, but re-purposed to the new entid.
|
|
assert_eq!(
|
|
conn.schema
|
|
.ident_map
|
|
.get(&to_namespaced_keyword(":name/Ivan").unwrap())
|
|
.cloned()
|
|
.unwrap(),
|
|
101
|
|
);
|
|
|
|
// We can retract an existing :db/ident.
|
|
assert_transact!(conn, "[[:db/retract :name/Petr :db/ident :name/Petr]]");
|
|
// It's really gone.
|
|
assert!(conn.schema.entid_map.get(&100).is_none());
|
|
assert!(conn
|
|
.schema
|
|
.ident_map
|
|
.get(&to_namespaced_keyword(":name/Petr").unwrap())
|
|
.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_db_alter_cardinality() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a :db.cardinality/one attribute.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db/ident :test/ident]
|
|
[:db/add 100 :db/valueType :db.type/long]
|
|
[:db/add 100 :db/cardinality :db.cardinality/one]]"
|
|
);
|
|
|
|
assert_transact!(conn, "[[:db/add 200 :test/ident 1]]");
|
|
|
|
// We can always go from :db.cardinality/one to :db.cardinality/many.
|
|
assert_transact!(conn, "[[:db/add 100 :db/cardinality :db.cardinality/many]]");
|
|
|
|
assert_transact!(conn, "[[:db/add 200 :test/ident 2]]");
|
|
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db/ident :test/ident]
|
|
[100 :db/valueType :db.type/long]
|
|
[100 :db/cardinality :db.cardinality/many]
|
|
[200 :test/ident 1]
|
|
[200 :test/ident 2]]"
|
|
);
|
|
|
|
// We can't always go from :db.cardinality/many to :db.cardinality/one.
|
|
assert_transact!(conn, "[[:db/add 100 :db/cardinality :db.cardinality/one]]",
|
|
// TODO: give more helpful error details.
|
|
Err("schema alteration failed: Cannot alter schema attribute 100 to be :db.cardinality/one"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_db_alter_unique_value() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a :db.cardinality/one attribute.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db/ident :test/ident]
|
|
[:db/add 100 :db/valueType :db.type/long]
|
|
[:db/add 100 :db/cardinality :db.cardinality/one]]"
|
|
);
|
|
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 200 :test/ident 1]
|
|
[:db/add 201 :test/ident 1]]"
|
|
);
|
|
|
|
// We can't always migrate to be :db.unique/value.
|
|
assert_transact!(conn, "[[:db/add :test/ident :db/unique :db.unique/value]]",
|
|
// TODO: give more helpful error details.
|
|
Err("schema alteration failed: Cannot alter schema attribute 100 to be :db.unique/value"));
|
|
|
|
// Not even indirectly!
|
|
assert_transact!(conn, "[[:db/add :test/ident :db/unique :db.unique/identity]]",
|
|
// TODO: give more helpful error details.
|
|
Err("schema alteration failed: Cannot alter schema attribute 100 to be :db.unique/identity"));
|
|
|
|
// But we can if we make sure there's no repeated [a v] pair.
|
|
assert_transact!(conn, "[[:db/add 201 :test/ident 2]]");
|
|
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add :test/ident :db/index true]
|
|
[:db/add :test/ident :db/unique :db.unique/value]
|
|
[:db/add :db.part/db :db.alter/attribute 100]]"
|
|
);
|
|
|
|
// We can also retract the uniqueness constraint altogether.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/retract :test/ident :db/unique :db.unique/value]]"
|
|
);
|
|
|
|
// Once we've done so, the schema shows it's not unique…
|
|
{
|
|
let attr = conn
|
|
.schema
|
|
.attribute_for_ident(&Keyword::namespaced("test", "ident"))
|
|
.unwrap()
|
|
.0;
|
|
assert_eq!(None, attr.unique);
|
|
}
|
|
|
|
// … and we can add more assertions with duplicate values.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 121 :test/ident 1]
|
|
[:db/add 221 :test/ident 2]]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_db_double_retraction_issue_818() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a :db.cardinality/one attribute.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 100 :db/ident :test/ident]
|
|
[:db/add 100 :db/valueType :db.type/string]
|
|
[:db/add 100 :db/cardinality :db.cardinality/one]
|
|
[:db/add 100 :db/unique :db.unique/identity]
|
|
[:db/add 100 :db/index true]]"
|
|
);
|
|
|
|
assert_transact!(conn, "[[:db/add 200 :test/ident \"Oi\"]]");
|
|
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 200 :test/ident \"Ai!\"]
|
|
[:db/retract 200 :test/ident \"Oi\"]]"
|
|
);
|
|
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[200 :test/ident \"Ai!\" ?tx true]
|
|
[200 :test/ident \"Oi\" ?tx false]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[100 :db/ident :test/ident]
|
|
[100 :db/valueType :db.type/string]
|
|
[100 :db/cardinality :db.cardinality/one]
|
|
[100 :db/unique :db.unique/identity]
|
|
[100 :db/index true]
|
|
[200 :test/ident \"Ai!\"]]"
|
|
);
|
|
}
|
|
|
|
/// Verify that we can't alter :db/fulltext schema characteristics at all.
|
|
#[test]
|
|
fn test_db_alter_fulltext() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a :db/fulltext true and a :db/fulltext unset attribute.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 111 :db/ident :test/fulltext]
|
|
[:db/add 111 :db/valueType :db.type/string]
|
|
[:db/add 111 :db/unique :db.unique/identity]
|
|
[:db/add 111 :db/index true]
|
|
[:db/add 111 :db/fulltext true]
|
|
[:db/add 222 :db/ident :test/string]
|
|
[:db/add 222 :db/cardinality :db.cardinality/one]
|
|
[:db/add 222 :db/valueType :db.type/string]
|
|
[:db/add 222 :db/index true]]"
|
|
);
|
|
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/retract 111 :db/fulltext true]]",
|
|
Err("bad schema assertion: Retracting attribute 12 for entity 111 not permitted.")
|
|
);
|
|
|
|
assert_transact!(conn,
|
|
"[[:db/add 222 :db/fulltext true]]",
|
|
Err("bad schema assertion: Schema alteration for existing attribute with entid 222 is not valid"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_db_fulltext() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a few :db/fulltext true attributes.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 111 :db/ident :test/fulltext]
|
|
[:db/add 111 :db/valueType :db.type/string]
|
|
[:db/add 111 :db/unique :db.unique/identity]
|
|
[:db/add 111 :db/index true]
|
|
[:db/add 111 :db/fulltext true]
|
|
[:db/add 222 :db/ident :test/other]
|
|
[:db/add 222 :db/cardinality :db.cardinality/one]
|
|
[:db/add 222 :db/valueType :db.type/string]
|
|
[:db/add 222 :db/index true]
|
|
[:db/add 222 :db/fulltext true]]"
|
|
);
|
|
|
|
// Let's check we actually have the schema characteristics we expect.
|
|
let fulltext = conn
|
|
.schema
|
|
.attribute_for_entid(111)
|
|
.cloned()
|
|
.expect(":test/fulltext");
|
|
assert_eq!(fulltext.value_type, ValueType::String);
|
|
assert_eq!(fulltext.fulltext, true);
|
|
assert_eq!(fulltext.multival, false);
|
|
assert_eq!(fulltext.unique, Some(attribute::Unique::Identity));
|
|
|
|
let other = conn
|
|
.schema
|
|
.attribute_for_entid(222)
|
|
.cloned()
|
|
.expect(":test/other");
|
|
assert_eq!(other.value_type, ValueType::String);
|
|
assert_eq!(other.fulltext, true);
|
|
assert_eq!(other.multival, false);
|
|
assert_eq!(other.unique, None);
|
|
|
|
// We can add fulltext indexed datoms.
|
|
assert_transact!(conn, "[[:db/add 301 :test/fulltext \"test this\"]]");
|
|
// value column is rowid into fulltext table.
|
|
assert_matches!(conn.fulltext_values(), "[[1 \"test this\"]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[301 :test/fulltext 1 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[111 :db/ident :test/fulltext]
|
|
[111 :db/valueType :db.type/string]
|
|
[111 :db/unique :db.unique/identity]
|
|
[111 :db/index true]
|
|
[111 :db/fulltext true]
|
|
[222 :db/ident :test/other]
|
|
[222 :db/valueType :db.type/string]
|
|
[222 :db/cardinality :db.cardinality/one]
|
|
[222 :db/index true]
|
|
[222 :db/fulltext true]
|
|
[301 :test/fulltext 1]]"
|
|
);
|
|
|
|
// We can replace existing fulltext indexed datoms.
|
|
assert_transact!(conn, "[[:db/add 301 :test/fulltext \"alternate thing\"]]");
|
|
// value column is rowid into fulltext table.
|
|
assert_matches!(
|
|
conn.fulltext_values(),
|
|
"[[1 \"test this\"]
|
|
[2 \"alternate thing\"]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[301 :test/fulltext 1 ?tx false]
|
|
[301 :test/fulltext 2 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[111 :db/ident :test/fulltext]
|
|
[111 :db/valueType :db.type/string]
|
|
[111 :db/unique :db.unique/identity]
|
|
[111 :db/index true]
|
|
[111 :db/fulltext true]
|
|
[222 :db/ident :test/other]
|
|
[222 :db/valueType :db.type/string]
|
|
[222 :db/cardinality :db.cardinality/one]
|
|
[222 :db/index true]
|
|
[222 :db/fulltext true]
|
|
[301 :test/fulltext 2]]"
|
|
);
|
|
|
|
// We can upsert keyed by fulltext indexed datoms.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add \"t\" :test/fulltext \"alternate thing\"]
|
|
[:db/add \"t\" :test/other \"other\"]]"
|
|
);
|
|
// value column is rowid into fulltext table.
|
|
assert_matches!(
|
|
conn.fulltext_values(),
|
|
"[[1 \"test this\"]
|
|
[2 \"alternate thing\"]
|
|
[3 \"other\"]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[301 :test/other 3 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[111 :db/ident :test/fulltext]
|
|
[111 :db/valueType :db.type/string]
|
|
[111 :db/unique :db.unique/identity]
|
|
[111 :db/index true]
|
|
[111 :db/fulltext true]
|
|
[222 :db/ident :test/other]
|
|
[222 :db/valueType :db.type/string]
|
|
[222 :db/cardinality :db.cardinality/one]
|
|
[222 :db/index true]
|
|
[222 :db/fulltext true]
|
|
[301 :test/fulltext 2]
|
|
[301 :test/other 3]]"
|
|
);
|
|
|
|
// We can re-use fulltext values; they won't be added to the fulltext values table twice.
|
|
assert_transact!(conn, "[[:db/add 302 :test/other \"alternate thing\"]]");
|
|
// value column is rowid into fulltext table.
|
|
assert_matches!(
|
|
conn.fulltext_values(),
|
|
"[[1 \"test this\"]
|
|
[2 \"alternate thing\"]
|
|
[3 \"other\"]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[302 :test/other 2 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[111 :db/ident :test/fulltext]
|
|
[111 :db/valueType :db.type/string]
|
|
[111 :db/unique :db.unique/identity]
|
|
[111 :db/index true]
|
|
[111 :db/fulltext true]
|
|
[222 :db/ident :test/other]
|
|
[222 :db/valueType :db.type/string]
|
|
[222 :db/cardinality :db.cardinality/one]
|
|
[222 :db/index true]
|
|
[222 :db/fulltext true]
|
|
[301 :test/fulltext 2]
|
|
[301 :test/other 3]
|
|
[302 :test/other 2]]"
|
|
);
|
|
|
|
// We can retract fulltext indexed datoms. The underlying fulltext value remains -- indeed,
|
|
// it might still be in use.
|
|
assert_transact!(conn, "[[:db/retract 302 :test/other \"alternate thing\"]]");
|
|
// value column is rowid into fulltext table.
|
|
assert_matches!(
|
|
conn.fulltext_values(),
|
|
"[[1 \"test this\"]
|
|
[2 \"alternate thing\"]
|
|
[3 \"other\"]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[302 :test/other 2 ?tx false]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(
|
|
conn.datoms(),
|
|
"[[111 :db/ident :test/fulltext]
|
|
[111 :db/valueType :db.type/string]
|
|
[111 :db/unique :db.unique/identity]
|
|
[111 :db/index true]
|
|
[111 :db/fulltext true]
|
|
[222 :db/ident :test/other]
|
|
[222 :db/valueType :db.type/string]
|
|
[222 :db/cardinality :db.cardinality/one]
|
|
[222 :db/index true]
|
|
[222 :db/fulltext true]
|
|
[301 :test/fulltext 2]
|
|
[301 :test/other 3]]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_lookup_refs_entity_column() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a few attributes.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 111 :db/ident :test/unique_value]
|
|
[:db/add 111 :db/valueType :db.type/string]
|
|
[:db/add 111 :db/unique :db.unique/value]
|
|
[:db/add 111 :db/index true]
|
|
[:db/add 222 :db/ident :test/unique_identity]
|
|
[:db/add 222 :db/valueType :db.type/long]
|
|
[:db/add 222 :db/unique :db.unique/identity]
|
|
[:db/add 222 :db/index true]
|
|
[:db/add 333 :db/ident :test/not_unique]
|
|
[:db/add 333 :db/cardinality :db.cardinality/one]
|
|
[:db/add 333 :db/valueType :db.type/keyword]
|
|
[:db/add 333 :db/index true]]"
|
|
);
|
|
|
|
// And a few datoms to match against.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 501 :test/unique_value \"test this\"]
|
|
[:db/add 502 :test/unique_value \"other\"]
|
|
[:db/add 503 :test/unique_identity -10]
|
|
[:db/add 504 :test/unique_identity -20]
|
|
[:db/add 505 :test/not_unique :test/keyword]
|
|
[:db/add 506 :test/not_unique :test/keyword]]"
|
|
);
|
|
|
|
// We can resolve lookup refs in the entity column, referring to the attribute as an entid or an ident.
|
|
assert_transact!(conn, "[[:db/add (lookup-ref :test/unique_value \"test this\") :test/not_unique :test/keyword]
|
|
[:db/add (lookup-ref 111 \"other\") :test/not_unique :test/keyword]
|
|
[:db/add (lookup-ref :test/unique_identity -10) :test/not_unique :test/keyword]
|
|
[:db/add (lookup-ref 222 -20) :test/not_unique :test/keyword]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[501 :test/not_unique :test/keyword ?tx true]
|
|
[502 :test/not_unique :test/keyword ?tx true]
|
|
[503 :test/not_unique :test/keyword ?tx true]
|
|
[504 :test/not_unique :test/keyword ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
// We cannot resolve lookup refs that aren't :db/unique.
|
|
assert_transact!(conn,
|
|
"[[:db/add (lookup-ref :test/not_unique :test/keyword) :test/not_unique :test/keyword]]",
|
|
Err("not yet implemented: Cannot resolve (lookup-ref 333 Keyword(Keyword(NamespaceableName { namespace: Some(\"test\"), name: \"keyword\" }))) with attribute that is not :db/unique"));
|
|
|
|
// We type check the lookup ref's value against the lookup ref's attribute.
|
|
assert_transact!(conn,
|
|
"[[:db/add (lookup-ref :test/unique_value :test/not_a_string) :test/not_unique :test/keyword]]",
|
|
Err("value \':test/not_a_string\' is not the expected Mentat value type String"));
|
|
|
|
// Each lookup ref in the entity column must resolve
|
|
assert_transact!(conn,
|
|
"[[:db/add (lookup-ref :test/unique_value \"unmatched string value\") :test/not_unique :test/keyword]]",
|
|
Err("no entid found for ident: couldn\'t lookup [a v]: (111, String(\"unmatched string value\"))"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_lookup_refs_value_column() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a few attributes.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 111 :db/ident :test/unique_value]
|
|
[:db/add 111 :db/valueType :db.type/string]
|
|
[:db/add 111 :db/unique :db.unique/value]
|
|
[:db/add 111 :db/index true]
|
|
[:db/add 222 :db/ident :test/unique_identity]
|
|
[:db/add 222 :db/valueType :db.type/long]
|
|
[:db/add 222 :db/unique :db.unique/identity]
|
|
[:db/add 222 :db/index true]
|
|
[:db/add 333 :db/ident :test/not_unique]
|
|
[:db/add 333 :db/cardinality :db.cardinality/one]
|
|
[:db/add 333 :db/valueType :db.type/keyword]
|
|
[:db/add 333 :db/index true]
|
|
[:db/add 444 :db/ident :test/ref]
|
|
[:db/add 444 :db/valueType :db.type/ref]
|
|
[:db/add 444 :db/unique :db.unique/identity]
|
|
[:db/add 444 :db/index true]]"
|
|
);
|
|
|
|
// And a few datoms to match against.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 501 :test/unique_value \"test this\"]
|
|
[:db/add 502 :test/unique_value \"other\"]
|
|
[:db/add 503 :test/unique_identity -10]
|
|
[:db/add 504 :test/unique_identity -20]
|
|
[:db/add 505 :test/not_unique :test/keyword]
|
|
[:db/add 506 :test/not_unique :test/keyword]]"
|
|
);
|
|
|
|
// We can resolve lookup refs in the entity column, referring to the attribute as an entid or an ident.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 601 :test/ref (lookup-ref :test/unique_value \"test this\")]
|
|
[:db/add 602 :test/ref (lookup-ref 111 \"other\")]
|
|
[:db/add 603 :test/ref (lookup-ref :test/unique_identity -10)]
|
|
[:db/add 604 :test/ref (lookup-ref 222 -20)]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[601 :test/ref 501 ?tx true]
|
|
[602 :test/ref 502 ?tx true]
|
|
[603 :test/ref 503 ?tx true]
|
|
[604 :test/ref 504 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
// We cannot resolve lookup refs for attributes that aren't :db/ref.
|
|
assert_transact!(conn,
|
|
"[[:db/add \"t\" :test/not_unique (lookup-ref :test/unique_value \"test this\")]]",
|
|
Err("not yet implemented: Cannot resolve value lookup ref for attribute 333 that is not :db/valueType :db.type/ref"));
|
|
|
|
// If a value column lookup ref resolves, we can upsert against it. Here, the lookup ref
|
|
// resolves to 501, which upserts "t" to 601.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add \"t\" :test/ref (lookup-ref :test/unique_value \"test this\")]
|
|
[:db/add \"t\" :test/not_unique :test/keyword]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[601 :test/not_unique :test/keyword ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
// Each lookup ref in the value column must resolve
|
|
assert_transact!(conn,
|
|
"[[:db/add \"t\" :test/ref (lookup-ref :test/unique_value \"unmatched string value\")]]",
|
|
Err("no entid found for ident: couldn\'t lookup [a v]: (111, String(\"unmatched string value\"))"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_explode_value_lists() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a few attributes.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 111 :db/ident :test/many]
|
|
[:db/add 111 :db/valueType :db.type/long]
|
|
[:db/add 111 :db/cardinality :db.cardinality/many]
|
|
[:db/add 222 :db/ident :test/one]
|
|
[:db/add 222 :db/valueType :db.type/long]
|
|
[:db/add 222 :db/cardinality :db.cardinality/one]]"
|
|
);
|
|
|
|
// Check that we can explode vectors for :db.cardinality/many attributes.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 501 :test/many [1]]
|
|
[:db/add 502 :test/many [2 3]]
|
|
[:db/add 503 :test/many [4 5 6]]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[501 :test/many 1 ?tx true]
|
|
[502 :test/many 2 ?tx true]
|
|
[502 :test/many 3 ?tx true]
|
|
[503 :test/many 4 ?tx true]
|
|
[503 :test/many 5 ?tx true]
|
|
[503 :test/many 6 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
// Check that we can explode nested vectors for :db.cardinality/many attributes.
|
|
assert_transact!(conn, "[[:db/add 600 :test/many [1 [2] [[3] [4]] []]]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[600 :test/many 1 ?tx true]
|
|
[600 :test/many 2 ?tx true]
|
|
[600 :test/many 3 ?tx true]
|
|
[600 :test/many 4 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
// Check that we cannot explode vectors for :db.cardinality/one attributes.
|
|
assert_transact!(conn,
|
|
"[[:db/add 501 :test/one [1]]]",
|
|
Err("not yet implemented: Cannot explode vector value for attribute 222 that is not :db.cardinality :db.cardinality/many"));
|
|
assert_transact!(conn,
|
|
"[[:db/add 501 :test/one [2 3]]]",
|
|
Err("not yet implemented: Cannot explode vector value for attribute 222 that is not :db.cardinality :db.cardinality/many"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_explode_map_notation() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a few attributes.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 111 :db/ident :test/many]
|
|
[:db/add 111 :db/valueType :db.type/long]
|
|
[:db/add 111 :db/cardinality :db.cardinality/many]
|
|
[:db/add 222 :db/ident :test/component]
|
|
[:db/add 222 :db/isComponent true]
|
|
[:db/add 222 :db/valueType :db.type/ref]
|
|
[:db/add 333 :db/ident :test/unique]
|
|
[:db/add 333 :db/unique :db.unique/identity]
|
|
[:db/add 333 :db/index true]
|
|
[:db/add 333 :db/valueType :db.type/long]
|
|
[:db/add 444 :db/ident :test/dangling]
|
|
[:db/add 444 :db/valueType :db.type/ref]]"
|
|
);
|
|
|
|
// Check that we can explode map notation without :db/id.
|
|
let report = assert_transact!(conn, "[{:test/many 1}]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?e :test/many 1 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Check that we can explode map notation with :db/id, as an entid, ident, and tempid.
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[{:db/id :db/ident :test/many 1}
|
|
{:db/id 500 :test/many 2}
|
|
{:db/id \"t\" :test/many 3}]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[1 :test/many 1 ?tx true]
|
|
[500 :test/many 2 ?tx true]
|
|
[?e :test/many 3 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{\"t\" 65537}");
|
|
|
|
// Check that we can explode map notation with :db/id as a lookup-ref or tx-function.
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[{:db/id (lookup-ref :db/ident :db/ident) :test/many 4}
|
|
{:db/id (transaction-tx) :test/many 5}]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[1 :test/many 4 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]
|
|
[?tx :test/many 5 ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Check that we can explode map notation with nested vector values.
|
|
let report = assert_transact!(conn, "[{:test/many [1 2]}]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?e :test/many 1 ?tx true]
|
|
[?e :test/many 2 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Check that we can explode map notation with nested maps if the attribute is
|
|
// :db/isComponent true.
|
|
let report = assert_transact!(conn, "[{:test/component {:test/many 1}}]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?e :test/component ?f ?tx true]
|
|
[?f :test/many 1 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Check that we can explode map notation with nested maps if the inner map contains a
|
|
// :db/unique :db.unique/identity attribute.
|
|
let report = assert_transact!(conn, "[{:test/dangling {:test/unique 10}}]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?e :test/dangling ?f ?tx true]
|
|
[?f :test/unique 10 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Verify that we can't explode map notation with nested maps if the inner map would be
|
|
// dangling.
|
|
assert_transact!(conn,
|
|
"[{:test/dangling {:test/many 11}}]",
|
|
Err("not yet implemented: Cannot explode nested map value that would lead to dangling entity for attribute 444"));
|
|
|
|
// Verify that we can explode map notation with nested maps, even if the inner map would be
|
|
// dangling, if we give a :db/id explicitly.
|
|
assert_transact!(conn, "[{:test/dangling {:db/id \"t\" :test/many 12}}]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_explode_reversed_notation() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a few attributes.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 111 :db/ident :test/many]
|
|
[:db/add 111 :db/valueType :db.type/long]
|
|
[:db/add 111 :db/cardinality :db.cardinality/many]
|
|
[:db/add 222 :db/ident :test/component]
|
|
[:db/add 222 :db/isComponent true]
|
|
[:db/add 222 :db/valueType :db.type/ref]
|
|
[:db/add 333 :db/ident :test/unique]
|
|
[:db/add 333 :db/unique :db.unique/identity]
|
|
[:db/add 333 :db/index true]
|
|
[:db/add 333 :db/valueType :db.type/long]
|
|
[:db/add 444 :db/ident :test/dangling]
|
|
[:db/add 444 :db/valueType :db.type/ref]]"
|
|
);
|
|
|
|
// Check that we can explode direct reversed notation, entids.
|
|
let report = assert_transact!(conn, "[[:db/add 100 :test/_dangling 200]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[200 :test/dangling 100 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Check that we can explode direct reversed notation, idents.
|
|
let report = assert_transact!(conn, "[[:db/add :test/many :test/_dangling :test/unique]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[333 :test/dangling :test/many ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Check that we can explode direct reversed notation, tempids.
|
|
let report = assert_transact!(conn, "[[:db/add \"s\" :test/_dangling \"t\"]]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[65537 :test/dangling 65536 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
// This is implementation specific, but it should be deterministic.
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"s\" 65536
|
|
\"t\" 65537}"
|
|
);
|
|
|
|
// Check that we can explode reversed notation in map notation without :db/id.
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[{:test/_dangling 501}
|
|
{:test/_dangling :test/many}
|
|
{:test/_dangling \"t\"}]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[111 :test/dangling ?e1 ?tx true]
|
|
[501 :test/dangling ?e2 ?tx true]
|
|
[65538 :test/dangling ?e3 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{\"t\" 65538}");
|
|
|
|
// Check that we can explode reversed notation in map notation with :db/id, entid.
|
|
let report = assert_transact!(conn, "[{:db/id 600 :test/_dangling 601}]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[601 :test/dangling 600 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Check that we can explode reversed notation in map notation with :db/id, ident.
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[{:db/id :test/component :test/_dangling :test/component}]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[222 :test/dangling :test/component ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Check that we can explode reversed notation in map notation with :db/id, tempid.
|
|
let report = assert_transact!(conn, "[{:db/id \"s\" :test/_dangling \"t\"}]");
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[65543 :test/dangling 65542 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
// This is implementation specific, but it should be deterministic.
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"s\" 65542
|
|
\"t\" 65543}"
|
|
);
|
|
|
|
// Check that we can use the same attribute in both forward and backward form in the same
|
|
// transaction.
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[[:db/add 888 :test/dangling 889]
|
|
[:db/add 888 :test/_dangling 889]]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[888 :test/dangling 889 ?tx true]
|
|
[889 :test/dangling 888 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
|
|
// Check that we can use the same attribute in both forward and backward form in the same
|
|
// transaction in map notation.
|
|
let report = assert_transact!(
|
|
conn,
|
|
"[{:db/id 998 :test/dangling 999 :test/_dangling 999}]"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[998 :test/dangling 999 ?tx true]
|
|
[999 :test/dangling 998 ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
assert_matches!(tempids(&report), "{}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_explode_reversed_notation_errors() {
|
|
let mut conn = TestConn::default();
|
|
|
|
// Start by installing a few attributes.
|
|
assert_transact!(
|
|
conn,
|
|
"[[:db/add 111 :db/ident :test/many]
|
|
[:db/add 111 :db/valueType :db.type/long]
|
|
[:db/add 111 :db/cardinality :db.cardinality/many]
|
|
[:db/add 222 :db/ident :test/component]
|
|
[:db/add 222 :db/isComponent true]
|
|
[:db/add 222 :db/valueType :db.type/ref]
|
|
[:db/add 333 :db/ident :test/unique]
|
|
[:db/add 333 :db/unique :db.unique/identity]
|
|
[:db/add 333 :db/index true]
|
|
[:db/add 333 :db/valueType :db.type/long]
|
|
[:db/add 444 :db/ident :test/dangling]
|
|
[:db/add 444 :db/valueType :db.type/ref]]"
|
|
);
|
|
|
|
// `tx-parser` should fail to parse direct reverse notation with nested value maps and
|
|
// nested value vectors, so we only test things that "get through" to the map notation
|
|
// dynamic processor here.
|
|
|
|
// Verify that we can't explode reverse notation in map notation with nested value maps.
|
|
assert_transact!(conn,
|
|
"[{:test/_dangling {:test/many 14}}]",
|
|
Err("not yet implemented: Cannot explode map notation value in :attr/_reversed notation for attribute 444"));
|
|
|
|
// Verify that we can't explode reverse notation in map notation with nested value vectors.
|
|
assert_transact!(conn,
|
|
"[{:test/_dangling [:test/many]}]",
|
|
Err("not yet implemented: Cannot explode vector value in :attr/_reversed notation for attribute 444"));
|
|
|
|
// Verify that we can't use reverse notation with non-:db.type/ref attributes.
|
|
assert_transact!(conn,
|
|
"[{:test/_unique 500}]",
|
|
Err("not yet implemented: Cannot use :attr/_reversed notation for attribute 333 that is not :db/valueType :db.type/ref"));
|
|
|
|
// Verify that we can't use reverse notation with unrecognized attributes.
|
|
assert_transact!(
|
|
conn,
|
|
"[{:test/_unknown 500}]",
|
|
Err("no entid found for ident: :test/unknown")
|
|
); // TODO: make this error reference the original :test/_unknown.
|
|
|
|
// Verify that we can't use reverse notation with bad value types: here, an unknown keyword
|
|
// that can't be coerced to a ref.
|
|
assert_transact!(
|
|
conn,
|
|
"[{:test/_dangling :test/unknown}]",
|
|
Err("no entid found for ident: :test/unknown")
|
|
);
|
|
// And here, a float.
|
|
assert_transact!(
|
|
conn,
|
|
"[{:test/_dangling 1.23}]",
|
|
Err("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]
|
|
]"#,
|
|
// This is implementation specific (due to the allocated entid), but it should be deterministic.
|
|
Err("schema constraint violation: cardinality conflicts:\n CardinalityOneAddConflict { e: 65536, a: 111, vs: {Long(123), Long(124)} }\n"));
|
|
|
|
// It also fails for map notation.
|
|
assert_transact!(conn, r#"[
|
|
{:test/unique "x", :test/one 123}
|
|
{:test/unique "x", :test/one 124}
|
|
]"#,
|
|
// This is implementation specific (due to the allocated entid), but it should be deterministic.
|
|
Err("schema constraint violation: cardinality conflicts:\n CardinalityOneAddConflict { e: 65536, a: 111, vs: {Long(123), Long(124)} }\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_conflicting_upserts() {
|
|
let mut conn = TestConn::default();
|
|
|
|
assert_transact!(
|
|
conn,
|
|
r#"[
|
|
{:db/ident :page/id :db/valueType :db.type/string :db/index true :db/unique :db.unique/identity}
|
|
{:db/ident :page/ref :db/valueType :db.type/ref :db/index true :db/unique :db.unique/identity}
|
|
{:db/ident :page/title :db/valueType :db.type/string :db/cardinality :db.cardinality/many}
|
|
]"#
|
|
);
|
|
|
|
// Let's test some conflicting upserts. First, valid data to work with -- note self references.
|
|
assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/add 111 :page/id "1"]
|
|
[:db/add 111 :page/ref 111]
|
|
[:db/add 222 :page/id "2"]
|
|
[:db/add 222 :page/ref 222]
|
|
]"#
|
|
);
|
|
|
|
// Now valid upserts. Note the references are valid.
|
|
let report = assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/add "a" :page/id "1"]
|
|
[:db/add "a" :page/ref "a"]
|
|
[:db/add "b" :page/id "2"]
|
|
[:db/add "b" :page/ref "b"]
|
|
]"#
|
|
);
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"a\" 111
|
|
\"b\" 222}"
|
|
);
|
|
|
|
// Now conflicting upserts. Note the references are reversed. This example is interesting
|
|
// because the first round `UpsertE` instances upsert, and this resolves all of the tempids
|
|
// in the `UpsertEV` instances. However, those `UpsertEV` instances lead to conflicting
|
|
// upserts! This tests that we don't resolve too far, giving a chance for those upserts to
|
|
// fail. This error message is crossing generations, although it's not reflected in the
|
|
// error data structure.
|
|
assert_transact!(conn, r#"[
|
|
[:db/add "a" :page/id "1"]
|
|
[:db/add "a" :page/ref "b"]
|
|
[:db/add "b" :page/id "2"]
|
|
[:db/add "b" :page/ref "a"]
|
|
]"#,
|
|
Err("schema constraint violation: conflicting upserts:\n tempid External(\"a\") upserts to {KnownEntid(111), KnownEntid(222)}\n tempid External(\"b\") upserts to {KnownEntid(111), KnownEntid(222)}\n"));
|
|
|
|
// Here's a case where the upsert is not resolved, just allocated, but leads to conflicting
|
|
// cardinality one datoms.
|
|
assert_transact!(conn, r#"[
|
|
[:db/add "x" :page/ref 333]
|
|
[:db/add "x" :page/ref 444]
|
|
]"#,
|
|
Err("schema constraint violation: cardinality conflicts:\n CardinalityOneAddConflict { e: 65539, a: 65537, vs: {Ref(333), Ref(444)} }\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_upsert_issue_532() {
|
|
let mut conn = TestConn::default();
|
|
|
|
assert_transact!(
|
|
conn,
|
|
r#"[
|
|
{:db/ident :page/id :db/valueType :db.type/string :db/index true :db/unique :db.unique/identity}
|
|
{:db/ident :page/ref :db/valueType :db.type/ref :db/index true :db/unique :db.unique/identity}
|
|
{:db/ident :page/title :db/valueType :db.type/string :db/cardinality :db.cardinality/many}
|
|
]"#
|
|
);
|
|
|
|
// Observe that "foo" and "zot" upsert to the same entid, and that doesn't cause a
|
|
// cardinality conflict, because we treat the input with set semantics and accept
|
|
// duplicate datoms.
|
|
let report = assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/add "bar" :page/id "z"]
|
|
[:db/add "foo" :page/ref "bar"]
|
|
[:db/add "foo" :page/title "x"]
|
|
[:db/add "zot" :page/ref "bar"]
|
|
[:db/add "zot" :db/ident :other/ident]
|
|
]"#
|
|
);
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"bar\" ?b
|
|
\"foo\" ?f
|
|
\"zot\" ?f}"
|
|
);
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?b :page/id \"z\" ?tx true]
|
|
[?f :db/ident :other/ident ?tx true]
|
|
[?f :page/ref ?b ?tx true]
|
|
[?f :page/title \"x\" ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
let report = assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/add "foo" :page/id "x"]
|
|
[:db/add "foo" :page/title "x"]
|
|
[:db/add "bar" :page/id "x"]
|
|
[:db/add "bar" :page/title "y"]
|
|
]"#
|
|
);
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"foo\" ?e
|
|
\"bar\" ?e}"
|
|
);
|
|
|
|
// One entity, two page titles.
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?e :page/id \"x\" ?tx true]
|
|
[?e :page/title \"x\" ?tx true]
|
|
[?e :page/title \"y\" ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
// Here, "foo", "bar", and "baz", all refer to the same reference, but none of them actually
|
|
// upsert to existing entities.
|
|
let report = assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/add "foo" :page/id "id"]
|
|
[:db/add "bar" :db/ident :bar/bar]
|
|
{:db/id "baz" :page/id "id" :db/ident :bar/bar}
|
|
]"#
|
|
);
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"foo\" ?e
|
|
\"bar\" ?e
|
|
\"baz\" ?e}"
|
|
);
|
|
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?e :db/ident :bar/bar ?tx true]
|
|
[?e :page/id \"id\" ?tx true]
|
|
[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
|
|
// If we do it again, everything resolves to the same IDs.
|
|
let report = assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/add "foo" :page/id "id"]
|
|
[:db/add "bar" :db/ident :bar/bar]
|
|
{:db/id "baz" :page/id "id" :db/ident :bar/bar}
|
|
]"#
|
|
);
|
|
assert_matches!(
|
|
tempids(&report),
|
|
"{\"foo\" ?e
|
|
\"bar\" ?e
|
|
\"baz\" ?e}"
|
|
);
|
|
|
|
assert_matches!(
|
|
conn.last_transaction(),
|
|
"[[?tx :db/txInstant ?ms ?tx true]]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_term_typechecking_issue_663() {
|
|
// The builder interfaces provide untrusted `Term` instances to the transactor, bypassing
|
|
// the typechecking layers invoked in the schema-aware coercion from `edn::Value` into
|
|
// `TypedValue`. Typechecking now happens lower in the stack (as well as higher in the
|
|
// stack) so we shouldn't be able to insert bad data into the store.
|
|
|
|
let mut conn = TestConn::default();
|
|
|
|
let mut terms = vec![];
|
|
|
|
terms.push(Term::AddOrRetract(
|
|
OpType::Add,
|
|
Left(KnownEntid(200)),
|
|
entids::DB_IDENT,
|
|
Left(TypedValue::typed_string("test")),
|
|
));
|
|
terms.push(Term::AddOrRetract(
|
|
OpType::Retract,
|
|
Left(KnownEntid(100)),
|
|
entids::DB_TX_INSTANT,
|
|
Left(TypedValue::Long(-1)),
|
|
));
|
|
|
|
let report = conn.transact_simple_terms(terms, InternSet::new());
|
|
|
|
match report.err().map(|e| e.kind()) {
|
|
Some(DbErrorKind::SchemaConstraintViolation(
|
|
errors::SchemaConstraintViolation::TypeDisagreements {
|
|
ref conflicting_datoms,
|
|
},
|
|
)) => {
|
|
let mut map = BTreeMap::default();
|
|
map.insert(
|
|
(100, entids::DB_TX_INSTANT, TypedValue::Long(-1)),
|
|
ValueType::Instant,
|
|
);
|
|
map.insert(
|
|
(200, entids::DB_IDENT, TypedValue::typed_string("test")),
|
|
ValueType::Keyword,
|
|
);
|
|
|
|
assert_eq!(conflicting_datoms, &map);
|
|
}
|
|
x => panic!("expected schema constraint violation, got {:?}", x),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_cardinality_constraints() {
|
|
let mut conn = TestConn::default();
|
|
|
|
assert_transact!(
|
|
conn,
|
|
r#"[
|
|
{:db/id 200 :db/ident :test/one :db/valueType :db.type/long :db/cardinality :db.cardinality/one}
|
|
{:db/id 201 :db/ident :test/many :db/valueType :db.type/long :db/cardinality :db.cardinality/many}
|
|
]"#
|
|
);
|
|
|
|
// Can add the same datom multiple times for an attribute, regardless of cardinality.
|
|
assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/add 100 :test/one 1]
|
|
[:db/add 100 :test/one 1]
|
|
[:db/add 100 :test/many 2]
|
|
[:db/add 100 :test/many 2]
|
|
]"#
|
|
);
|
|
|
|
// Can retract the same datom multiple times for an attribute, regardless of cardinality.
|
|
assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/retract 100 :test/one 1]
|
|
[:db/retract 100 :test/one 1]
|
|
[:db/retract 100 :test/many 2]
|
|
[:db/retract 100 :test/many 2]
|
|
]"#
|
|
);
|
|
|
|
// Can't transact multiple datoms for a cardinality one attribute.
|
|
assert_transact!(conn, r#"[
|
|
[:db/add 100 :test/one 3]
|
|
[:db/add 100 :test/one 4]
|
|
]"#,
|
|
Err("schema constraint violation: cardinality conflicts:\n CardinalityOneAddConflict { e: 100, a: 200, vs: {Long(3), Long(4)} }\n"));
|
|
|
|
// Can transact multiple datoms for a cardinality many attribute.
|
|
assert_transact!(
|
|
conn,
|
|
r#"[
|
|
[:db/add 100 :test/many 5]
|
|
[:db/add 100 :test/many 6]
|
|
]"#
|
|
);
|
|
|
|
// Can't add and retract the same datom for an attribute, regardless of cardinality.
|
|
assert_transact!(conn, r#"[
|
|
[:db/add 100 :test/one 7]
|
|
[:db/retract 100 :test/one 7]
|
|
[:db/add 100 :test/many 8]
|
|
[:db/retract 100 :test/many 8]
|
|
]"#,
|
|
Err("schema constraint violation: cardinality conflicts:\n AddRetractConflict { e: 100, a: 200, vs: {Long(7)} }\n AddRetractConflict { e: 100, a: 201, vs: {Long(8)} }\n"));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "sqlcipher")]
|
|
fn test_sqlcipher_openable() {
|
|
let secret_key = "key";
|
|
let sqlite = new_connection_with_key("../fixtures/v1encrypted.db", secret_key)
|
|
.expect("Failed to find test DB");
|
|
sqlite
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM sqlite_master",
|
|
rusqlite::params![],
|
|
|row| row.get::<_, i64>(0),
|
|
)
|
|
.expect("Failed to execute sql query on encrypted DB");
|
|
}
|
|
|
|
#[cfg(feature = "sqlcipher")]
|
|
fn test_open_fail<F>(opener: F)
|
|
where
|
|
F: FnOnce() -> rusqlite::Result<rusqlite::Connection>,
|
|
{
|
|
let err = opener().expect_err("Should fail to open encrypted DB");
|
|
match err {
|
|
rusqlite::Error::SqliteFailure(err, ..) => {
|
|
assert_eq!(
|
|
err.extended_code, 26,
|
|
"Should get error code 26 (not a database)."
|
|
);
|
|
}
|
|
err => {
|
|
panic!("Wrong error type! {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "sqlcipher")]
|
|
fn test_sqlcipher_requires_key() {
|
|
// Don't use a key.
|
|
test_open_fail(|| new_connection("../fixtures/v1encrypted.db"));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "sqlcipher")]
|
|
fn test_sqlcipher_requires_correct_key() {
|
|
// Use a key, but the wrong one.
|
|
test_open_fail(|| new_connection_with_key("../fixtures/v1encrypted.db", "wrong key"));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "sqlcipher")]
|
|
fn test_sqlcipher_some_transactions() {
|
|
let sqlite =
|
|
new_connection_with_key("", "hunter2").expect("Failed to create encrypted connection");
|
|
// Run a basic test as a sanity check.
|
|
run_test_add(TestConn::with_sqlite(sqlite));
|
|
}
|
|
}
|