From 5c2c29bf26cdc1fb083daebdc41a61458e5ddf01 Mon Sep 17 00:00:00 2001 From: Grisha Kruglov Date: Thu, 1 Feb 2018 14:31:27 -0500 Subject: [PATCH] working with tests --- tests/tolstoy.rs | 66 +++++++++++++++++++ tolstoy/src/tx_client.rs | 134 +++++++++++++++------------------------ 2 files changed, 117 insertions(+), 83 deletions(-) create mode 100644 tests/tolstoy.rs diff --git a/tests/tolstoy.rs b/tests/tolstoy.rs new file mode 100644 index 00000000..f73c3621 --- /dev/null +++ b/tests/tolstoy.rs @@ -0,0 +1,66 @@ +extern crate mentat; +extern crate mentat_core; +extern crate mentat_tolstoy; + +use mentat::conn::Conn; + +use mentat::new_connection; +use mentat_tolstoy::tx_client::{ + Tx, + TxReader, + TxClient +}; +use mentat_core::{ + ValueType, + TypedValue +}; + +#[test] +fn test_reader() { + let mut c = new_connection("").expect("Couldn't open conn."); + let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); + + let txes = TxClient::all(&c).expect("bootstrap transactions"); + + // Don't inspect the bootstrap, but we'd like to see it's there. + assert_eq!(1, txes.len()); + assert_eq!(76, txes[0].parts.len()); + + let ids = conn.transact(&mut c, r#"[ + [:db/add "s" :db/ident :foo/numba] + [:db/add "s" :db/valueType :db.type/long] + [:db/add "s" :db/cardinality :db.cardinality/one] + ]"#).expect("successful transaction").tempids; + let numba_entity_id = ids.get("s").unwrap(); + + let txes = TxClient::all(&c).expect("got transactions"); + + // Expect to see one more transaction of three parts. + assert_eq!(2, txes.len()); + assert_eq!(3, txes[1].parts.len()); + + println!("{:?}", txes[1]); + + let ids = conn.transact(&mut c, r#"[ + [:db/add "b" :foo/numba 123] + ]"#).expect("successful transaction").tempids; + let asserted_e = ids.get("b").unwrap(); + + let txes = TxClient::all(&c).expect("got transactions"); + + // Expect to see a single part transactions + // TODO verify that tx itself looks sane + assert_eq!(3, txes.len()); + assert_eq!(1, txes[2].parts.len()); + + // Inspect the transaction part. + let part = &txes[2].parts[0]; + + assert_eq!(asserted_e, &part.e); + assert_eq!(numba_entity_id, &part.a); + assert!(part.v.matches_type(ValueType::Long)); + assert_eq!(TypedValue::Long(123), part.v); + assert_eq!(1, part.added); + + // TODO retractions +} diff --git a/tolstoy/src/tx_client.rs b/tolstoy/src/tx_client.rs index 8374e9f7..aa91871d 100644 --- a/tolstoy/src/tx_client.rs +++ b/tolstoy/src/tx_client.rs @@ -8,29 +8,14 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. -// read all txs from the database -// return a list of structures that represent all we need to know about transactions - -// so, what do we need here then? -// we need a "way in"! - -// could just query the transactions database directly, read stuff in, and represent -// it as some data structure on the way out - -// will need to weed out transactions as we work through the records -// -> and then associate with it the "chunks" - -// "transaction" then is a meta-concept, it's a label for a collection of concrete changes - -// perhaps mentat has useful primitives, but let's begin by just "doing the work" - -use std::collections::HashMap; -use std::collections::hash_map::Entry; +use std::collections::BTreeMap; +use std::collections::btree_map::Entry; use rusqlite; use errors::{ - Result + Result, + ErrorKind }; use mentat_db::types::{ @@ -43,103 +28,86 @@ use mentat_db::{ }; use mentat_core::{ - DateTime, - Utc, TypedValue }; -use edn::FromMicros; - +#[derive(Debug)] pub struct TxPart { + pub e: Entid, + pub a: i64, + pub v: TypedValue, + pub added: i32 +} + +#[derive(Debug)] +pub struct Tx { + pub tx: Entid, + pub tx_instant: TypedValue, + pub parts: Vec +} + +struct RawTx { e: Entid, a: i64, v: TypedValue, + tx: Entid, added: i32 } -pub struct Tx { - tx: Entid, - tx_instant: DateTime, - parts: Vec +pub trait TxReader { + fn all(sqlite: &rusqlite::Connection) -> Result>; } -trait TxReader { - fn txs(&self) -> Result>; -} - -struct TxClient { - conn: rusqlite::Connection -} - -// TODO This needs to take a mentat connection, as we're making assumptions about -// what that connection will provide (a transactions table). -impl TxClient { - fn new(conn: rusqlite::Connection) -> Self { - TxClient { - conn: conn - } - } -} +pub struct TxClient {} impl TxReader for TxClient { - fn txs(&self) -> Result> { - let mut txes_by_tx = HashMap::new(); - + fn all(sqlite: &rusqlite::Connection) -> Result> { // Make sure a=txInstant rows are first, so that we process // all transactions before we process any transaction parts. - let mut stmt = self.conn.prepare( + let mut stmt = sqlite.prepare( "SELECT e, a, v, tx, added, value_type_tag, CASE a WHEN :txInstant THEN 1 ELSE 0 END is_transaction - FROM transactions ORDER BY is_transaction DESC" + FROM transactions ORDER BY is_transaction DESC, tx ASC" )?; - let _ = stmt.query_and_then_named(&[(":txInstant", &entids::DB_TX_INSTANT)], |row| { - let e = row.get(0); - let a = row.get(1); - let v_instant: i64 = row.get(2); // TODO unify this and typed_value below - let tx = row.get(3); - let added = row.get(4); - let value_type_tag = row.get(5); - - let raw_value: rusqlite::types::Value = row.get(2); - let typed_value = match TypedValue::from_sql_value_pair(raw_value, value_type_tag) { - Ok(v) => v, - Err(e) => return Err(e) - }; + let rows: Vec> = stmt.query_and_then_named(&[(":txInstant", &entids::DB_TX_INSTANT)], |row| -> Result { + Ok(RawTx { + e: row.get(0), + a: row.get(1), + v: TypedValue::from_sql_value_pair(row.get(2), row.get(5))?, + tx: row.get(3), + added: row.get(4) + }) + })?.collect(); + // It's convenient to have a consistently ordered set of results, + // so we use a sorting map. + let mut txes_by_tx = BTreeMap::new(); + for row_result in rows { + let row = row_result?; // Row represents a transaction. - if a == entids::DB_TX_INSTANT { - txes_by_tx.insert(tx, Tx { - tx: tx, - // TODO enforce correct type of v and return ErrorKind::BadSQLValuePair - // otherwise. - tx_instant: DateTime::::from_micros(v_instant), + if row.a == entids::DB_TX_INSTANT { + txes_by_tx.insert(row.tx, Tx { + tx: row.tx, + tx_instant: row.v, parts: Vec::new() }); - Ok(()) // Row represents part of a transaction. Our query statement above guarantees // that we've already processed corresponding transaction at this point. } else { - if let Entry::Occupied(mut t) = txes_by_tx.entry(tx) { + if let Entry::Occupied(mut t) = txes_by_tx.entry(row.tx) { t.get_mut().parts.push(TxPart { - e: e, - a: a, - v: typed_value, - added: added + e: row.e, + a: row.a, + v: row.v, + added: row.added }); - Ok(()) } else { - // TODO not ok... ErrorKind::UnexpectedError - Ok(()) + bail!(ErrorKind::UnexpectedState(format!("Encountered transaction part before transaction {:?}", row.tx))) } } - })?; + } Ok(txes_by_tx.into_iter().map(|(_, tx)| tx).collect()) } } - -#[cfg(test)] -mod tests { - use super::*; -}