Extract and improve test macros (#787) r=nalexander
* Part 1: Extract low-level test framework into mentat_db::debug for re-use. * Part 2: Improve assert_matches!. This corrects an incorrect pattern: a conversion method taking &self but returning an owned value should be named like `to_FOO(&self) -> FOO`. (A reference-to-reference conversion should be named like `as_FOO(&self) -> &FOO`. A consuming conversion should be named like `into_FOO(self) -> FOO`.) In addition, this pushes the conversion via `to_edn` into the `assert_matches!` macro, which lets consumers get a real data structure (say, `Datoms`) and use it directly before or after `assert_matches!`. (Currently, consumers get back `edn::Value` instances, which aren't nearly as pleasant to use as real data structures.) Co-authored-by: Grisha Kruglov <gkruglov@mozilla.com> * Part 3: Use mentat_db::debug framework in Tolstoy crate. The advantage of this approach is that compiling Tolstoy (or anything that's not db, really) can be quite a bit faster than compiling db.
This commit is contained in:
parent
e9cddd63e4
commit
675a865896
6 changed files with 317 additions and 205 deletions
187
db/src/db.rs
187
db/src/db.rs
|
@ -426,7 +426,7 @@ impl TypedSQLValue for TypedValue {
|
||||||
|
|
||||||
/// Read an arbitrary [e a v value_type_tag] materialized view from the given table in the SQL
|
/// Read an arbitrary [e a v value_type_tag] materialized view from the given table in the SQL
|
||||||
/// store.
|
/// store.
|
||||||
fn read_materialized_view(conn: &rusqlite::Connection, table: &str) -> Result<Vec<(Entid, Entid, TypedValue)>> {
|
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 mut stmt: rusqlite::Statement = conn.prepare(format!("SELECT e, a, v, value_type_tag FROM {}", table).as_str())?;
|
||||||
let m: Result<Vec<(Entid, Entid, TypedValue)>> = stmt.query_and_then(&[], |row| {
|
let m: Result<Vec<(Entid, Entid, TypedValue)>> = stmt.query_and_then(&[], |row| {
|
||||||
let e: Entid = row.get_checked(0)?;
|
let e: Entid = row.get_checked(0)?;
|
||||||
|
@ -449,7 +449,7 @@ fn read_partition_map(conn: &rusqlite::Connection) -> Result<PartitionMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the ident map materialized view from the given SQL store.
|
/// Read the ident map materialized view from the given SQL store.
|
||||||
fn read_ident_map(conn: &rusqlite::Connection) -> Result<IdentMap> {
|
pub(crate) fn read_ident_map(conn: &rusqlite::Connection) -> Result<IdentMap> {
|
||||||
let v = read_materialized_view(conn, "idents")?;
|
let v = read_materialized_view(conn, "idents")?;
|
||||||
v.into_iter().map(|(e, a, typed_value)| {
|
v.into_iter().map(|(e, a, typed_value)| {
|
||||||
if a != entids::DB_IDENT {
|
if a != entids::DB_IDENT {
|
||||||
|
@ -464,7 +464,7 @@ fn read_ident_map(conn: &rusqlite::Connection) -> Result<IdentMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the schema materialized view from the given SQL store.
|
/// Read the schema materialized view from the given SQL store.
|
||||||
fn read_attribute_map(conn: &rusqlite::Connection) -> Result<AttributeMap> {
|
pub(crate) fn read_attribute_map(conn: &rusqlite::Connection) -> Result<AttributeMap> {
|
||||||
let entid_triples = read_materialized_view(conn, "schema")?;
|
let entid_triples = read_materialized_view(conn, "schema")?;
|
||||||
let mut attribute_map = AttributeMap::default();
|
let mut attribute_map = AttributeMap::default();
|
||||||
metadata::update_attribute_map_from_entid_triples(&mut attribute_map, entid_triples, ::std::iter::empty())?;
|
metadata::update_attribute_map_from_entid_triples(&mut attribute_map, entid_triples, ::std::iter::empty())?;
|
||||||
|
@ -473,7 +473,7 @@ fn read_attribute_map(conn: &rusqlite::Connection) -> Result<AttributeMap> {
|
||||||
|
|
||||||
/// Read the materialized views from the given SQL store and return a Mentat `DB` for querying and
|
/// Read the materialized views from the given SQL store and return a Mentat `DB` for querying and
|
||||||
/// applying transactions.
|
/// applying transactions.
|
||||||
pub fn read_db(conn: &rusqlite::Connection) -> Result<DB> {
|
pub(crate) fn read_db(conn: &rusqlite::Connection) -> Result<DB> {
|
||||||
let partition_map = read_partition_map(conn)?;
|
let partition_map = read_partition_map(conn)?;
|
||||||
let ident_map = read_ident_map(conn)?;
|
let ident_map = read_ident_map(conn)?;
|
||||||
let attribute_map = read_attribute_map(conn)?;
|
let attribute_map = read_attribute_map(conn)?;
|
||||||
|
@ -1115,201 +1115,28 @@ mod tests {
|
||||||
extern crate env_logger;
|
extern crate env_logger;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use bootstrap;
|
use debug::{TestConn,tempids};
|
||||||
use debug;
|
|
||||||
use errors;
|
|
||||||
use edn;
|
|
||||||
use edn::{
|
use edn::{
|
||||||
|
self,
|
||||||
InternSet,
|
InternSet,
|
||||||
};
|
};
|
||||||
use edn::entities::{
|
use edn::entities::{
|
||||||
OpType,
|
OpType,
|
||||||
TempId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
HasSchema,
|
HasSchema,
|
||||||
Keyword,
|
Keyword,
|
||||||
KnownEntid,
|
KnownEntid,
|
||||||
TxReport,
|
|
||||||
attribute,
|
attribute,
|
||||||
};
|
};
|
||||||
use mentat_core::util::Either::*;
|
use mentat_core::util::Either::*;
|
||||||
use rusqlite;
|
|
||||||
use std::collections::{
|
use std::collections::{
|
||||||
BTreeMap,
|
BTreeMap,
|
||||||
};
|
};
|
||||||
|
use errors;
|
||||||
use internal_types::{
|
use internal_types::{
|
||||||
Term,
|
Term,
|
||||||
TermWithTempIds,
|
|
||||||
};
|
};
|
||||||
use tx::{
|
|
||||||
transact_terms,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Macro to parse a `Borrow<str>` to an `edn::Value` and assert the given `edn::Value` `matches`
|
|
||||||
// against it.
|
|
||||||
//
|
|
||||||
// This is a macro only to give nice line numbers when tests fail.
|
|
||||||
macro_rules! assert_matches {
|
|
||||||
( $input: expr, $expected: expr ) => {{
|
|
||||||
// Failure to parse the expected pattern is a coding error, so we unwrap.
|
|
||||||
let pattern_value = edn::parse::value($expected.borrow())
|
|
||||||
.expect(format!("to be able to parse expected {}", $expected).as_str())
|
|
||||||
.without_spans();
|
|
||||||
assert!($input.matches(&pattern_value),
|
|
||||||
"Expected value:\n{}\nto match pattern:\n{}\n",
|
|
||||||
$input.to_pretty(120).unwrap(),
|
|
||||||
pattern_value.to_pretty(120).unwrap());
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transact $input against the given $conn, expecting success or a `Result<TxReport, String>`.
|
|
||||||
//
|
|
||||||
// This unwraps safely and makes asserting errors pleasant.
|
|
||||||
macro_rules! assert_transact {
|
|
||||||
( $conn: expr, $input: expr, $expected: expr ) => {{
|
|
||||||
trace!("assert_transact: {}", $input);
|
|
||||||
let result = $conn.transact($input).map_err(|e| e.to_string());
|
|
||||||
assert_eq!(result, $expected.map_err(|e| e.to_string()));
|
|
||||||
}};
|
|
||||||
( $conn: expr, $input: expr ) => {{
|
|
||||||
trace!("assert_transact: {}", $input);
|
|
||||||
let result = $conn.transact($input);
|
|
||||||
assert!(result.is_ok(), "Expected Ok(_), got `{}`", result.unwrap_err());
|
|
||||||
result.unwrap()
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
// A connection that doesn't try to be clever about possibly sharing its `Schema`. Compare to
|
|
||||||
// `mentat::Conn`.
|
|
||||||
struct TestConn {
|
|
||||||
sqlite: rusqlite::Connection,
|
|
||||||
partition_map: PartitionMap,
|
|
||||||
schema: Schema,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestConn {
|
|
||||||
fn assert_materialized_views(&self) {
|
|
||||||
let materialized_ident_map = read_ident_map(&self.sqlite).expect("ident map");
|
|
||||||
let materialized_attribute_map = read_attribute_map(&self.sqlite).expect("schema map");
|
|
||||||
|
|
||||||
let materialized_schema = Schema::from_ident_map_and_attribute_map(materialized_ident_map, materialized_attribute_map).expect("schema");
|
|
||||||
assert_eq!(materialized_schema, self.schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transact<I>(&mut self, transaction: I) -> Result<TxReport> where I: Borrow<str> {
|
|
||||||
// Failure to parse the transaction is a coding error, so we unwrap.
|
|
||||||
let entities = edn::parse::entities(transaction.borrow()).expect(format!("to be able to parse {} into entities", transaction.borrow()).as_str());
|
|
||||||
|
|
||||||
let details = {
|
|
||||||
// The block scopes the borrow of self.sqlite.
|
|
||||||
// We're about to write, so go straight ahead and get an IMMEDIATE transaction.
|
|
||||||
let tx = self.sqlite.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
|
||||||
// Applying the transaction can fail, so we don't unwrap.
|
|
||||||
let details = transact(&tx, self.partition_map.clone(), &self.schema, &self.schema, NullWatcher(), entities)?;
|
|
||||||
tx.commit()?;
|
|
||||||
details
|
|
||||||
};
|
|
||||||
|
|
||||||
let (report, next_partition_map, next_schema, _watcher) = details;
|
|
||||||
self.partition_map = next_partition_map;
|
|
||||||
if let Some(next_schema) = next_schema {
|
|
||||||
self.schema = next_schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that we've updated the materialized views during transacting.
|
|
||||||
self.assert_materialized_views();
|
|
||||||
|
|
||||||
Ok(report)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transact_simple_terms<I>(&mut self, terms: I, tempid_set: InternSet<TempId>) -> Result<TxReport> where I: IntoIterator<Item=TermWithTempIds> {
|
|
||||||
let details = {
|
|
||||||
// The block scopes the borrow of self.sqlite.
|
|
||||||
// We're about to write, so go straight ahead and get an IMMEDIATE transaction.
|
|
||||||
let tx = self.sqlite.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
|
||||||
// Applying the transaction can fail, so we don't unwrap.
|
|
||||||
let details = transact_terms(&tx, self.partition_map.clone(), &self.schema, &self.schema, NullWatcher(), terms, tempid_set)?;
|
|
||||||
tx.commit()?;
|
|
||||||
details
|
|
||||||
};
|
|
||||||
|
|
||||||
let (report, next_partition_map, next_schema, _watcher) = details;
|
|
||||||
self.partition_map = next_partition_map;
|
|
||||||
if let Some(next_schema) = next_schema {
|
|
||||||
self.schema = next_schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that we've updated the materialized views during transacting.
|
|
||||||
self.assert_materialized_views();
|
|
||||||
|
|
||||||
Ok(report)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn last_tx_id(&self) -> Entid {
|
|
||||||
self.partition_map.get(&":db.part/tx".to_string()).unwrap().index - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
fn last_transaction(&self) -> edn::Value {
|
|
||||||
debug::transactions_after(&self.sqlite, &self.schema, self.last_tx_id() - 1).expect("last_transaction").0[0].into_edn()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn datoms(&self) -> edn::Value {
|
|
||||||
debug::datoms_after(&self.sqlite, &self.schema, bootstrap::TX0).expect("datoms").into_edn()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fulltext_values(&self) -> edn::Value {
|
|
||||||
debug::fulltext_values(&self.sqlite).expect("fulltext_values").into_edn()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_sqlite(mut conn: rusqlite::Connection) -> TestConn {
|
|
||||||
let db = ensure_current_version(&mut conn).unwrap();
|
|
||||||
|
|
||||||
// Does not include :db/txInstant.
|
|
||||||
let datoms = debug::datoms_after(&conn, &db.schema, 0).unwrap();
|
|
||||||
assert_eq!(datoms.0.len(), 94);
|
|
||||||
|
|
||||||
// Includes :db/txInstant.
|
|
||||||
let transactions = debug::transactions_after(&conn, &db.schema, 0).unwrap();
|
|
||||||
assert_eq!(transactions.0.len(), 1);
|
|
||||||
assert_eq!(transactions.0[0].0.len(), 95);
|
|
||||||
|
|
||||||
let mut parts = db.partition_map;
|
|
||||||
|
|
||||||
// Add a fake partition to allow tests to do things like
|
|
||||||
// [:db/add 111 :foo/bar 222]
|
|
||||||
{
|
|
||||||
let fake_partition = Partition { start: 100, end: 2000, index: 1000, allow_excision: true };
|
|
||||||
parts.insert(":db.part/fake".into(), fake_partition);
|
|
||||||
}
|
|
||||||
|
|
||||||
let test_conn = TestConn {
|
|
||||||
sqlite: conn,
|
|
||||||
partition_map: parts,
|
|
||||||
schema: db.schema,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verify that we've created the materialized views during bootstrapping.
|
|
||||||
test_conn.assert_materialized_views();
|
|
||||||
|
|
||||||
test_conn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TestConn {
|
|
||||||
fn default() -> TestConn {
|
|
||||||
TestConn::with_sqlite(new_connection("").expect("Couldn't open in-memory db"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tempids(report: &TxReport) -> edn::Value {
|
|
||||||
let mut map: BTreeMap<edn::Value, edn::Value> = BTreeMap::default();
|
|
||||||
for (tempid, &entid) in report.tempids.iter() {
|
|
||||||
map.insert(edn::Value::Text(tempid.clone()), edn::Value::Integer(entid));
|
|
||||||
}
|
|
||||||
edn::Value::Map(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_test_add(mut conn: TestConn) {
|
fn run_test_add(mut conn: TestConn) {
|
||||||
// Test inserting :db.cardinality/one elements.
|
// Test inserting :db.cardinality/one elements.
|
||||||
|
|
247
db/src/debug.rs
247
db/src/debug.rs
|
@ -9,45 +9,99 @@
|
||||||
// specific language governing permissions and limitations under the License.
|
// specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
#![allow(unused_macros)]
|
||||||
|
|
||||||
/// Low-level functions for testing.
|
/// Low-level functions for testing.
|
||||||
|
|
||||||
|
// Macro to parse a `Borrow<str>` to an `edn::Value` and assert the given `edn::Value` `matches`
|
||||||
|
// against it.
|
||||||
|
//
|
||||||
|
// This is a macro only to give nice line numbers when tests fail.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! assert_matches {
|
||||||
|
( $input: expr, $expected: expr ) => {{
|
||||||
|
// Failure to parse the expected pattern is a coding error, so we unwrap.
|
||||||
|
let pattern_value = edn::parse::value($expected.borrow())
|
||||||
|
.expect(format!("to be able to parse expected {}", $expected).as_str())
|
||||||
|
.without_spans();
|
||||||
|
let input_value = $input.to_edn();
|
||||||
|
assert!(input_value.matches(&pattern_value),
|
||||||
|
"Expected value:\n{}\nto match pattern:\n{}\n",
|
||||||
|
input_value.to_pretty(120).unwrap(),
|
||||||
|
pattern_value.to_pretty(120).unwrap());
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transact $input against the given $conn, expecting success or a `Result<TxReport, String>`.
|
||||||
|
//
|
||||||
|
// This unwraps safely and makes asserting errors pleasant.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! assert_transact {
|
||||||
|
( $conn: expr, $input: expr, $expected: expr ) => {{
|
||||||
|
trace!("assert_transact: {}", $input);
|
||||||
|
let result = $conn.transact($input).map_err(|e| e.to_string());
|
||||||
|
assert_eq!(result, $expected.map_err(|e| e.to_string()));
|
||||||
|
}};
|
||||||
|
( $conn: expr, $input: expr ) => {{
|
||||||
|
trace!("assert_transact: {}", $input);
|
||||||
|
let result = $conn.transact($input);
|
||||||
|
assert!(result.is_ok(), "Expected Ok(_), got `{}`", result.unwrap_err());
|
||||||
|
result.unwrap()
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::io::{Write};
|
use std::io::{Write};
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rusqlite;
|
use rusqlite;
|
||||||
|
use rusqlite::{TransactionBehavior};
|
||||||
use rusqlite::types::{ToSql};
|
use rusqlite::types::{ToSql};
|
||||||
use tabwriter::TabWriter;
|
use tabwriter::TabWriter;
|
||||||
|
|
||||||
use bootstrap;
|
use bootstrap;
|
||||||
use db::TypedSQLValue;
|
use db::*;
|
||||||
|
use db::{read_attribute_map,read_ident_map};
|
||||||
use edn;
|
use edn;
|
||||||
use entids;
|
use entids;
|
||||||
use errors::Result;
|
use errors::Result;
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
HasSchema,
|
HasSchema,
|
||||||
SQLValueType,
|
SQLValueType,
|
||||||
|
TxReport,
|
||||||
TypedValue,
|
TypedValue,
|
||||||
ValueType,
|
ValueType,
|
||||||
};
|
};
|
||||||
|
use edn::{
|
||||||
|
InternSet,
|
||||||
|
};
|
||||||
use edn::entities::{
|
use edn::entities::{
|
||||||
EntidOrIdent,
|
EntidOrIdent,
|
||||||
|
TempId,
|
||||||
|
};
|
||||||
|
use internal_types::{
|
||||||
|
TermWithTempIds,
|
||||||
};
|
};
|
||||||
use schema::{
|
use schema::{
|
||||||
SchemaBuilding,
|
SchemaBuilding,
|
||||||
};
|
};
|
||||||
use types::Schema;
|
use types::*;
|
||||||
|
use tx::{
|
||||||
|
transact,
|
||||||
|
transact_terms,
|
||||||
|
};
|
||||||
|
use watcher::NullWatcher;
|
||||||
|
|
||||||
/// Represents a *datom* (assertion) in the store.
|
/// Represents a *datom* (assertion) in the store.
|
||||||
#[derive(Clone,Debug,Eq,Hash,Ord,PartialOrd,PartialEq)]
|
#[derive(Clone,Debug,Eq,Hash,Ord,PartialOrd,PartialEq)]
|
||||||
pub(crate) struct Datom {
|
pub struct Datom {
|
||||||
// TODO: generalize this.
|
// TODO: generalize this.
|
||||||
e: EntidOrIdent,
|
pub e: EntidOrIdent,
|
||||||
a: EntidOrIdent,
|
pub a: EntidOrIdent,
|
||||||
v: edn::Value,
|
pub v: edn::Value,
|
||||||
tx: i64,
|
pub tx: i64,
|
||||||
added: Option<bool>,
|
pub added: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a set of datoms (assertions) in the store.
|
/// Represents a set of datoms (assertions) in the store.
|
||||||
|
@ -55,7 +109,7 @@ pub(crate) struct Datom {
|
||||||
/// To make comparision easier, we deterministically order. The ordering is the ascending tuple
|
/// To make comparision easier, we deterministically order. The ordering is the ascending tuple
|
||||||
/// ordering determined by `(e, a, (value_type_tag, v), tx)`, where `value_type_tag` is an internal
|
/// ordering determined by `(e, a, (value_type_tag, v), tx)`, where `value_type_tag` is an internal
|
||||||
/// value that is not exposed but is deterministic.
|
/// value that is not exposed but is deterministic.
|
||||||
pub(crate) struct Datoms(pub Vec<Datom>);
|
pub struct Datoms(pub Vec<Datom>);
|
||||||
|
|
||||||
/// Represents an ordered sequence of transactions in the store.
|
/// Represents an ordered sequence of transactions in the store.
|
||||||
///
|
///
|
||||||
|
@ -63,13 +117,13 @@ pub(crate) struct Datoms(pub Vec<Datom>);
|
||||||
/// ordering determined by `(e, a, (value_type_tag, v), tx, added)`, where `value_type_tag` is an
|
/// ordering determined by `(e, a, (value_type_tag, v), tx, added)`, where `value_type_tag` is an
|
||||||
/// internal value that is not exposed but is deterministic, and `added` is ordered such that
|
/// internal value that is not exposed but is deterministic, and `added` is ordered such that
|
||||||
/// retracted assertions appear before added assertions.
|
/// retracted assertions appear before added assertions.
|
||||||
pub(crate) struct Transactions(pub Vec<Datoms>);
|
pub struct Transactions(pub Vec<Datoms>);
|
||||||
|
|
||||||
/// Represents the fulltext values in the store.
|
/// Represents the fulltext values in the store.
|
||||||
pub(crate) struct FulltextValues(pub Vec<(i64, String)>);
|
pub struct FulltextValues(pub Vec<(i64, String)>);
|
||||||
|
|
||||||
impl Datom {
|
impl Datom {
|
||||||
pub(crate) fn into_edn(&self) -> edn::Value {
|
pub fn to_edn(&self) -> edn::Value {
|
||||||
let f = |entid: &EntidOrIdent| -> edn::Value {
|
let f = |entid: &EntidOrIdent| -> edn::Value {
|
||||||
match *entid {
|
match *entid {
|
||||||
EntidOrIdent::Entid(ref y) => edn::Value::Integer(y.clone()),
|
EntidOrIdent::Entid(ref y) => edn::Value::Integer(y.clone()),
|
||||||
|
@ -88,19 +142,19 @@ impl Datom {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Datoms {
|
impl Datoms {
|
||||||
pub(crate) fn into_edn(&self) -> edn::Value {
|
pub fn to_edn(&self) -> edn::Value {
|
||||||
edn::Value::Vector((&self.0).into_iter().map(|x| x.into_edn()).collect())
|
edn::Value::Vector((&self.0).into_iter().map(|x| x.to_edn()).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Transactions {
|
impl Transactions {
|
||||||
pub(crate) fn into_edn(&self) -> edn::Value {
|
pub fn to_edn(&self) -> edn::Value {
|
||||||
edn::Value::Vector((&self.0).into_iter().map(|x| x.into_edn()).collect())
|
edn::Value::Vector((&self.0).into_iter().map(|x| x.to_edn()).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FulltextValues {
|
impl FulltextValues {
|
||||||
pub(crate) fn into_edn(&self) -> edn::Value {
|
pub fn to_edn(&self) -> edn::Value {
|
||||||
edn::Value::Vector((&self.0).into_iter().map(|&(x, ref y)| edn::Value::Vector(vec![edn::Value::Integer(x), edn::Value::Text(y.clone())])).collect())
|
edn::Value::Vector((&self.0).into_iter().map(|&(x, ref y)| edn::Value::Vector(vec![edn::Value::Integer(x), edn::Value::Text(y.clone())])).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,13 +175,18 @@ impl ToIdent for TypedValue {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a numeric entid to an ident `Entid` if possible, otherwise a numeric `Entid`.
|
/// Convert a numeric entid to an ident `Entid` if possible, otherwise a numeric `Entid`.
|
||||||
fn to_entid(schema: &Schema, entid: i64) -> EntidOrIdent {
|
pub fn to_entid(schema: &Schema, entid: i64) -> EntidOrIdent {
|
||||||
schema.get_ident(entid).map_or(EntidOrIdent::Entid(entid), |ident| EntidOrIdent::Ident(ident.clone()))
|
schema.get_ident(entid).map_or(EntidOrIdent::Entid(entid), |ident| EntidOrIdent::Ident(ident.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /// Convert a symbolic ident to an ident `Entid` if possible, otherwise a numeric `Entid`.
|
||||||
|
// pub fn to_ident(schema: &Schema, entid: i64) -> Entid {
|
||||||
|
// schema.get_ident(entid).map_or(Entid::Entid(entid), |ident| Entid::Ident(ident.clone()))
|
||||||
|
// }
|
||||||
|
|
||||||
/// Return the set of datoms in the store, ordered by (e, a, v, tx), but not including any datoms of
|
/// Return the set of datoms in the store, ordered by (e, a, v, tx), but not including any datoms of
|
||||||
/// the form [... :db/txInstant ...].
|
/// the form [... :db/txInstant ...].
|
||||||
pub(crate) fn datoms<S: Borrow<Schema>>(conn: &rusqlite::Connection, schema: &S) -> Result<Datoms> {
|
pub fn datoms<S: Borrow<Schema>>(conn: &rusqlite::Connection, schema: &S) -> Result<Datoms> {
|
||||||
datoms_after(conn, schema, bootstrap::TX0 - 1)
|
datoms_after(conn, schema, bootstrap::TX0 - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +194,7 @@ pub(crate) fn datoms<S: Borrow<Schema>>(conn: &rusqlite::Connection, schema: &S)
|
||||||
/// ordered by (e, a, v, tx).
|
/// ordered by (e, a, v, tx).
|
||||||
///
|
///
|
||||||
/// The datom set returned does not include any datoms of the form [... :db/txInstant ...].
|
/// The datom set returned does not include any datoms of the form [... :db/txInstant ...].
|
||||||
pub(crate) fn datoms_after<S: Borrow<Schema>>(conn: &rusqlite::Connection, schema: &S, tx: i64) -> Result<Datoms> {
|
pub fn datoms_after<S: Borrow<Schema>>(conn: &rusqlite::Connection, schema: &S, tx: i64) -> Result<Datoms> {
|
||||||
let borrowed_schema = schema.borrow();
|
let borrowed_schema = schema.borrow();
|
||||||
|
|
||||||
let mut stmt: rusqlite::Statement = conn.prepare("SELECT e, a, v, value_type_tag, tx FROM datoms WHERE tx > ? ORDER BY e ASC, a ASC, value_type_tag ASC, v ASC, tx ASC")?;
|
let mut stmt: rusqlite::Statement = conn.prepare("SELECT e, a, v, value_type_tag, tx FROM datoms WHERE tx > ? ORDER BY e ASC, a ASC, value_type_tag ASC, v ASC, tx ASC")?;
|
||||||
|
@ -175,7 +234,7 @@ pub(crate) fn datoms_after<S: Borrow<Schema>>(conn: &rusqlite::Connection, schem
|
||||||
/// given `tx`, ordered by (tx, e, a, v).
|
/// given `tx`, ordered by (tx, e, a, v).
|
||||||
///
|
///
|
||||||
/// Each transaction returned includes the [(transaction-tx) :db/txInstant ...] datom.
|
/// Each transaction returned includes the [(transaction-tx) :db/txInstant ...] datom.
|
||||||
pub(crate) fn transactions_after<S: Borrow<Schema>>(conn: &rusqlite::Connection, schema: &S, tx: i64) -> Result<Transactions> {
|
pub fn transactions_after<S: Borrow<Schema>>(conn: &rusqlite::Connection, schema: &S, tx: i64) -> Result<Transactions> {
|
||||||
let borrowed_schema = schema.borrow();
|
let borrowed_schema = schema.borrow();
|
||||||
|
|
||||||
let mut stmt: rusqlite::Statement = conn.prepare("SELECT e, a, v, value_type_tag, tx, added FROM transactions WHERE tx > ? ORDER BY tx ASC, e ASC, a ASC, value_type_tag ASC, v ASC, added ASC")?;
|
let mut stmt: rusqlite::Statement = conn.prepare("SELECT e, a, v, value_type_tag, tx, added FROM transactions WHERE tx > ? ORDER BY tx ASC, e ASC, a ASC, value_type_tag ASC, v ASC, added ASC")?;
|
||||||
|
@ -211,7 +270,7 @@ pub(crate) fn transactions_after<S: Borrow<Schema>>(conn: &rusqlite::Connection,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the set of fulltext values in the store, ordered by rowid.
|
/// Return the set of fulltext values in the store, ordered by rowid.
|
||||||
pub(crate) fn fulltext_values(conn: &rusqlite::Connection) -> Result<FulltextValues> {
|
pub fn fulltext_values(conn: &rusqlite::Connection) -> Result<FulltextValues> {
|
||||||
let mut stmt: rusqlite::Statement = conn.prepare("SELECT rowid, text FROM fulltext_values ORDER BY rowid")?;
|
let mut stmt: rusqlite::Statement = conn.prepare("SELECT rowid, text FROM fulltext_values ORDER BY rowid")?;
|
||||||
|
|
||||||
let r: Result<Vec<_>> = stmt.query_and_then(&[], |row| {
|
let r: Result<Vec<_>> = stmt.query_and_then(&[], |row| {
|
||||||
|
@ -228,7 +287,7 @@ pub(crate) fn fulltext_values(conn: &rusqlite::Connection) -> Result<FulltextVal
|
||||||
///
|
///
|
||||||
/// The query is printed followed by a newline, then the returned columns followed by a newline, and
|
/// The query is printed followed by a newline, then the returned columns followed by a newline, and
|
||||||
/// then the data rows and columns. All columns are aligned.
|
/// then the data rows and columns. All columns are aligned.
|
||||||
pub(crate) fn dump_sql_query(conn: &rusqlite::Connection, sql: &str, params: &[&ToSql]) -> Result<String> {
|
pub fn dump_sql_query(conn: &rusqlite::Connection, sql: &str, params: &[&ToSql]) -> Result<String> {
|
||||||
let mut stmt: rusqlite::Statement = conn.prepare(sql)?;
|
let mut stmt: rusqlite::Statement = conn.prepare(sql)?;
|
||||||
|
|
||||||
let mut tw = TabWriter::new(Vec::new()).padding(2);
|
let mut tw = TabWriter::new(Vec::new()).padding(2);
|
||||||
|
@ -252,3 +311,145 @@ pub(crate) fn dump_sql_query(conn: &rusqlite::Connection, sql: &str, params: &[&
|
||||||
let dump = String::from_utf8(tw.into_inner().unwrap()).unwrap();
|
let dump = String::from_utf8(tw.into_inner().unwrap()).unwrap();
|
||||||
Ok(dump)
|
Ok(dump)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A connection that doesn't try to be clever about possibly sharing its `Schema`. Compare to
|
||||||
|
// `mentat::Conn`.
|
||||||
|
pub struct TestConn {
|
||||||
|
pub sqlite: rusqlite::Connection,
|
||||||
|
pub partition_map: PartitionMap,
|
||||||
|
pub schema: Schema,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestConn {
|
||||||
|
fn assert_materialized_views(&self) {
|
||||||
|
let materialized_ident_map = read_ident_map(&self.sqlite).expect("ident map");
|
||||||
|
let materialized_attribute_map = read_attribute_map(&self.sqlite).expect("schema map");
|
||||||
|
|
||||||
|
let materialized_schema = Schema::from_ident_map_and_attribute_map(materialized_ident_map, materialized_attribute_map).expect("schema");
|
||||||
|
assert_eq!(materialized_schema, self.schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transact<I>(&mut self, transaction: I) -> Result<TxReport> where I: Borrow<str> {
|
||||||
|
// Failure to parse the transaction is a coding error, so we unwrap.
|
||||||
|
let entities = edn::parse::entities(transaction.borrow()).expect(format!("to be able to parse {} into entities", transaction.borrow()).as_str());
|
||||||
|
|
||||||
|
let details = {
|
||||||
|
// The block scopes the borrow of self.sqlite.
|
||||||
|
// We're about to write, so go straight ahead and get an IMMEDIATE transaction.
|
||||||
|
let tx = self.sqlite.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
// Applying the transaction can fail, so we don't unwrap.
|
||||||
|
let details = transact(&tx, self.partition_map.clone(), &self.schema, &self.schema, NullWatcher(), entities)?;
|
||||||
|
tx.commit()?;
|
||||||
|
details
|
||||||
|
};
|
||||||
|
|
||||||
|
let (report, next_partition_map, next_schema, _watcher) = details;
|
||||||
|
self.partition_map = next_partition_map;
|
||||||
|
if let Some(next_schema) = next_schema {
|
||||||
|
self.schema = next_schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that we've updated the materialized views during transacting.
|
||||||
|
self.assert_materialized_views();
|
||||||
|
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transact_simple_terms<I>(&mut self, terms: I, tempid_set: InternSet<TempId>) -> Result<TxReport> where I: IntoIterator<Item=TermWithTempIds> {
|
||||||
|
let details = {
|
||||||
|
// The block scopes the borrow of self.sqlite.
|
||||||
|
// We're about to write, so go straight ahead and get an IMMEDIATE transaction.
|
||||||
|
let tx = self.sqlite.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
// Applying the transaction can fail, so we don't unwrap.
|
||||||
|
let details = transact_terms(&tx, self.partition_map.clone(), &self.schema, &self.schema, NullWatcher(), terms, tempid_set)?;
|
||||||
|
tx.commit()?;
|
||||||
|
details
|
||||||
|
};
|
||||||
|
|
||||||
|
let (report, next_partition_map, next_schema, _watcher) = details;
|
||||||
|
self.partition_map = next_partition_map;
|
||||||
|
if let Some(next_schema) = next_schema {
|
||||||
|
self.schema = next_schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that we've updated the materialized views during transacting.
|
||||||
|
self.assert_materialized_views();
|
||||||
|
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_tx_id(&self) -> Entid {
|
||||||
|
self.partition_map.get(&":db.part/tx".to_string()).unwrap().index - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_transaction(&self) -> Datoms {
|
||||||
|
transactions_after(&self.sqlite, &self.schema, self.last_tx_id() - 1).expect("last_transaction").0.pop().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transactions(&self) -> Transactions {
|
||||||
|
transactions_after(&self.sqlite, &self.schema, bootstrap::TX0).expect("transactions")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn datoms(&self) -> Datoms {
|
||||||
|
datoms_after(&self.sqlite, &self.schema, bootstrap::TX0).expect("datoms")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fulltext_values(&self) -> FulltextValues {
|
||||||
|
fulltext_values(&self.sqlite).expect("fulltext_values")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_sqlite(mut conn: rusqlite::Connection) -> TestConn {
|
||||||
|
let db = ensure_current_version(&mut conn).unwrap();
|
||||||
|
|
||||||
|
// Does not include :db/txInstant.
|
||||||
|
let datoms = datoms_after(&conn, &db.schema, 0).unwrap();
|
||||||
|
assert_eq!(datoms.0.len(), 94);
|
||||||
|
|
||||||
|
// Includes :db/txInstant.
|
||||||
|
let transactions = transactions_after(&conn, &db.schema, 0).unwrap();
|
||||||
|
assert_eq!(transactions.0.len(), 1);
|
||||||
|
assert_eq!(transactions.0[0].0.len(), 95);
|
||||||
|
|
||||||
|
let mut parts = db.partition_map;
|
||||||
|
|
||||||
|
// Add a fake partition to allow tests to do things like
|
||||||
|
// [:db/add 111 :foo/bar 222]
|
||||||
|
{
|
||||||
|
let fake_partition = Partition { start: 100, end: 2000, index: 1000, allow_excision: true };
|
||||||
|
parts.insert(":db.part/fake".into(), fake_partition);
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_conn = TestConn {
|
||||||
|
sqlite: conn,
|
||||||
|
partition_map: parts,
|
||||||
|
schema: db.schema,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify that we've created the materialized views during bootstrapping.
|
||||||
|
test_conn.assert_materialized_views();
|
||||||
|
|
||||||
|
test_conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TestConn {
|
||||||
|
fn default() -> TestConn {
|
||||||
|
TestConn::with_sqlite(new_connection("").expect("Couldn't open in-memory db"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TempIds(edn::Value);
|
||||||
|
|
||||||
|
impl TempIds {
|
||||||
|
pub fn to_edn(&self) -> edn::Value {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tempids(report: &TxReport) -> TempIds {
|
||||||
|
let mut map: BTreeMap<edn::Value, edn::Value> = BTreeMap::default();
|
||||||
|
for (tempid, &entid) in report.tempids.iter() {
|
||||||
|
map.insert(edn::Value::Text(tempid.clone()), edn::Value::Integer(entid));
|
||||||
|
}
|
||||||
|
TempIds(edn::Value::Map(map))
|
||||||
|
}
|
||||||
|
|
|
@ -39,11 +39,12 @@ pub use errors::{
|
||||||
};
|
};
|
||||||
#[macro_use] pub mod errors;
|
#[macro_use] pub mod errors;
|
||||||
|
|
||||||
|
#[macro_use] pub mod debug;
|
||||||
|
|
||||||
mod add_retract_alter_set;
|
mod add_retract_alter_set;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
mod bootstrap;
|
mod bootstrap;
|
||||||
pub mod debug;
|
|
||||||
pub mod entids;
|
pub mod entids;
|
||||||
pub mod internal_types; // pub because we need them for building entities programmatically.
|
pub mod internal_types; // pub because we need them for building entities programmatically.
|
||||||
mod metadata;
|
mod metadata;
|
||||||
|
|
|
@ -9,6 +9,7 @@ failure = "0.1.1"
|
||||||
failure_derive = "0.1.1"
|
failure_derive = "0.1.1"
|
||||||
futures = "0.1"
|
futures = "0.1"
|
||||||
hyper = "0.11"
|
hyper = "0.11"
|
||||||
|
log = "0.4"
|
||||||
tokio-core = "0.1"
|
tokio-core = "0.1"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
@ -17,6 +18,9 @@ serde_derive = "1.0"
|
||||||
lazy_static = "0.2"
|
lazy_static = "0.2"
|
||||||
uuid = { version = "0.5", features = ["v4", "serde"] }
|
uuid = { version = "0.5", features = ["v4", "serde"] }
|
||||||
|
|
||||||
|
[dependencies.edn]
|
||||||
|
path = "../edn"
|
||||||
|
|
||||||
[dependencies.mentat_core]
|
[dependencies.mentat_core]
|
||||||
path = "../core"
|
path = "../core"
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ extern crate lazy_static;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
|
|
||||||
|
extern crate edn;
|
||||||
|
|
||||||
extern crate hyper;
|
extern crate hyper;
|
||||||
// TODO https://github.com/mozilla/mentat/issues/569
|
// TODO https://github.com/mozilla/mentat/issues/569
|
||||||
// extern crate hyper_tls;
|
// extern crate hyper_tls;
|
||||||
|
@ -26,7 +28,11 @@ extern crate futures;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
extern crate serde_cbor;
|
extern crate serde_cbor;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
extern crate mentat_db;
|
|
||||||
|
// See https://github.com/rust-lang/rust/issues/44342#issuecomment-376010077.
|
||||||
|
#[cfg_attr(test, macro_use)] extern crate log;
|
||||||
|
#[cfg_attr(test, macro_use)] extern crate mentat_db;
|
||||||
|
|
||||||
extern crate mentat_core;
|
extern crate mentat_core;
|
||||||
extern crate rusqlite;
|
extern crate rusqlite;
|
||||||
extern crate uuid;
|
extern crate uuid;
|
||||||
|
|
|
@ -413,8 +413,13 @@ impl RemoteClient {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::borrow::Borrow;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use edn;
|
||||||
|
|
||||||
|
use mentat_db::debug::{TestConn};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_remote_client_bound_uri() {
|
fn test_remote_client_bound_uri() {
|
||||||
let user_uuid = Uuid::from_str(&"316ea470-ce35-4adf-9c61-e0de6e289c59").expect("uuid");
|
let user_uuid = Uuid::from_str(&"316ea470-ce35-4adf-9c61-e0de6e289c59").expect("uuid");
|
||||||
|
@ -422,4 +427,72 @@ mod tests {
|
||||||
let remote_client = RemoteClient::new(server_uri, user_uuid);
|
let remote_client = RemoteClient::new(server_uri, user_uuid);
|
||||||
assert_eq!("https://example.com/api/0.1/316ea470-ce35-4adf-9c61-e0de6e289c59", remote_client.bound_base_uri());
|
assert_eq!("https://example.com/api/0.1/316ea470-ce35-4adf-9c61-e0de6e289c59", remote_client.bound_base_uri());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add() {
|
||||||
|
let mut conn = TestConn::default();
|
||||||
|
|
||||||
|
// 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]]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue