use super::*; use crate::mvcc::clock::LocalClock; pub(crate) struct MvccTestDbNoConn { pub(crate) db: Arc, } pub(crate) struct MvccTestDb { pub(crate) mvcc_store: Arc>, pub(crate) _db: Arc, pub(crate) conn: Arc, } impl MvccTestDb { pub fn new() -> Self { let io = Arc::new(MemoryIO::new()); let db = Database::open_file(io.clone(), ":memory:", true, true).unwrap(); let conn = db.connect().unwrap(); let mvcc_store = db.mv_store.as_ref().unwrap().clone(); Self { mvcc_store, _db: db, conn, } } } impl MvccTestDbNoConn { pub fn new() -> Self { let io = Arc::new(MemoryIO::new()); let db = Database::open_file(io.clone(), ":memory:", true, true).unwrap(); Self { db } } } pub(crate) fn generate_simple_string_row(table_id: u64, id: i64, data: &str) -> Row { let record = ImmutableRecord::from_values(&[Value::Text(Text::new(data))], 1); Row { id: RowID { table_id, row_id: id, }, column_count: 1, data: record.as_blob().to_vec(), } } #[test] fn test_insert_read() { let db = MvccTestDb::new(); let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let tx1_row = generate_simple_string_row(1, 1, "Hello"); db.mvcc_store.insert(tx1, tx1_row.clone()).unwrap(); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); db.mvcc_store .commit_tx(tx1, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); let tx2 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let row = db .mvcc_store .read( tx2, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); } #[test] fn test_read_nonexistent() { let db = MvccTestDb::new(); let tx = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let row = db.mvcc_store.read( tx, RowID { table_id: 1, row_id: 1, }, ); assert!(row.unwrap().is_none()); } #[test] fn test_delete() { let db = MvccTestDb::new(); let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let tx1_row = generate_simple_string_row(1, 1, "Hello"); db.mvcc_store.insert(tx1, tx1_row.clone()).unwrap(); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); db.mvcc_store .delete( tx1, RowID { table_id: 1, row_id: 1, }, db.conn.pager.borrow().clone(), ) .unwrap(); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap(); assert!(row.is_none()); db.mvcc_store .commit_tx(tx1, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); let tx2 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let row = db .mvcc_store .read( tx2, RowID { table_id: 1, row_id: 1, }, ) .unwrap(); assert!(row.is_none()); } #[test] fn test_delete_nonexistent() { let db = MvccTestDb::new(); let tx = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); assert!(!db .mvcc_store .delete( tx, RowID { table_id: 1, row_id: 1 }, db.conn.pager.borrow().clone(), ) .unwrap()); } #[test] fn test_commit() { let db = MvccTestDb::new(); let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let tx1_row = generate_simple_string_row(1, 1, "Hello"); db.mvcc_store.insert(tx1, tx1_row.clone()).unwrap(); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); let tx1_updated_row = generate_simple_string_row(1, 1, "World"); db.mvcc_store .update(tx1, tx1_updated_row.clone(), db.conn.pager.borrow().clone()) .unwrap(); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_updated_row, row); db.mvcc_store .commit_tx(tx1, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); let tx2 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let row = db .mvcc_store .read( tx2, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); db.mvcc_store .commit_tx(tx2, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); assert_eq!(tx1_updated_row, row); db.mvcc_store.drop_unused_row_versions(); } #[test] fn test_rollback() { let db = MvccTestDb::new(); let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let row1 = generate_simple_string_row(1, 1, "Hello"); db.mvcc_store.insert(tx1, row1.clone()).unwrap(); let row2 = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(row1, row2); let row3 = generate_simple_string_row(1, 1, "World"); db.mvcc_store .update(tx1, row3.clone(), db.conn.pager.borrow().clone()) .unwrap(); let row4 = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(row3, row4); db.mvcc_store .rollback_tx(tx1, db.conn.pager.borrow().clone()); let tx2 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let row5 = db .mvcc_store .read( tx2, RowID { table_id: 1, row_id: 1, }, ) .unwrap(); assert_eq!(row5, None); } #[test] fn test_dirty_write() { let db = MvccTestDb::new(); // T1 inserts a row with ID 1, but does not commit. let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let tx1_row = generate_simple_string_row(1, 1, "Hello"); db.mvcc_store.insert(tx1, tx1_row.clone()).unwrap(); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); let conn2 = db._db.connect().unwrap(); // T2 attempts to delete row with ID 1, but fails because T1 has not committed. let tx2 = db.mvcc_store.begin_tx(conn2.pager.borrow().clone()); let tx2_row = generate_simple_string_row(1, 1, "World"); assert!(!db .mvcc_store .update(tx2, tx2_row, conn2.pager.borrow().clone()) .unwrap()); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); } #[test] fn test_dirty_read() { let db = MvccTestDb::new(); // T1 inserts a row with ID 1, but does not commit. let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let row1 = generate_simple_string_row(1, 1, "Hello"); db.mvcc_store.insert(tx1, row1).unwrap(); // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. let conn2 = db._db.connect().unwrap(); let tx2 = db.mvcc_store.begin_tx(conn2.pager.borrow().clone()); let row2 = db .mvcc_store .read( tx2, RowID { table_id: 1, row_id: 1, }, ) .unwrap(); assert_eq!(row2, None); } #[test] fn test_dirty_read_deleted() { let db = MvccTestDb::new(); // T1 inserts a row with ID 1 and commits. let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let tx1_row = generate_simple_string_row(1, 1, "Hello"); db.mvcc_store.insert(tx1, tx1_row.clone()).unwrap(); db.mvcc_store .commit_tx(tx1, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); // T2 deletes row with ID 1, but does not commit. let conn2 = db._db.connect().unwrap(); let tx2 = db.mvcc_store.begin_tx(conn2.pager.borrow().clone()); assert!(db .mvcc_store .delete( tx2, RowID { table_id: 1, row_id: 1 }, conn2.pager.borrow().clone(), ) .unwrap()); // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. let conn3 = db._db.connect().unwrap(); let tx3 = db.mvcc_store.begin_tx(conn3.pager.borrow().clone()); let row = db .mvcc_store .read( tx3, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); } #[test] fn test_fuzzy_read() { let db = MvccTestDb::new(); // T1 inserts a row with ID 1 and commits. let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let tx1_row = generate_simple_string_row(1, 1, "First"); db.mvcc_store.insert(tx1, tx1_row.clone()).unwrap(); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); db.mvcc_store .commit_tx(tx1, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); // T2 reads the row with ID 1 within an active transaction. let conn2 = db._db.connect().unwrap(); let tx2 = db.mvcc_store.begin_tx(conn2.pager.borrow().clone()); let row = db .mvcc_store .read( tx2, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); // T3 updates the row and commits. let conn3 = db._db.connect().unwrap(); let tx3 = db.mvcc_store.begin_tx(conn3.pager.borrow().clone()); let tx3_row = generate_simple_string_row(1, 1, "Second"); db.mvcc_store .update(tx3, tx3_row, conn3.pager.borrow().clone()) .unwrap(); db.mvcc_store .commit_tx(tx3, conn3.pager.borrow().clone(), &db.conn) .unwrap(); // T2 still reads the same version of the row as before. let row = db .mvcc_store .read( tx2, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); // T2 tries to update the row, but fails because T3 has already committed an update to the row, // so T2 trying to write would violate snapshot isolation if it succeeded. let tx2_newrow = generate_simple_string_row(1, 1, "Third"); let update_result = db .mvcc_store .update(tx2, tx2_newrow, conn2.pager.borrow().clone()); assert_eq!(Err(DatabaseError::WriteWriteConflict), update_result); } #[test] fn test_lost_update() { let db = MvccTestDb::new(); // T1 inserts a row with ID 1 and commits. let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let tx1_row = generate_simple_string_row(1, 1, "Hello"); db.mvcc_store.insert(tx1, tx1_row.clone()).unwrap(); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); db.mvcc_store .commit_tx(tx1, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); // T2 attempts to update row ID 1 within an active transaction. let conn2 = db._db.connect().unwrap(); let tx2 = db.mvcc_store.begin_tx(conn2.pager.borrow().clone()); let tx2_row = generate_simple_string_row(1, 1, "World"); assert!(db .mvcc_store .update(tx2, tx2_row.clone(), conn2.pager.borrow().clone()) .unwrap()); // T3 also attempts to update row ID 1 within an active transaction. let conn3 = db._db.connect().unwrap(); let tx3 = db.mvcc_store.begin_tx(conn3.pager.borrow().clone()); let tx3_row = generate_simple_string_row(1, 1, "Hello, world!"); assert_eq!( Err(DatabaseError::WriteWriteConflict), db.mvcc_store .update(tx3, tx3_row, conn3.pager.borrow().clone()) ); db.mvcc_store .commit_tx(tx2, conn2.pager.borrow().clone(), &db.conn) .unwrap(); assert_eq!( Err(DatabaseError::TxTerminated), db.mvcc_store .commit_tx(tx3, conn3.pager.borrow().clone(), &db.conn) ); let conn4 = db._db.connect().unwrap(); let tx4 = db.mvcc_store.begin_tx(conn4.pager.borrow().clone()); let row = db .mvcc_store .read( tx4, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx2_row, row); } // Test for the visibility to check if a new transaction can see old committed values. // This test checks for the typo present in the paper, explained in https://github.com/penberg/mvcc-rs/issues/15 #[test] fn test_committed_visibility() { let db = MvccTestDb::new(); // let's add $10 to my account since I like money let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let tx1_row = generate_simple_string_row(1, 1, "10"); db.mvcc_store.insert(tx1, tx1_row.clone()).unwrap(); db.mvcc_store .commit_tx(tx1, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); // but I like more money, so let me try adding $10 more let conn2 = db._db.connect().unwrap(); let tx2 = db.mvcc_store.begin_tx(conn2.pager.borrow().clone()); let tx2_row = generate_simple_string_row(1, 1, "20"); assert!(db .mvcc_store .update(tx2, tx2_row.clone(), conn2.pager.borrow().clone()) .unwrap()); let row = db .mvcc_store .read( tx2, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(row, tx2_row); // can I check how much money I have? let conn3 = db._db.connect().unwrap(); let tx3 = db.mvcc_store.begin_tx(conn3.pager.borrow().clone()); let row = db .mvcc_store .read( tx3, RowID { table_id: 1, row_id: 1, }, ) .unwrap() .unwrap(); assert_eq!(tx1_row, row); } // Test to check if a older transaction can see (un)committed future rows #[test] fn test_future_row() { let db = MvccTestDb::new(); let tx1 = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let conn2 = db._db.connect().unwrap(); let tx2 = db.mvcc_store.begin_tx(conn2.pager.borrow().clone()); let tx2_row = generate_simple_string_row(1, 1, "Hello"); db.mvcc_store.insert(tx2, tx2_row).unwrap(); // transaction in progress, so tx1 shouldn't be able to see the value let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap(); assert_eq!(row, None); // lets commit the transaction and check if tx1 can see it db.mvcc_store .commit_tx(tx2, conn2.pager.borrow().clone(), &db.conn) .unwrap(); let row = db .mvcc_store .read( tx1, RowID { table_id: 1, row_id: 1, }, ) .unwrap(); assert_eq!(row, None); } use crate::mvcc::cursor::MvccLazyCursor; use crate::mvcc::database::{MvStore, Row, RowID}; use crate::types::Text; use crate::Database; use crate::MemoryIO; use crate::RefValue; use crate::Value; // Simple atomic clock implementation for testing fn setup_test_db() -> (MvccTestDb, u64) { let db = MvccTestDb::new(); let tx_id = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let table_id = 1; let test_rows = [ (5, "row5"), (10, "row10"), (15, "row15"), (20, "row20"), (30, "row30"), ]; for (row_id, data) in test_rows.iter() { let id = RowID::new(table_id, *row_id); let record = ImmutableRecord::from_values(&[Value::Text(Text::new(data))], 1); let row = Row::new(id, record.as_blob().to_vec(), 1); db.mvcc_store.insert(tx_id, row).unwrap(); } db.mvcc_store .commit_tx(tx_id, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); let tx_id = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); (db, tx_id) } fn setup_lazy_db(initial_keys: &[i64]) -> (MvccTestDb, u64) { let db = MvccTestDb::new(); let tx_id = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let table_id = 1; for i in initial_keys { let id = RowID::new(table_id, *i); let data = format!("row{i}"); let record = ImmutableRecord::from_values(&[Value::Text(Text::new(&data))], 1); let row = Row::new(id, record.as_blob().to_vec(), 1); db.mvcc_store.insert(tx_id, row).unwrap(); } db.mvcc_store .commit_tx(tx_id, db.conn.pager.borrow().clone(), &db.conn) .unwrap(); let tx_id = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); (db, tx_id) } #[test] fn test_lazy_scan_cursor_basic() { let (db, tx_id) = setup_lazy_db(&[1, 2, 3, 4, 5]); let table_id = 1; let mut cursor = MvccLazyCursor::new( db.mvcc_store.clone(), tx_id, table_id, db.conn.pager.borrow().clone(), ) .unwrap(); // Check first row assert!(cursor.forward()); assert!(!cursor.is_empty()); let row = cursor.current_row().unwrap().unwrap(); assert_eq!(row.id.row_id, 1); // Iterate through all rows let mut count = 1; while cursor.forward() { count += 1; let row = cursor.current_row().unwrap().unwrap(); assert_eq!(row.id.row_id, count); } // Should have found 5 rows assert_eq!(count, 5); // After the last row, is_empty should return true assert!(!cursor.forward()); assert!(cursor.is_empty()); } #[test] fn test_lazy_scan_cursor_with_gaps() { let (db, tx_id) = setup_test_db(); let table_id = 1; let mut cursor = MvccLazyCursor::new( db.mvcc_store.clone(), tx_id, table_id, db.conn.pager.borrow().clone(), ) .unwrap(); // Check first row assert!(cursor.forward()); assert!(!cursor.is_empty()); let row = cursor.current_row().unwrap().unwrap(); assert_eq!(row.id.row_id, 5); // Test moving forward and checking IDs let expected_ids = [5, 10, 15, 20, 30]; let mut index = 0; assert_eq!(cursor.current_row_id().unwrap().row_id, expected_ids[index]); while cursor.forward() { index += 1; if index < expected_ids.len() { assert_eq!(cursor.current_row_id().unwrap().row_id, expected_ids[index]); } } // Should have found all 5 rows assert_eq!(index, expected_ids.len() - 1); } #[test] fn test_cursor_basic() { let (db, tx_id) = setup_lazy_db(&[1, 2, 3, 4, 5]); let table_id = 1; let mut cursor = MvccLazyCursor::new( db.mvcc_store.clone(), tx_id, table_id, db.conn.pager.borrow().clone(), ) .unwrap(); cursor.forward(); // Check first row assert!(!cursor.is_empty()); let row = cursor.current_row().unwrap().unwrap(); assert_eq!(row.id.row_id, 1); // Iterate through all rows let mut count = 1; while cursor.forward() { count += 1; let row = cursor.current_row().unwrap().unwrap(); assert_eq!(row.id.row_id, count); } // Should have found 5 rows assert_eq!(count, 5); // After the last row, is_empty should return true assert!(!cursor.forward()); assert!(cursor.is_empty()); } #[test] fn test_cursor_with_empty_table() { let db = MvccTestDb::new(); { // FIXME: force page 1 initialization let pager = db.conn.pager.borrow().clone(); let tx_id = db.mvcc_store.begin_tx(pager.clone()); db.mvcc_store.commit_tx(tx_id, pager, &db.conn).unwrap(); } let tx_id = db.mvcc_store.begin_tx(db.conn.pager.borrow().clone()); let table_id = 1; // Empty table // Test LazyScanCursor with empty table let mut cursor = MvccLazyCursor::new( db.mvcc_store.clone(), tx_id, table_id, db.conn.pager.borrow().clone(), ) .unwrap(); assert!(cursor.is_empty()); assert!(cursor.current_row_id().is_none()); } #[test] fn test_cursor_modification_during_scan() { let (db, tx_id) = setup_lazy_db(&[1, 2, 4, 5]); let table_id = 1; let mut cursor = MvccLazyCursor::new( db.mvcc_store.clone(), tx_id, table_id, db.conn.pager.borrow().clone(), ) .unwrap(); // Read first row assert!(cursor.forward()); let first_row = cursor.current_row().unwrap().unwrap(); assert_eq!(first_row.id.row_id, 1); // Insert a new row with ID between existing rows let new_row_id = RowID::new(table_id, 3); let new_row = generate_simple_string_row(table_id, new_row_id.row_id, "new_row"); cursor.insert(new_row).unwrap(); let row = db.mvcc_store.read(tx_id, new_row_id).unwrap().unwrap(); let mut record = ImmutableRecord::new(1024); record.start_serialization(&row.data); let value = record.get_value(0).unwrap(); match value { RefValue::Text(text) => { assert_eq!(text.as_str(), "new_row"); } _ => panic!("Expected Text value"), } assert_eq!(row.id.row_id, 3); // Continue scanning - the cursor should still work correctly cursor.forward(); // Move to 4 let row = db .mvcc_store .read(tx_id, RowID::new(table_id, 4)) .unwrap() .unwrap(); assert_eq!(row.id.row_id, 4); cursor.forward(); // Move to 5 (our new row) let row = db .mvcc_store .read(tx_id, RowID::new(table_id, 5)) .unwrap() .unwrap(); assert_eq!(row.id.row_id, 5); assert!(!cursor.forward()); assert!(cursor.is_empty()); } /* States described in the Hekaton paper *for serializability*: Table 1: Case analysis of action to take when version V’s Begin field contains the ID of transaction TB ------------------------------------------------------------------------------------------------------ TB’s state | TB’s end timestamp | Action to take when transaction T checks visibility of version V. ------------------------------------------------------------------------------------------------------ Active | Not set | V is visible only if TB=T and V’s end timestamp equals infinity. ------------------------------------------------------------------------------------------------------ Preparing | TS | V’s begin timestamp will be TS ut V is not yet committed. Use TS | as V’s begin time when testing visibility. If the test is true, | allow T to speculatively read V. Committed TS V’s begin timestamp | will be TS and V is committed. Use TS as V’s begin time to test | visibility. ------------------------------------------------------------------------------------------------------ Committed | TS | V’s begin timestamp will be TS and V is committed. Use TS as V’s | begin time to test visibility. ------------------------------------------------------------------------------------------------------ Aborted | Irrelevant | Ignore V; it’s a garbage version. ------------------------------------------------------------------------------------------------------ Terminated | Irrelevant | Reread V’s Begin field. TB has terminated so it must have finalized or not found | | the timestamp. ------------------------------------------------------------------------------------------------------ Table 2: Case analysis of action to take when V's End field contains a transaction ID TE. ------------------------------------------------------------------------------------------------------ TE’s state | TE’s end timestamp | Action to take when transaction T checks visibility of a version V | | as of read time RT. ------------------------------------------------------------------------------------------------------ Active | Not set | V is visible only if TE is not T. ------------------------------------------------------------------------------------------------------ Preparing | TS | V’s end timestamp will be TS provided that TE commits. If TS > RT, | V is visible to T. If TS < RT, T speculatively ignores V. ------------------------------------------------------------------------------------------------------ Committed | TS | V’s end timestamp will be TS and V is committed. Use TS as V’s end | timestamp when testing visibility. ------------------------------------------------------------------------------------------------------ Aborted | Irrelevant | V is visible. ------------------------------------------------------------------------------------------------------ Terminated | Irrelevant | Reread V’s End field. TE has terminated so it must have finalized or not found | | the timestamp. */ fn new_tx(tx_id: TxID, begin_ts: u64, state: TransactionState) -> RwLock { let state = state.into(); RwLock::new(Transaction { state, tx_id, begin_ts, write_set: SkipSet::new(), read_set: SkipSet::new(), }) } #[test] fn test_snapshot_isolation_tx_visible1() { let txs: SkipMap> = SkipMap::from_iter([ (1, new_tx(1, 1, TransactionState::Committed(2))), (2, new_tx(2, 2, TransactionState::Committed(5))), (3, new_tx(3, 3, TransactionState::Aborted)), (5, new_tx(5, 5, TransactionState::Preparing)), (6, new_tx(6, 6, TransactionState::Committed(10))), (7, new_tx(7, 7, TransactionState::Active)), ]); let current_tx = new_tx(4, 4, TransactionState::Preparing); let current_tx = current_tx.read(); let rv_visible = |begin: TxTimestampOrID, end: Option| { let row_version = RowVersion { begin, end, row: generate_simple_string_row(1, 1, "testme"), }; tracing::debug!("Testing visibility of {row_version:?}"); row_version.is_visible_to(¤t_tx, &txs) }; // begin visible: transaction committed with ts < current_tx.begin_ts // end visible: inf assert!(rv_visible(TxTimestampOrID::TxID(1), None)); // begin invisible: transaction committed with ts > current_tx.begin_ts assert!(!rv_visible(TxTimestampOrID::TxID(2), None)); // begin invisible: transaction aborted assert!(!rv_visible(TxTimestampOrID::TxID(3), None)); // begin visible: timestamp < current_tx.begin_ts // end invisible: transaction committed with ts > current_tx.begin_ts assert!(!rv_visible( TxTimestampOrID::Timestamp(0), Some(TxTimestampOrID::TxID(1)) )); // begin visible: timestamp < current_tx.begin_ts // end visible: transaction committed with ts < current_tx.begin_ts assert!(rv_visible( TxTimestampOrID::Timestamp(0), Some(TxTimestampOrID::TxID(2)) )); // begin visible: timestamp < current_tx.begin_ts // end invisible: transaction aborted assert!(!rv_visible( TxTimestampOrID::Timestamp(0), Some(TxTimestampOrID::TxID(3)) )); // begin invisible: transaction preparing assert!(!rv_visible(TxTimestampOrID::TxID(5), None)); // begin invisible: transaction committed with ts > current_tx.begin_ts assert!(!rv_visible(TxTimestampOrID::TxID(6), None)); // begin invisible: transaction active assert!(!rv_visible(TxTimestampOrID::TxID(7), None)); // begin invisible: transaction committed with ts > current_tx.begin_ts assert!(!rv_visible(TxTimestampOrID::TxID(6), None)); // begin invisible: transaction active assert!(!rv_visible(TxTimestampOrID::TxID(7), None)); // begin visible: timestamp < current_tx.begin_ts // end invisible: transaction preparing assert!(!rv_visible( TxTimestampOrID::Timestamp(0), Some(TxTimestampOrID::TxID(5)) )); // begin invisible: timestamp > current_tx.begin_ts assert!(!rv_visible( TxTimestampOrID::Timestamp(6), Some(TxTimestampOrID::TxID(6)) )); // begin visible: timestamp < current_tx.begin_ts // end visible: some active transaction will eventually overwrite this version, // but that hasn't happened // (this is the https://avi.im/blag/2023/hekaton-paper-typo/ case, I believe!) assert!(rv_visible( TxTimestampOrID::Timestamp(0), Some(TxTimestampOrID::TxID(7)) )); }