2017-01-26 00:13:56 +00:00
// 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) ]
2017-02-08 22:04:32 +00:00
use std ::iter ::once ;
2017-02-09 20:23:04 +00:00
use std ::path ::Path ;
2017-02-08 22:04:32 +00:00
use itertools ;
use itertools ::Itertools ;
2017-01-26 00:13:56 +00:00
use rusqlite ;
use rusqlite ::types ::{ ToSql , ToSqlOutput } ;
2017-02-08 22:04:32 +00:00
use time ;
2017-01-26 00:13:56 +00:00
2017-02-08 22:04:32 +00:00
use ::{ repeat_values , to_namespaced_keyword } ;
2017-01-26 00:13:56 +00:00
use bootstrap ;
use edn ::types ::Value ;
2017-02-08 22:04:32 +00:00
use entids ;
2017-01-26 00:13:56 +00:00
use errors ::* ;
use mentat_tx ::entities as entmod ;
2017-02-08 23:45:09 +00:00
use mentat_tx ::entities ::{ Entity , OpType } ;
2017-01-26 00:13:56 +00:00
use types ::* ;
2017-02-09 20:23:04 +00:00
pub fn new_connection < T > ( uri : T ) -> rusqlite ::Result < rusqlite ::Connection > where T : AsRef < Path > {
let conn = match uri . as_ref ( ) . to_string_lossy ( ) . len ( ) {
0 = > rusqlite ::Connection ::open_in_memory ( ) ? ,
_ = > rusqlite ::Connection ::open ( uri ) ? ,
} ;
conn . execute_batch ( "
PRAGMA page_size = 32768 ;
PRAGMA journal_mode = wal ;
PRAGMA wal_autocheckpoint = 32 ;
PRAGMA journal_size_limit = 3145728 ;
PRAGMA foreign_keys = ON ;
" )?;
Ok ( conn )
2017-01-26 00:13:56 +00:00
}
/// Version history:
///
/// 1: initial schema.
/// 2: added :db.schema/version and /attribute in bootstrap; assigned idents 36 and 37, so we bump
/// the part range here; tie bootstrapping to the SQLite user_version.
pub const CURRENT_VERSION : i32 = 2 ;
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 2).
#[ cfg_attr(rustfmt, rustfmt_skip) ]
static ref V2_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 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)"# ,
r # "CREATE INDEX idx_transactions_tx ON transactions (tx, added)"# ,
// 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 schema.
r # "CREATE TABLE idents (ident TEXT NOT NULL PRIMARY KEY, entid INTEGER UNIQUE NOT NULL)"# ,
r #" CREATE TABLE schema (ident TEXT NOT NULL, attr TEXT NOT NULL, value BLOB NOT NULL, value_type_tag SMALLINT NOT NULL,
FOREIGN KEY ( ident ) REFERENCES idents ( ident ) ) " #,
r # "CREATE INDEX idx_schema_unique ON schema (ident, attr, value, value_type_tag)"# ,
r # "CREATE TABLE parts (part TEXT NOT NULL PRIMARY KEY, start INTEGER NOT NULL, idx INTEGER 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 ) , & [ ] )
. chain_err ( | | " Could not set_user_version " )
. map ( | _ | ( ) )
}
/// 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 > {
conn . query_row ( " PRAGMA user_version " , & [ ] , | row | {
row . get ( 0 )
} )
. chain_err ( | | " Could not get_user_version " )
}
// TODO: rename "SQL" functions to align with "datoms" functions.
pub fn create_current_version ( conn : & mut rusqlite ::Connection ) -> Result < i32 > {
let tx = conn . transaction ( ) ? ;
for statement in ( & V2_STATEMENTS ) . iter ( ) {
tx . execute ( statement , & [ ] ) ? ;
}
let bootstrap_partition_map = bootstrap ::bootstrap_partition_map ( ) ;
// TODO: think more carefully about allocating new parts and bitmasking 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.
for ( part , partition ) in bootstrap_partition_map . iter ( ) {
// TODO: Convert "keyword" part to SQL using Value conversion.
tx . execute ( " INSERT INTO parts VALUES (?, ?, ?) " , & [ part , & partition . start , & partition . index ] ) ? ;
}
let bootstrap_db = DB ::new ( bootstrap_partition_map , bootstrap ::bootstrap_schema ( ) ) ;
2017-02-08 22:04:32 +00:00
bootstrap_db . transact_internal ( & tx , & bootstrap ::bootstrap_entities ( ) [ .. ] , bootstrap ::TX0 ) ? ;
2017-01-26 00:13:56 +00:00
set_user_version ( & tx , CURRENT_VERSION ) ? ;
let user_version = get_user_version ( & tx ) ? ;
// TODO: use the drop semantics to do this automagically?
tx . commit ( ) ? ;
Ok ( user_version )
}
// (def v2-statements v1-statements)
// (defn create-temp-tx-lookup-statement [table-name]
// // n.b., v0/value_type_tag0 can be NULL, in which case we look up v from datoms;
// // and the datom columns are NULL into the LEFT JOIN fills them in.
// // The table-name is not escaped in any way, in order to allow r#"temp.dotted" names.
// // TODO: update comment about sv.
// [(str r#"CREATE TABLE IF NOT EXISTS r#" table-name
// r#" (e0 INTEGER NOT NULL, a0 SMALLINT NOT NULL, v0 BLOB NOT NULL, tx0 INTEGER NOT NULL, added0 TINYINT NOT NULL,
// value_type_tag0 SMALLINT NOT NULL,
// index_avet0 TINYINT, index_vaet0 TINYINT,
// index_fulltext0 TINYINT,
// unique_value0 TINYINT,
// sv BLOB,
// svalue_type_tag SMALLINT,
// rid INTEGER,
// e INTEGER, a SMALLINT, v BLOB, tx INTEGER, value_type_tag SMALLINT)")])
// (defn create-temp-tx-lookup-eavt-statement [idx-name table-name]
// // Note that the consuming code creates and drops the indexes
// // manually, which makes insertion slightly faster.
// // This index prevents overlapping transactions.
// // The idx-name and table-name are not escaped in any way, in order
// // to allow r#"temp.dotted" names.
// // TODO: drop added0?
// [(str r#"CREATE UNIQUE INDEX IF NOT EXISTS r#"#,
// idx-name
// r#" ON r#"#,
// table-name
// r#" (e0, a0, v0, added0, value_type_tag0) WHERE sv IS NOT NULL")])
// (defn <create-current-version
// [db bootstrapper]
// (println r#"Creating database at" current-version)
// (s/in-transaction!
// db
// #(go-pair
// (doseq [statement v2-statements]
// (try
// (<? (s/execute! db [statement]))
// (catch #?(:clj Throwable :cljs js/Error) e
// (throw (ex-info r#"Failed to execute statement" {:statement statement} e)))))
// (<? (bootstrapper db 0))
// (<? (s/set-user-version db current-version))
// [0 (<? (s/get-user-version db))])))
// (defn <update-from-version
// [db from-version bootstrapper]
// {:pre [(> from-version 0)]} // Or we'd create-current-version instead.
// {:pre [(< from-version current-version)]} // Or we wouldn't need to update-from-version.
// (println r#"Upgrading database from" from-version r#"to" current-version)
// (s/in-transaction!
// db
// #(go-pair
// // We must only be migrating from v1 to v2.
// (let [statement r#"UPDATE parts SET idx = idx + 2 WHERE part = ?"]
// (try
// (<? (s/execute!
// db
// [statement :db.part/db]))
// (catch #?(:clj Throwable :cljs js/Error) e
// (throw (ex-info r#"Failed to execute statement" {:statement statement} e)))))
// (<? (bootstrapper db from-version))
// (<? (s/set-user-version db current-version))
// [from-version (<? (s/get-user-version db))])))
// (defn <ensure-current-version
// r#"Returns a pair: [previous-version current-version]."#,
// [db bootstrapper]
// (go-pair
// (let [v (<? (s/get-user-version db))]
// (cond
// (= v current-version)
// [v v]
// (= v 0)
// (<? (<create-current-version db bootstrapper))
// (< v current-version)
// (<? (<update-from-version db v bootstrapper))))))
// */
pub fn update_from_version ( conn : & mut rusqlite ::Connection , current_version : i32 ) -> Result < i32 > {
if current_version < 0 | | CURRENT_VERSION < = current_version {
bail! ( ErrorKind ::BadSQLiteStoreVersion ( current_version ) )
}
let tx = conn . transaction ( ) ? ;
// TODO: actually implement upgrade.
set_user_version ( & tx , CURRENT_VERSION ) ? ;
let user_version = get_user_version ( & tx ) ? ;
// TODO: use the drop semantics to do this automagically?
tx . commit ( ) ? ;
Ok ( user_version )
}
pub fn ensure_current_version ( conn : & mut rusqlite ::Connection ) -> Result < i32 > {
let user_version = get_user_version ( & conn ) ? ;
match user_version {
CURRENT_VERSION = > Ok ( user_version ) ,
0 = > create_current_version ( conn ) ,
v = > update_from_version ( conn , v ) ,
}
}
2017-02-08 21:59:56 +00:00
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 {
2017-01-26 00:13:56 +00:00
/// Given a SQLite `value` and a `value_type_tag`, return the corresponding `TypedValue`.
2017-02-08 21:59:56 +00:00
fn from_sql_value_pair ( value : rusqlite ::types ::Value , value_type_tag : i32 ) -> Result < TypedValue > {
2017-02-08 22:04:32 +00:00
match ( value_type_tag , value ) {
2017-01-26 00:13:56 +00:00
( 0 , rusqlite ::types ::Value ::Integer ( x ) ) = > Ok ( TypedValue ::Ref ( x ) ) ,
( 1 , rusqlite ::types ::Value ::Integer ( x ) ) = > Ok ( TypedValue ::Boolean ( 0 ! = 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 ( TypedValue ::String ( x ) ) ,
( 13 , rusqlite ::types ::Value ::Text ( x ) ) = > Ok ( TypedValue ::Keyword ( x ) ) ,
2017-02-08 22:04:32 +00:00
( _ , value ) = > bail! ( ErrorKind ::BadSQLValuePair ( value , value_type_tag ) ) ,
2017-01-26 00:13:56 +00:00
}
}
/// 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.
2017-02-08 21:59:56 +00:00
fn from_edn_value ( value : & Value ) -> Option < TypedValue > {
2017-01-26 00:13:56 +00:00
match value {
& Value ::Boolean ( x ) = > Some ( TypedValue ::Boolean ( x ) ) ,
& Value ::Integer ( x ) = > Some ( TypedValue ::Long ( x ) ) ,
& Value ::Float ( ref x ) = > Some ( TypedValue ::Double ( x . clone ( ) ) ) ,
& Value ::Text ( ref x ) = > Some ( TypedValue ::String ( x . clone ( ) ) ) ,
& Value ::NamespacedKeyword ( ref x ) = > Some ( TypedValue ::Keyword ( x . to_string ( ) ) ) ,
_ = > None
}
}
/// Return the corresponding SQLite `value` and `value_type_tag` pair.
2017-02-08 21:59:56 +00:00
fn to_sql_value_pair < ' a > ( & ' a self ) -> ( ToSqlOutput < ' a > , i32 ) {
2017-01-26 00:13:56 +00:00
match self {
& TypedValue ::Ref ( x ) = > ( rusqlite ::types ::Value ::Integer ( x ) . into ( ) , 0 ) ,
& TypedValue ::Boolean ( x ) = > ( rusqlite ::types ::Value ::Integer ( if x { 1 } else { 0 } ) . into ( ) , 1 ) ,
// SQLite distinguishes integral from decimal types, allowing long and double to share a tag.
& TypedValue ::Long ( x ) = > ( rusqlite ::types ::Value ::Integer ( x ) . into ( ) , 5 ) ,
& TypedValue ::Double ( x ) = > ( rusqlite ::types ::Value ::Real ( x . into_inner ( ) ) . into ( ) , 5 ) ,
& TypedValue ::String ( ref x ) = > ( rusqlite ::types ::ValueRef ::Text ( x . as_str ( ) ) . into ( ) , 10 ) ,
& TypedValue ::Keyword ( ref x ) = > ( rusqlite ::types ::ValueRef ::Text ( x . as_str ( ) ) . into ( ) , 13 ) ,
}
}
/// Return the corresponding EDN `value` and `value_type` pair.
2017-02-08 21:59:56 +00:00
fn to_edn_value_pair ( & self ) -> ( Value , ValueType ) {
2017-01-26 00:13:56 +00:00
match self {
& TypedValue ::Ref ( x ) = > ( Value ::Integer ( x ) , ValueType ::Ref ) ,
& TypedValue ::Boolean ( x ) = > ( Value ::Boolean ( x ) , ValueType ::Boolean ) ,
& TypedValue ::Long ( x ) = > ( Value ::Integer ( x ) , ValueType ::Long ) ,
& TypedValue ::Double ( x ) = > ( Value ::Float ( x ) , ValueType ::Double ) ,
& TypedValue ::String ( ref x ) = > ( Value ::Text ( x . clone ( ) ) , ValueType ::String ) ,
2017-02-08 22:04:32 +00:00
& TypedValue ::Keyword ( ref x ) = > ( Value ::NamespacedKeyword ( to_namespaced_keyword ( & x ) . unwrap ( ) ) , ValueType ::Keyword ) ,
2017-01-26 00:13:56 +00:00
}
}
}
/// Read the ident map materialized view from the given SQL store.
pub fn read_ident_map ( conn : & rusqlite ::Connection ) -> Result < IdentMap > {
let mut stmt : rusqlite ::Statement = conn . prepare ( " SELECT ident, entid FROM idents " ) ? ;
let m = stmt . query_and_then ( & [ ] , | row | -> Result < ( String , Entid ) > {
Ok ( ( row . get ( 0 ) , row . get ( 1 ) ) )
} ) ? . collect ( ) ;
m
}
/// Read the partition map materialized view from the given SQL store.
pub fn read_partition_map ( conn : & rusqlite ::Connection ) -> Result < PartitionMap > {
let mut stmt : rusqlite ::Statement = conn . prepare ( " SELECT part, start, idx FROM parts " ) ? ;
let m = stmt . query_and_then ( & [ ] , | row | -> Result < ( String , Partition ) > {
Ok ( ( row . get_checked ( 0 ) ? , Partition ::new ( row . get_checked ( 1 ) ? , row . get_checked ( 2 ) ? ) ) )
} ) ? . collect ( ) ;
m
}
/// Read the schema materialized view from the given SQL store.
pub fn read_schema ( conn : & rusqlite ::Connection , ident_map : & IdentMap ) -> Result < Schema > {
let mut stmt : rusqlite ::Statement = conn . prepare ( " SELECT ident, attr, value, value_type_tag FROM schema " ) ? ;
let r : Result < Vec < ( String , String , TypedValue ) > > = stmt . query_and_then ( & [ ] , | row | {
// Each row looks like :db/index|:db/valueType|28|0. Observe that 28|0 represents a
// :db.type/ref to entid 28, which needs to be converted to a TypedValue.
// TODO: don't use textual ident and attr; just use entids directly.
let symbolic_ident : String = row . get_checked ( 0 ) ? ;
let symbolic_attr : String = row . get_checked ( 1 ) ? ;
let v : rusqlite ::types ::Value = row . get_checked ( 2 ) ? ;
let value_type_tag : i32 = row . get_checked ( 3 ) ? ;
2017-02-08 22:04:32 +00:00
let typed_value = TypedValue ::from_sql_value_pair ( v , value_type_tag ) ? ;
2017-01-26 00:13:56 +00:00
Ok ( ( symbolic_ident , symbolic_attr , typed_value ) )
} ) ? . collect ( ) ;
r . and_then ( | triples | Schema ::from_ident_map_and_triples ( ident_map . clone ( ) , triples ) )
}
/// 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 > {
let partition_map = read_partition_map ( conn ) ? ;
let ident_map = read_ident_map ( conn ) ? ;
let schema = read_schema ( conn , & ident_map ) ? ;
Ok ( DB ::new ( partition_map , schema ) )
}
2017-02-08 22:04:32 +00:00
/// Internal representation of an [e a v added] datom, ready to be transacted against the store.
type ReducedEntity = ( i64 , i64 , TypedValue , bool ) ;
#[ derive(Clone,Debug,Eq,Hash,Ord,PartialOrd,PartialEq) ]
pub enum SearchType {
Exact ,
Inexact ,
}
2017-01-26 00:13:56 +00:00
impl DB {
/// Do schema-aware typechecking and coercion.
///
/// Either assert that the given value is in the attribute's value set, or (in limited cases)
/// coerce the given value into the attribute's value set.
pub fn to_typed_value ( & self , value : & Value , attribute : & Attribute ) -> Result < TypedValue > {
// TODO: encapsulate entid-ident-attribute for better error messages.
match TypedValue ::from_edn_value ( value ) {
// We don't recognize this EDN at all. Get out!
None = > bail! ( ErrorKind ::BadEDNValuePair ( value . clone ( ) , attribute . value_type . clone ( ) ) ) ,
Some ( typed_value ) = > match ( & attribute . value_type , typed_value ) {
// Most types don't coerce at all.
( & ValueType ::Boolean , tv @ TypedValue ::Boolean ( _ ) ) = > Ok ( tv ) ,
( & ValueType ::Long , tv @ TypedValue ::Long ( _ ) ) = > Ok ( tv ) ,
( & ValueType ::Double , tv @ TypedValue ::Double ( _ ) ) = > Ok ( tv ) ,
( & ValueType ::String , tv @ TypedValue ::String ( _ ) ) = > Ok ( tv ) ,
( & ValueType ::Keyword , tv @ TypedValue ::Keyword ( _ ) ) = > Ok ( tv ) ,
// Ref coerces a little: we interpret some things depending on the schema as a Ref.
( & ValueType ::Ref , TypedValue ::Long ( x ) ) = > Ok ( TypedValue ::Ref ( x ) ) ,
( & ValueType ::Ref , TypedValue ::Keyword ( ref x ) ) = > self . schema . require_entid ( & x . to_string ( ) ) . map ( | & entid | TypedValue ::Ref ( entid ) ) ,
// Otherwise, we have a type mismatch.
( value_type , _ ) = > bail! ( ErrorKind ::BadEDNValuePair ( value . clone ( ) , value_type . clone ( ) ) ) ,
}
}
}
2017-02-08 22:04:32 +00:00
/// Create empty temporary tables for search parameters and search results.
fn create_temp_tables ( & self , conn : & rusqlite ::Connection ) -> 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"# ,
// TODO: compress bit flags into a single bit field, and expand when inserting into
// `datoms` and `transactions`.
// TODO: drop tx0 entirely.
r #" CREATE TABLE temp.exact_searches (
e0 INTEGER NOT NULL ,
a0 SMALLINT NOT NULL ,
v0 BLOB NOT NULL ,
value_type_tag0 SMALLINT NOT NULL ,
tx0 INTEGER NOT NULL ,
added0 TINYINT NOT NULL ,
index_avet0 TINYINT NOT NULL ,
index_vaet0 TINYINT NOT NULL ,
index_fulltext0 TINYINT NOT NULL ,
unique_value0 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 ,
tx0 INTEGER NOT NULL ,
added0 TINYINT NOT NULL ,
index_avet0 TINYINT NOT NULL ,
index_vaet0 TINYINT NOT NULL ,
index_fulltext0 TINYINT NOT NULL ,
unique_value0 TINYINT NOT NULL ) " #,
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 ,
tx0 INTEGER NOT NULL ,
added0 TINYINT NOT NULL ,
index_avet0 TINYINT NOT NULL ,
index_vaet0 TINYINT NOT NULL ,
index_fulltext0 TINYINT NOT NULL ,
unique_value0 TINYINT NOT NULL ,
search_type STRING NOT NULL ,
rid INTEGER ,
v BLOB ) " #,
// It is an error to transact the same [e a v] twice in one transaction. This index will
// cause insertion to fail if a transaction tries to do that. (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 = conn . prepare_cached ( statement ) ? ;
stmt . execute ( & [ ] )
. map ( | _c | ( ) )
. chain_err ( | | " Failed to create temporary tables " ) ? ;
}
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 , conn : & rusqlite ::Connection , entities : & ' a [ ReducedEntity ] , tx : Entid , search_type : SearchType ) -> Result < ( ) > {
let bindings_per_statement = 10 ;
let chunks : itertools ::IntoChunks < _ > = entities . into_iter ( ) . chunks ( ::SQLITE_MAX_VARIABLE_NUMBER / 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, index_avet0, index_vaet0, index_fulltext0, unique_value0)
let block : Result < Vec < ( i64 /* e */ , i64 /* a */ ,
ToSqlOutput < ' a > /* value */ , /* value_type_tag */ i32 ,
/* added0 */ bool ,
/* index_avet0 */ bool ,
/* index_vaet0 */ bool ,
/* index_fulltext0 */ bool ,
/* unique_value0 */ bool ) > > = chunk . map ( | & ( e , a , ref typed_value , added ) | {
count + = 1 ;
let attribute : & Attribute = self . schema . require_attribute_for_entid ( & a ) ? ;
// 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 . index ,
attribute . value_type = = ValueType ::Ref ,
attribute . fulltext ,
attribute . unique_value ) )
} ) . collect ( ) ;
let block = block ? ;
// `params` reference computed values in `block`.
let params : Vec < & ToSql > = block . iter ( ) . flat_map ( | & ( ref e , ref a , ref value , ref value_type_tag , added , index_avet , index_vaet , index_fulltext , unique_value ) | {
// Avoid inner heap allocation.
// TODO: extract some finite length iterator to make this less indented!
once ( e as & ToSql )
. chain ( once ( a as & ToSql )
. chain ( once ( value as & ToSql )
. chain ( once ( value_type_tag as & ToSql )
. chain ( once ( & tx as & ToSql )
. chain ( once ( to_bool_ref ( added ) as & ToSql )
. chain ( once ( to_bool_ref ( index_avet ) as & ToSql )
. chain ( once ( to_bool_ref ( index_vaet ) as & ToSql )
. chain ( once ( to_bool_ref ( index_fulltext ) as & ToSql )
. chain ( once ( to_bool_ref ( unique_value ) as & ToSql ) ) ) ) ) ) ) ) ) )
} ) . collect ( ) ;
// TODO: cache this for selected values of count.
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, tx0, added0, index_avet0, index_vaet0, index_fulltext0, unique_value0) VALUES {} " , values )
} else {
format! ( " INSERT INTO temp.inexact_searches (e0, a0, v0, value_type_tag0, tx0, added0, index_avet0, index_vaet0, index_fulltext0, unique_value0) VALUES {} " , values )
} ;
// TODO: consider ensuring we inserted the expected number of rows.
let mut stmt = conn . prepare_cached ( s . as_str ( ) ) ? ;
stmt . execute ( & params )
. map ( | _c | ( ) )
. chain_err ( | | " Could not insert non-fts one statements into temporary search table! " )
} ) . collect ::< Result < Vec < ( ) > > > ( ) ;
results . map ( | _ | ( ) )
}
/// Take search rows and complete `temp.search_results`.
///
/// See https://github.com/mozilla/mentat/wiki/Transacting:-entity-to-SQL-translation.
fn search ( & self , 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 . tx0 , t . added0 , t . index_avet0 , t . index_vaet0 , t . index_fulltext0 , t . unique_value0 , ' :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 . tx0 , t . added0 , t . index_avet0 , t . index_vaet0 , t . index_fulltext0 , t . unique_value0 , ' :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 ( & [ ] )
. map ( | _c | ( ) )
. chain_err ( | | " Could not search! " )
}
/// 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.
// TODO: capture `conn` in a `TxInternal` structure.
fn insert_transaction ( & self , conn : & rusqlite ::Connection , tx : Entid ) -> Result < ( ) > {
let s = r #"
INSERT INTO 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 ] )
. map ( | _c | ( ) )
. chain_err ( | | " Could not insert transaction: failed to add datoms not already present " ) ? ;
let s = r #"
INSERT INTO transactions ( e , a , v , tx , added , value_type_tag )
SELECT 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 ] )
. map ( | _c | ( ) )
. chain_err ( | | " Could not insert transaction: failed to retract datoms already present " ) ? ;
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.
// TODO: capture `conn` in a `TxInternal` structure.
fn update_datoms ( & self , 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 ( & [ ] )
. map ( | _c | ( ) )
. chain_err ( | | " Could not update datoms: failed to retract datoms already present " ) ? ;
// Insert datoms that were added and not already present.
let s = 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 ,
index_avet0 , index_vaet0 , index_fulltext0 , unique_value0
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 ] )
. map ( | _c | ( ) )
. chain_err ( | | " Could not update datoms: failed to add datoms not already present " ) ? ;
Ok ( ( ) )
}
/// Transact the given `entities` against the given SQLite `conn`, using the metadata in
/// `self.DB`.
///
/// This approach is explained in https://github.com/mozilla/mentat/wiki/Transacting.
2017-01-26 00:13:56 +00:00
// TODO: move this to the transactor layer.
2017-02-08 22:04:32 +00:00
pub fn transact_internal ( & self , conn : & rusqlite ::Connection , entities : & [ Entity ] , tx : Entid ) -> Result < ( ) > {
// TODO: push these into an internal transaction report?
/// Assertions that are :db.cardinality/one and not :db.fulltext.
let mut non_fts_one : Vec < ReducedEntity > = vec! [ ] ;
/// Assertions that are :db.cardinality/many and not :db.fulltext.
let mut non_fts_many : Vec < ReducedEntity > = vec! [ ] ;
// Transact [:db/add :db/txInstant NOW :db/tx].
// TODO: allow this to be present in the transaction data.
let now = time ::get_time ( ) ;
let tx_instant = ( now . sec as i64 * 1_000 ) + ( now . nsec as i64 / ( 1_000_000 ) ) ;
non_fts_one . push ( ( tx ,
entids ::DB_TX_INSTANT ,
TypedValue ::Long ( tx_instant ) ,
true ) ) ;
// Right now, this could be a for loop, saving some mapping, collecting, and type
// annotations. However, I expect it to be a multi-stage map as we start to transform the
// underlying entities, in which case this expression is more natural than for loops.
2017-01-26 00:13:56 +00:00
let r : Vec < Result < ( ) > > = entities . into_iter ( ) . map ( | entity : & Entity | -> Result < ( ) > {
match * entity {
2017-02-08 23:45:09 +00:00
Entity ::AddOrRetract {
op : OpType ::Add ,
2017-02-08 22:04:32 +00:00
e : entmod ::EntidOrLookupRef ::Entid ( ref e_ ) ,
a : ref a_ ,
2017-02-08 23:45:09 +00:00
v : entmod ::ValueOrLookupRef ::Value ( ref v_ ) } = > {
2017-01-26 00:13:56 +00:00
2017-02-08 22:04:32 +00:00
let e : i64 = match e_ {
& entmod ::Entid ::Entid ( ref e__ ) = > * e__ ,
& entmod ::Entid ::Ident ( ref e__ ) = > * self . schema . require_entid ( & e__ . to_string ( ) ) ? ,
} ;
let a : i64 = match a_ {
& entmod ::Entid ::Entid ( ref a__ ) = > * a__ ,
& entmod ::Entid ::Ident ( ref a__ ) = > * self . schema . require_entid ( & a__ . to_string ( ) ) ? ,
} ;
2017-01-26 00:13:56 +00:00
let attribute : & Attribute = self . schema . require_attribute_for_entid ( & a ) ? ;
2017-02-08 22:04:32 +00:00
if attribute . fulltext {
bail! ( ErrorKind ::NotYetImplemented ( format! ( " Transacting :db/fulltext entities is not yet implemented: {:?} " , entity ) ) )
}
2017-01-26 00:13:56 +00:00
// This is our chance to do schema-aware typechecking: to either assert that the
// given value is in the attribute's value set, or (in limited cases) to coerce
// the value into the attribute's value set.
let typed_value : TypedValue = self . to_typed_value ( v_ , & attribute ) ? ;
2017-02-08 22:04:32 +00:00
let added = true ;
if attribute . multival {
non_fts_many . push ( ( e , a , typed_value , added ) ) ;
} else {
non_fts_one . push ( ( e , a , typed_value , added ) ) ;
}
Ok ( ( ) )
} ,
2017-02-08 23:45:09 +00:00
Entity ::AddOrRetract {
op : OpType ::Retract ,
2017-02-08 22:04:32 +00:00
e : entmod ::EntidOrLookupRef ::Entid ( ref e_ ) ,
a : ref a_ ,
v : entmod ::ValueOrLookupRef ::Value ( ref v_ ) } = > {
let e : i64 = match e_ {
& entmod ::Entid ::Entid ( ref e__ ) = > * e__ ,
& entmod ::Entid ::Ident ( ref e__ ) = > * self . schema . require_entid ( & e__ . to_string ( ) ) ? ,
} ;
let a : i64 = match a_ {
& entmod ::Entid ::Entid ( ref a__ ) = > * a__ ,
& entmod ::Entid ::Ident ( ref a__ ) = > * self . schema . require_entid ( & a__ . to_string ( ) ) ? ,
} ;
let attribute : & Attribute = self . schema . require_attribute_for_entid ( & a ) ? ;
if attribute . fulltext {
bail! ( ErrorKind ::NotYetImplemented ( format! ( " Transacting :db/fulltext entities is not yet implemented: {:?} " , entity ) ) )
}
2017-01-26 00:13:56 +00:00
2017-02-08 22:04:32 +00:00
// This is our chance to do schema-aware typechecking: to either assert that the
// given value is in the attribute's value set, or (in limited cases) to coerce
// the value into the attribute's value set.
let typed_value : TypedValue = self . to_typed_value ( v_ , & attribute ) ? ;
let added = false ;
if attribute . multival {
non_fts_many . push ( ( e , a , typed_value , added ) ) ;
} else {
non_fts_one . push ( ( e , a , typed_value , added ) ) ;
}
2017-01-26 00:13:56 +00:00
Ok ( ( ) )
} ,
2017-02-08 22:04:32 +00:00
_ = > bail! ( ErrorKind ::NotYetImplemented ( format! ( " Transacting this entity is not yet implemented: {:?} " , entity ) ) )
2017-01-26 00:13:56 +00:00
}
} ) . collect ( ) ;
2017-02-08 22:04:32 +00:00
let r : Result < Vec < ( ) > > = r . into_iter ( ) . collect ( ) ;
r ? ;
self . create_temp_tables ( conn ) ? ;
if ! non_fts_one . is_empty ( ) {
self . insert_non_fts_searches ( conn , & non_fts_one [ .. ] , tx , SearchType ::Inexact ) ? ;
}
if ! non_fts_many . is_empty ( ) {
self . insert_non_fts_searches ( conn , & non_fts_many [ .. ] , tx , SearchType ::Exact ) ? ;
}
self . search ( conn ) ? ;
self . insert_transaction ( conn , tx ) ? ;
self . update_datoms ( conn , tx ) ? ;
// TODO: update parts, idents, schema materialized views.
Ok ( ( ) )
2017-01-26 00:13:56 +00:00
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
use bootstrap ;
use debug ;
2017-02-08 22:04:32 +00:00
use edn ;
use edn ::symbols ;
use mentat_tx_parser ;
2017-01-26 00:13:56 +00:00
use rusqlite ;
use types ::* ;
#[ test ]
fn test_open_current_version ( ) {
// TODO: figure out how to reference the fixtures directory for real. For now, assume we're
// executing `cargo test` in `db/`.
let conn = rusqlite ::Connection ::open ( " ../fixtures/v2empty.db " ) . unwrap ( ) ;
let ident_map = read_ident_map ( & conn ) . unwrap ( ) ;
assert_eq! ( ident_map , bootstrap ::bootstrap_ident_map ( ) ) ;
let schema = read_schema ( & conn , & ident_map ) . unwrap ( ) ;
2017-02-08 22:04:32 +00:00
assert_eq! ( schema , bootstrap ::bootstrap_schema ( ) ) ;
2017-01-26 00:13:56 +00:00
let db = read_db ( & conn ) . unwrap ( ) ;
2017-02-08 22:04:32 +00:00
// Does not include :db/txInstant.
let datoms = debug ::datoms_after ( & conn , & db , 0 ) . unwrap ( ) ;
assert_eq! ( datoms . 0. len ( ) , 88 ) ;
2017-01-26 00:13:56 +00:00
2017-02-08 22:04:32 +00:00
// Includes :db/txInstant.
let transactions = debug ::transactions_after ( & conn , & db , 0 ) . unwrap ( ) ;
assert_eq! ( transactions . 0. len ( ) , 1 ) ;
assert_eq! ( transactions . 0 [ 0 ] . 0. len ( ) , 89 ) ;
2017-01-26 00:13:56 +00:00
}
2017-02-08 22:04:32 +00:00
/// Assert that a sequence of transactions meets expectations.
///
/// The transactions, expectations, and optional labels, are given in a simple EDN format; see
/// https://github.com/mozilla/mentat/wiki/Transacting:-EDN-test-format.
///
/// There is some magic here about transaction numbering that I don't want to commit to or
/// document just yet. The end state might be much more general pattern matching syntax, rather
/// than the targeted transaction ID and timestamp replacement we have right now.
fn assert_transactions ( conn : & rusqlite ::Connection , db : & DB , transactions : & Vec < edn ::Value > ) {
for ( index , transaction ) in transactions . into_iter ( ) . enumerate ( ) {
let index = index as i64 ;
let transaction = transaction . as_map ( ) . unwrap ( ) ;
let label : edn ::Value = transaction . get ( & edn ::Value ::NamespacedKeyword ( symbols ::NamespacedKeyword ::new ( " test " , " label " ) ) ) . unwrap ( ) . clone ( ) ;
let assertions : edn ::Value = transaction . get ( & edn ::Value ::NamespacedKeyword ( symbols ::NamespacedKeyword ::new ( " test " , " assertions " ) ) ) . unwrap ( ) . clone ( ) ;
// TODO: use hyphenated keywords, like :test/expected-transaction -- when the EDN parser
// supports them!
let expected_transaction : Option < & edn ::Value > = transaction . get ( & edn ::Value ::NamespacedKeyword ( symbols ::NamespacedKeyword ::new ( " test " , " expectedtransaction " ) ) ) ;
let expected_datoms : Option < & edn ::Value > = transaction . get ( & edn ::Value ::NamespacedKeyword ( symbols ::NamespacedKeyword ::new ( " test " , " expecteddatoms " ) ) ) ;
let entities : Vec < _ > = mentat_tx_parser ::Tx ::parse ( & [ assertions ] [ .. ] ) . unwrap ( ) ;
db . transact_internal ( & conn , & entities [ .. ] , bootstrap ::TX0 + index + 1 ) . unwrap ( ) ;
if let Some ( expected_transaction ) = expected_transaction {
let transactions = debug ::transactions_after ( & conn , & db , bootstrap ::TX0 + index ) . unwrap ( ) ;
assert_eq! ( transactions . 0 [ 0 ] . into_edn ( ) ,
* expected_transaction ,
" \n {} - expected transaction: \n {} \n {} " , label , transactions . 0 [ 0 ] . into_edn ( ) , * expected_transaction ) ;
}
2017-01-26 00:13:56 +00:00
2017-02-08 22:04:32 +00:00
if let Some ( expected_datoms ) = expected_datoms {
let datoms = debug ::datoms_after ( & conn , & db , bootstrap ::TX0 ) . unwrap ( ) ;
assert_eq! ( datoms . into_edn ( ) ,
* expected_datoms ,
" \n {} - expected datoms: \n {} \n {} " , label , datoms . into_edn ( ) , * expected_datoms ) ;
}
2017-01-26 00:13:56 +00:00
2017-02-08 22:04:32 +00:00
// Don't allow empty tests. This will need to change if we allow transacting schema
// fragments in a preamble, but for now it might catch malformed tests.
assert_ne! ( ( expected_transaction , expected_datoms ) , ( None , None ) ,
" Transaction test must include at least one of :test/expectedtransaction or :test/expecteddatoms " ) ;
}
}
#[ test ]
fn test_add ( ) {
2017-02-09 20:23:04 +00:00
let mut conn = new_connection ( " " ) . expect ( " Couldn't open in-memory db " ) ;
2017-02-08 22:04:32 +00:00
assert_eq! ( ensure_current_version ( & mut conn ) . unwrap ( ) , CURRENT_VERSION ) ;
let bootstrap_db = DB ::new ( bootstrap ::bootstrap_partition_map ( ) , bootstrap ::bootstrap_schema ( ) ) ;
// Does not include :db/txInstant.
let datoms = debug ::datoms_after ( & conn , & bootstrap_db , 0 ) . unwrap ( ) ;
assert_eq! ( datoms . 0. len ( ) , 88 ) ;
// Includes :db/txInstant.
let transactions = debug ::transactions_after ( & conn , & bootstrap_db , 0 ) . unwrap ( ) ;
assert_eq! ( transactions . 0. len ( ) , 1 ) ;
assert_eq! ( transactions . 0 [ 0 ] . 0. len ( ) , 89 ) ;
2017-01-26 00:13:56 +00:00
2017-02-08 22:04:32 +00:00
// TODO: extract a test macro simplifying this boilerplate yet further.
let value = edn ::parse ::value ( include_str! ( " ../../tx/fixtures/test_add.edn " ) ) . unwrap ( ) ;
let transactions = value . as_vector ( ) . unwrap ( ) ;
assert_transactions ( & conn , & bootstrap_db , transactions ) ;
}
#[ test ]
fn test_retract ( ) {
2017-02-09 20:23:04 +00:00
let mut conn = new_connection ( " " ) . expect ( " Couldn't open in-memory db " ) ;
2017-01-26 00:13:56 +00:00
assert_eq! ( ensure_current_version ( & mut conn ) . unwrap ( ) , CURRENT_VERSION ) ;
let bootstrap_db = DB ::new ( bootstrap ::bootstrap_partition_map ( ) , bootstrap ::bootstrap_schema ( ) ) ;
2017-02-08 22:04:32 +00:00
// Does not include :db/txInstant.
let datoms = debug ::datoms_after ( & conn , & bootstrap_db , 0 ) . unwrap ( ) ;
assert_eq! ( datoms . 0. len ( ) , 88 ) ;
// Includes :db/txInstant.
let transactions = debug ::transactions_after ( & conn , & bootstrap_db , 0 ) . unwrap ( ) ;
assert_eq! ( transactions . 0. len ( ) , 1 ) ;
assert_eq! ( transactions . 0 [ 0 ] . 0. len ( ) , 89 ) ;
let value = edn ::parse ::value ( include_str! ( " ../../tx/fixtures/test_retract.edn " ) ) . unwrap ( ) ;
let transactions = value . as_vector ( ) . unwrap ( ) ;
assert_transactions ( & conn , & bootstrap_db , transactions ) ;
2017-01-26 00:13:56 +00:00
}
}