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
|
||||
/// 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 m: Result<Vec<(Entid, Entid, TypedValue)>> = stmt.query_and_then(&[], |row| {
|
||||
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.
|
||||
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")?;
|
||||
v.into_iter().map(|(e, a, typed_value)| {
|
||||
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.
|
||||
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 mut attribute_map = AttributeMap::default();
|
||||
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
|
||||
/// 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 ident_map = read_ident_map(conn)?;
|
||||
let attribute_map = read_attribute_map(conn)?;
|
||||
|
@ -1115,201 +1115,28 @@ mod tests {
|
|||
extern crate env_logger;
|
||||
|
||||
use super::*;
|
||||
use bootstrap;
|
||||
use debug;
|
||||
use errors;
|
||||
use edn;
|
||||
use debug::{TestConn,tempids};
|
||||
use edn::{
|
||||
self,
|
||||
InternSet,
|
||||
};
|
||||
use edn::entities::{
|
||||
OpType,
|
||||
TempId,
|
||||
};
|
||||
|
||||
use mentat_core::{
|
||||
HasSchema,
|
||||
Keyword,
|
||||
KnownEntid,
|
||||
TxReport,
|
||||
attribute,
|
||||
};
|
||||
use mentat_core::util::Either::*;
|
||||
use rusqlite;
|
||||
use std::collections::{
|
||||
BTreeMap,
|
||||
};
|
||||
use errors;
|
||||
use internal_types::{
|
||||
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) {
|
||||
// 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.
|
||||
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_macros)]
|
||||
|
||||
/// 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::collections::BTreeMap;
|
||||
use std::io::{Write};
|
||||
|
||||
use itertools::Itertools;
|
||||
use rusqlite;
|
||||
use rusqlite::{TransactionBehavior};
|
||||
use rusqlite::types::{ToSql};
|
||||
use tabwriter::TabWriter;
|
||||
|
||||
use bootstrap;
|
||||
use db::TypedSQLValue;
|
||||
use db::*;
|
||||
use db::{read_attribute_map,read_ident_map};
|
||||
use edn;
|
||||
use entids;
|
||||
use errors::Result;
|
||||
use mentat_core::{
|
||||
HasSchema,
|
||||
SQLValueType,
|
||||
TxReport,
|
||||
TypedValue,
|
||||
ValueType,
|
||||
};
|
||||
use edn::{
|
||||
InternSet,
|
||||
};
|
||||
use edn::entities::{
|
||||
EntidOrIdent,
|
||||
TempId,
|
||||
};
|
||||
use internal_types::{
|
||||
TermWithTempIds,
|
||||
};
|
||||
use schema::{
|
||||
SchemaBuilding,
|
||||
};
|
||||
use types::Schema;
|
||||
use types::*;
|
||||
use tx::{
|
||||
transact,
|
||||
transact_terms,
|
||||
};
|
||||
use watcher::NullWatcher;
|
||||
|
||||
/// Represents a *datom* (assertion) in the store.
|
||||
#[derive(Clone,Debug,Eq,Hash,Ord,PartialOrd,PartialEq)]
|
||||
pub(crate) struct Datom {
|
||||
pub struct Datom {
|
||||
// TODO: generalize this.
|
||||
e: EntidOrIdent,
|
||||
a: EntidOrIdent,
|
||||
v: edn::Value,
|
||||
tx: i64,
|
||||
added: Option<bool>,
|
||||
pub e: EntidOrIdent,
|
||||
pub a: EntidOrIdent,
|
||||
pub v: edn::Value,
|
||||
pub tx: i64,
|
||||
pub added: Option<bool>,
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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.
|
||||
pub(crate) struct Datoms(pub Vec<Datom>);
|
||||
pub struct Datoms(pub Vec<Datom>);
|
||||
|
||||
/// 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
|
||||
/// internal value that is not exposed but is deterministic, and `added` is ordered such that
|
||||
/// 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.
|
||||
pub(crate) struct FulltextValues(pub Vec<(i64, String)>);
|
||||
pub struct FulltextValues(pub Vec<(i64, String)>);
|
||||
|
||||
impl Datom {
|
||||
pub(crate) fn into_edn(&self) -> edn::Value {
|
||||
pub fn to_edn(&self) -> edn::Value {
|
||||
let f = |entid: &EntidOrIdent| -> edn::Value {
|
||||
match *entid {
|
||||
EntidOrIdent::Entid(ref y) => edn::Value::Integer(y.clone()),
|
||||
|
@ -88,19 +142,19 @@ impl Datom {
|
|||
}
|
||||
|
||||
impl Datoms {
|
||||
pub(crate) fn into_edn(&self) -> edn::Value {
|
||||
edn::Value::Vector((&self.0).into_iter().map(|x| x.into_edn()).collect())
|
||||
pub fn to_edn(&self) -> edn::Value {
|
||||
edn::Value::Vector((&self.0).into_iter().map(|x| x.to_edn()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Transactions {
|
||||
pub(crate) fn into_edn(&self) -> edn::Value {
|
||||
edn::Value::Vector((&self.0).into_iter().map(|x| x.into_edn()).collect())
|
||||
pub fn to_edn(&self) -> edn::Value {
|
||||
edn::Value::Vector((&self.0).into_iter().map(|x| x.to_edn()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@ -121,13 +175,18 @@ impl ToIdent for TypedValue {
|
|||
}
|
||||
|
||||
/// 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()))
|
||||
}
|
||||
|
||||
// /// 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
|
||||
/// 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)
|
||||
}
|
||||
|
||||
|
@ -135,7 +194,7 @@ pub(crate) fn datoms<S: Borrow<Schema>>(conn: &rusqlite::Connection, schema: &S)
|
|||
/// ordered by (e, a, v, tx).
|
||||
///
|
||||
/// 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 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).
|
||||
///
|
||||
/// 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 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.
|
||||
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 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
|
||||
/// 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 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();
|
||||
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 debug;
|
||||
|
||||
mod add_retract_alter_set;
|
||||
pub mod cache;
|
||||
pub mod db;
|
||||
mod bootstrap;
|
||||
pub mod debug;
|
||||
pub mod entids;
|
||||
pub mod internal_types; // pub because we need them for building entities programmatically.
|
||||
mod metadata;
|
||||
|
|
|
@ -9,6 +9,7 @@ failure = "0.1.1"
|
|||
failure_derive = "0.1.1"
|
||||
futures = "0.1"
|
||||
hyper = "0.11"
|
||||
log = "0.4"
|
||||
tokio-core = "0.1"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
@ -17,6 +18,9 @@ serde_derive = "1.0"
|
|||
lazy_static = "0.2"
|
||||
uuid = { version = "0.5", features = ["v4", "serde"] }
|
||||
|
||||
[dependencies.edn]
|
||||
path = "../edn"
|
||||
|
||||
[dependencies.mentat_core]
|
||||
path = "../core"
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ extern crate lazy_static;
|
|||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
extern crate edn;
|
||||
|
||||
extern crate hyper;
|
||||
// TODO https://github.com/mozilla/mentat/issues/569
|
||||
// extern crate hyper_tls;
|
||||
|
@ -26,7 +28,11 @@ extern crate futures;
|
|||
extern crate serde;
|
||||
extern crate serde_cbor;
|
||||
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 rusqlite;
|
||||
extern crate uuid;
|
||||
|
|
|
@ -413,8 +413,13 @@ impl RemoteClient {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::borrow::Borrow;
|
||||
use std::str::FromStr;
|
||||
|
||||
use edn;
|
||||
|
||||
use mentat_db::debug::{TestConn};
|
||||
|
||||
#[test]
|
||||
fn test_remote_client_bound_uri() {
|
||||
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);
|
||||
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