working with tests
This commit is contained in:
parent
4f243fbc32
commit
5c2c29bf26
2 changed files with 117 additions and 83 deletions
66
tests/tolstoy.rs
Normal file
66
tests/tolstoy.rs
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -8,29 +8,14 @@
|
||||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||||
// specific language governing permissions and limitations under the License.
|
// specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
// read all txs from the database
|
use std::collections::BTreeMap;
|
||||||
// return a list of structures that represent all we need to know about transactions
|
use std::collections::btree_map::Entry;
|
||||||
|
|
||||||
// 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 rusqlite;
|
use rusqlite;
|
||||||
|
|
||||||
use errors::{
|
use errors::{
|
||||||
Result
|
Result,
|
||||||
|
ErrorKind
|
||||||
};
|
};
|
||||||
|
|
||||||
use mentat_db::types::{
|
use mentat_db::types::{
|
||||||
|
@ -43,103 +28,86 @@ use mentat_db::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
DateTime,
|
|
||||||
Utc,
|
|
||||||
TypedValue
|
TypedValue
|
||||||
};
|
};
|
||||||
|
|
||||||
use edn::FromMicros;
|
#[derive(Debug)]
|
||||||
|
|
||||||
pub struct TxPart {
|
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<TxPart>
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawTx {
|
||||||
e: Entid,
|
e: Entid,
|
||||||
a: i64,
|
a: i64,
|
||||||
v: TypedValue,
|
v: TypedValue,
|
||||||
|
tx: Entid,
|
||||||
added: i32
|
added: i32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Tx {
|
pub trait TxReader {
|
||||||
tx: Entid,
|
fn all(sqlite: &rusqlite::Connection) -> Result<Vec<Tx>>;
|
||||||
tx_instant: DateTime<Utc>,
|
|
||||||
parts: Vec<TxPart>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trait TxReader {
|
pub struct TxClient {}
|
||||||
fn txs(&self) -> Result<Vec<Tx>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TxReader for TxClient {
|
impl TxReader for TxClient {
|
||||||
fn txs(&self) -> Result<Vec<Tx>> {
|
fn all(sqlite: &rusqlite::Connection) -> Result<Vec<Tx>> {
|
||||||
let mut txes_by_tx = HashMap::new();
|
|
||||||
|
|
||||||
// Make sure a=txInstant rows are first, so that we process
|
// Make sure a=txInstant rows are first, so that we process
|
||||||
// all transactions before we process any transaction parts.
|
// all transactions before we process any transaction parts.
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = sqlite.prepare(
|
||||||
"SELECT
|
"SELECT
|
||||||
e, a, v, tx, added, value_type_tag,
|
e, a, v, tx, added, value_type_tag,
|
||||||
CASE a WHEN :txInstant THEN 1 ELSE 0 END is_transaction
|
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 rows: Vec<Result<RawTx>> = stmt.query_and_then_named(&[(":txInstant", &entids::DB_TX_INSTANT)], |row| -> Result<RawTx> {
|
||||||
let e = row.get(0);
|
Ok(RawTx {
|
||||||
let a = row.get(1);
|
e: row.get(0),
|
||||||
let v_instant: i64 = row.get(2); // TODO unify this and typed_value below
|
a: row.get(1),
|
||||||
let tx = row.get(3);
|
v: TypedValue::from_sql_value_pair(row.get(2), row.get(5))?,
|
||||||
let added = row.get(4);
|
tx: row.get(3),
|
||||||
let value_type_tag = row.get(5);
|
added: row.get(4)
|
||||||
|
})
|
||||||
let raw_value: rusqlite::types::Value = row.get(2);
|
})?.collect();
|
||||||
let typed_value = match TypedValue::from_sql_value_pair(raw_value, value_type_tag) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => return Err(e)
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 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.
|
// Row represents a transaction.
|
||||||
if a == entids::DB_TX_INSTANT {
|
if row.a == entids::DB_TX_INSTANT {
|
||||||
txes_by_tx.insert(tx, Tx {
|
txes_by_tx.insert(row.tx, Tx {
|
||||||
tx: tx,
|
tx: row.tx,
|
||||||
// TODO enforce correct type of v and return ErrorKind::BadSQLValuePair
|
tx_instant: row.v,
|
||||||
// otherwise.
|
|
||||||
tx_instant: DateTime::<Utc>::from_micros(v_instant),
|
|
||||||
parts: Vec::new()
|
parts: Vec::new()
|
||||||
});
|
});
|
||||||
Ok(())
|
|
||||||
// Row represents part of a transaction. Our query statement above guarantees
|
// Row represents part of a transaction. Our query statement above guarantees
|
||||||
// that we've already processed corresponding transaction at this point.
|
// that we've already processed corresponding transaction at this point.
|
||||||
} else {
|
} 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 {
|
t.get_mut().parts.push(TxPart {
|
||||||
e: e,
|
e: row.e,
|
||||||
a: a,
|
a: row.a,
|
||||||
v: typed_value,
|
v: row.v,
|
||||||
added: added
|
added: row.added
|
||||||
});
|
});
|
||||||
Ok(())
|
|
||||||
} else {
|
} else {
|
||||||
// TODO not ok... ErrorKind::UnexpectedError
|
bail!(ErrorKind::UnexpectedState(format!("Encountered transaction part before transaction {:?}", row.tx)))
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})?;
|
}
|
||||||
|
|
||||||
Ok(txes_by_tx.into_iter().map(|(_, tx)| tx).collect())
|
Ok(txes_by_tx.into_iter().map(|(_, tx)| tx).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue