From 74a7628a0a10de277228b64b96ef28f075ca5206 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 5 Jun 2023 12:50:35 +0200 Subject: [PATCH] mvcc-rs: move database tests to a separate file That makes the file more human-readable --- .../src/{database.rs => database/mod.rs} | 788 +----------------- core/mvcc/mvcc-rs/src/database/tests.rs | 780 +++++++++++++++++ 2 files changed, 784 insertions(+), 784 deletions(-) rename core/mvcc/mvcc-rs/src/{database.rs => database/mod.rs} (53%) create mode 100644 core/mvcc/mvcc-rs/src/database/tests.rs diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database/mod.rs similarity index 53% rename from core/mvcc/mvcc-rs/src/database.rs rename to core/mvcc/mvcc-rs/src/database/mod.rs index 989519be4..07d532374 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -1,8 +1,8 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; use crate::persistent_storage::Storage; -use parking_lot::Mutex; use crossbeam_skiplist::SkipMap; +use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -11,6 +11,9 @@ use std::sync::{Arc, RwLock}; pub type Result = std::result::Result; +#[cfg(test)] +mod tests; + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)] pub struct RowID { pub table_id: u64, @@ -620,786 +623,3 @@ fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVe None => true, } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::clock::LocalClock; - use tracing_test::traced_test; - - fn test_db() -> Database { - let clock = LocalClock::new(); - let storage = crate::persistent_storage::Storage::new_noop(); - Database::new(clock, storage) - } - - #[traced_test] - #[test] - fn test_insert_read() { - let db = test_db(); - - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - db.commit_tx(tx1).unwrap(); - - let tx2 = db.begin_tx(); - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - } - - #[traced_test] - #[test] - fn test_read_nonexistent() { - let db = test_db(); - let tx = db.begin_tx(); - let row = db.read( - tx, - RowID { - table_id: 1, - row_id: 1, - }, - ); - assert!(row.unwrap().is_none()); - } - - #[traced_test] - #[test] - fn test_delete() { - let db = test_db(); - - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - db.delete( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert!(row.is_none()); - db.commit_tx(tx1).unwrap(); - - let tx2 = db.begin_tx(); - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert!(row.is_none()); - } - - #[traced_test] - #[test] - fn test_delete_nonexistent() { - let db = test_db(); - let tx = db.begin_tx(); - assert!(!db - .delete( - tx, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap()); - } - - #[traced_test] - #[test] - fn test_commit() { - let db = test_db(); - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - let tx1_updated_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - db.update(tx1, tx1_updated_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_updated_row, row); - db.commit_tx(tx1).unwrap(); - - let tx2 = db.begin_tx(); - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - db.commit_tx(tx2).unwrap(); - assert_eq!(tx1_updated_row, row); - db.drop_unused_row_versions(); - } - - #[traced_test] - #[test] - fn test_rollback() { - let db = test_db(); - let tx1 = db.begin_tx(); - let row1 = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, row1.clone()).unwrap(); - let row2 = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(row1, row2); - let row3 = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - db.update(tx1, row3.clone()).unwrap(); - let row4 = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(row3, row4); - db.rollback_tx(tx1); - let tx2 = db.begin_tx(); - let row5 = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert_eq!(row5, None); - } - - #[traced_test] - #[test] - fn test_dirty_write() { - let db = test_db(); - - // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - - // T2 attempts to delete row with ID 1, but fails because T1 has not committed. - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - assert!(!db.update(tx2, tx2_row).unwrap()); - - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - } - - #[traced_test] - #[test] - fn test_dirty_read() { - let db = test_db(); - - // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx(); - let row1 = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, row1).unwrap(); - - // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. - let tx2 = db.begin_tx(); - let row2 = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert_eq!(row2, None); - } - - #[ignore] - #[traced_test] - #[test] - fn test_dirty_read_deleted() { - let db = test_db(); - - // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - db.commit_tx(tx1).unwrap(); - - // T2 deletes row with ID 1, but does not commit. - let tx2 = db.begin_tx(); - assert!(db - .delete( - tx2, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap()); - - // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. - let tx3 = db.begin_tx(); - let row = db - .read( - tx3, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - } - - #[traced_test] - #[test] - fn test_fuzzy_read() { - let db = test_db(); - - // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - db.commit_tx(tx1).unwrap(); - - // T2 reads the row with ID 1 within an active transaction. - let tx2 = db.begin_tx(); - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - - // T3 updates the row and commits. - let tx3 = db.begin_tx(); - let tx3_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - db.update(tx3, tx3_row).unwrap(); - db.commit_tx(tx3).unwrap(); - - // T2 still reads the same version of the row as before. - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - } - - #[traced_test] - #[test] - fn test_lost_update() { - let db = test_db(); - - // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - db.commit_tx(tx1).unwrap(); - - // T2 attempts to update row ID 1 within an active transaction. - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - assert!(db.update(tx2, tx2_row.clone()).unwrap()); - - // T3 also attempts to update row ID 1 within an active transaction. - let tx3 = db.begin_tx(); - let tx3_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello, world!".to_string(), - }; - assert_eq!( - Err(DatabaseError::WriteWriteConflict), - db.update(tx3, tx3_row) - ); - - db.commit_tx(tx2).unwrap(); - assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3)); - - let tx4 = db.begin_tx(); - let row = db - .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 - #[traced_test] - #[test] - fn test_committed_visibility() { - let db = test_db(); - - // let's add $10 to my account since I like money - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "10".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - db.commit_tx(tx1).unwrap(); - - // but I like more money, so let me try adding $10 more - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "20".to_string(), - }; - assert!(db.update(tx2, tx2_row.clone()).unwrap()); - let row = db - .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 tx3 = db.begin_tx(); - let row = db - .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 - #[traced_test] - #[test] - fn test_future_row() { - let db = test_db(); - - let tx1 = db.begin_tx(); - - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "10".to_string(), - }; - db.insert(tx2, tx2_row).unwrap(); - - // transaction in progress, so tx1 shouldn't be able to see the value - let row = db - .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.commit_tx(tx2).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert_eq!(row, None); - } - - #[traced_test] - #[test] - fn test_storage1() { - let clock = LocalClock::new(); - let mut path = std::env::temp_dir(); - path.push(format!( - "mvcc-rs-storage-test-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos(), - )); - let storage = crate::persistent_storage::Storage::new_json_on_disk(path.clone()); - let db = Database::new(clock, storage); - - let tx1 = db.begin_tx(); - let tx2 = db.begin_tx(); - let tx3 = db.begin_tx(); - - db.insert( - tx3, - Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "testme".to_string(), - }, - ) - .unwrap(); - - db.commit_tx(tx1).unwrap(); - db.rollback_tx(tx2); - db.commit_tx(tx3).unwrap(); - - let tx4 = db.begin_tx(); - db.insert( - tx4, - Row { - id: RowID { - table_id: 1, - row_id: 2, - }, - data: "testme2".to_string(), - }, - ) - .unwrap(); - db.insert( - tx4, - Row { - id: RowID { - table_id: 1, - row_id: 3, - }, - data: "testme3".to_string(), - }, - ) - .unwrap(); - - assert_eq!( - db.read( - tx4, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap() - .unwrap() - .data, - "testme" - ); - assert_eq!( - db.read( - tx4, - RowID { - table_id: 1, - row_id: 2 - } - ) - .unwrap() - .unwrap() - .data, - "testme2" - ); - assert_eq!( - db.read( - tx4, - RowID { - table_id: 1, - row_id: 3 - } - ) - .unwrap() - .unwrap() - .data, - "testme3" - ); - db.commit_tx(tx4).unwrap(); - - let clock = LocalClock::new(); - let storage = crate::persistent_storage::Storage::new_json_on_disk(path); - let db = Database::new(clock, storage); - db.recover().unwrap(); - println!("{:#?}", db); - - let tx5 = db.begin_tx(); - println!( - "{:#?}", - db.read( - tx5, - RowID { - table_id: 1, - row_id: 1 - } - ) - ); - assert_eq!( - db.read( - tx5, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap() - .unwrap() - .data, - "testme" - ); - assert_eq!( - db.read( - tx5, - RowID { - table_id: 1, - row_id: 2 - } - ) - .unwrap() - .unwrap() - .data, - "testme2" - ); - assert_eq!( - db.read( - tx5, - RowID { - table_id: 1, - row_id: 3 - } - ) - .unwrap() - .unwrap() - .data, - "testme3" - ); - } -} diff --git a/core/mvcc/mvcc-rs/src/database/tests.rs b/core/mvcc/mvcc-rs/src/database/tests.rs new file mode 100644 index 000000000..adb29856e --- /dev/null +++ b/core/mvcc/mvcc-rs/src/database/tests.rs @@ -0,0 +1,780 @@ + +use super::*; +use crate::clock::LocalClock; +use tracing_test::traced_test; + +fn test_db() -> Database { + let clock = LocalClock::new(); + let storage = crate::persistent_storage::Storage::new_noop(); + Database::new(clock, storage) +} + +#[traced_test] +#[test] +fn test_insert_read() { + let db = test_db(); + + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1).unwrap(); + + let tx2 = db.begin_tx(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); +} + +#[traced_test] +#[test] +fn test_read_nonexistent() { + let db = test_db(); + let tx = db.begin_tx(); + let row = db.read( + tx, + RowID { + table_id: 1, + row_id: 1, + }, + ); + assert!(row.unwrap().is_none()); +} + +#[traced_test] +#[test] +fn test_delete() { + let db = test_db(); + + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + db.delete( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert!(row.is_none()); + db.commit_tx(tx1).unwrap(); + + let tx2 = db.begin_tx(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert!(row.is_none()); +} + +#[traced_test] +#[test] +fn test_delete_nonexistent() { + let db = test_db(); + let tx = db.begin_tx(); + assert!(!db + .delete( + tx, + RowID { + table_id: 1, + row_id: 1 + } + ) + .unwrap()); +} + +#[traced_test] +#[test] +fn test_commit() { + let db = test_db(); + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + let tx1_updated_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + db.update(tx1, tx1_updated_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_updated_row, row); + db.commit_tx(tx1).unwrap(); + + let tx2 = db.begin_tx(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + db.commit_tx(tx2).unwrap(); + assert_eq!(tx1_updated_row, row); + db.drop_unused_row_versions(); +} + +#[traced_test] +#[test] +fn test_rollback() { + let db = test_db(); + let tx1 = db.begin_tx(); + let row1 = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, row1.clone()).unwrap(); + let row2 = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(row1, row2); + let row3 = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + db.update(tx1, row3.clone()).unwrap(); + let row4 = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(row3, row4); + db.rollback_tx(tx1); + let tx2 = db.begin_tx(); + let row5 = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert_eq!(row5, None); +} + +#[traced_test] +#[test] +fn test_dirty_write() { + let db = test_db(); + + // T1 inserts a row with ID 1, but does not commit. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + + // T2 attempts to delete row with ID 1, but fails because T1 has not committed. + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + assert!(!db.update(tx2, tx2_row).unwrap()); + + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); +} + +#[traced_test] +#[test] +fn test_dirty_read() { + let db = test_db(); + + // T1 inserts a row with ID 1, but does not commit. + let tx1 = db.begin_tx(); + let row1 = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, row1).unwrap(); + + // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. + let tx2 = db.begin_tx(); + let row2 = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert_eq!(row2, None); +} + +#[ignore] +#[traced_test] +#[test] +fn test_dirty_read_deleted() { + let db = test_db(); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + db.commit_tx(tx1).unwrap(); + + // T2 deletes row with ID 1, but does not commit. + let tx2 = db.begin_tx(); + assert!(db + .delete( + tx2, + RowID { + table_id: 1, + row_id: 1 + } + ) + .unwrap()); + + // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. + let tx3 = db.begin_tx(); + let row = db + .read( + tx3, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); +} + +#[traced_test] +#[test] +fn test_fuzzy_read() { + let db = test_db(); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1).unwrap(); + + // T2 reads the row with ID 1 within an active transaction. + let tx2 = db.begin_tx(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + + // T3 updates the row and commits. + let tx3 = db.begin_tx(); + let tx3_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + db.update(tx3, tx3_row).unwrap(); + db.commit_tx(tx3).unwrap(); + + // T2 still reads the same version of the row as before. + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); +} + +#[traced_test] +#[test] +fn test_lost_update() { + let db = test_db(); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1).unwrap(); + + // T2 attempts to update row ID 1 within an active transaction. + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + assert!(db.update(tx2, tx2_row.clone()).unwrap()); + + // T3 also attempts to update row ID 1 within an active transaction. + let tx3 = db.begin_tx(); + let tx3_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello, world!".to_string(), + }; + assert_eq!( + Err(DatabaseError::WriteWriteConflict), + db.update(tx3, tx3_row) + ); + + db.commit_tx(tx2).unwrap(); + assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3)); + + let tx4 = db.begin_tx(); + let row = db + .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 +#[traced_test] +#[test] +fn test_committed_visibility() { + let db = test_db(); + + // let's add $10 to my account since I like money + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "10".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + db.commit_tx(tx1).unwrap(); + + // but I like more money, so let me try adding $10 more + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "20".to_string(), + }; + assert!(db.update(tx2, tx2_row.clone()).unwrap()); + let row = db + .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 tx3 = db.begin_tx(); + let row = db + .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 +#[traced_test] +#[test] +fn test_future_row() { + let db = test_db(); + + let tx1 = db.begin_tx(); + + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "10".to_string(), + }; + db.insert(tx2, tx2_row).unwrap(); + + // transaction in progress, so tx1 shouldn't be able to see the value + let row = db + .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.commit_tx(tx2).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert_eq!(row, None); +} + +#[traced_test] +#[test] +fn test_storage1() { + let clock = LocalClock::new(); + let mut path = std::env::temp_dir(); + path.push(format!( + "mvcc-rs-storage-test-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(), + )); + let storage = crate::persistent_storage::Storage::new_json_on_disk(path.clone()); + let db = Database::new(clock, storage); + + let tx1 = db.begin_tx(); + let tx2 = db.begin_tx(); + let tx3 = db.begin_tx(); + + db.insert( + tx3, + Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "testme".to_string(), + }, + ) + .unwrap(); + + db.commit_tx(tx1).unwrap(); + db.rollback_tx(tx2); + db.commit_tx(tx3).unwrap(); + + let tx4 = db.begin_tx(); + db.insert( + tx4, + Row { + id: RowID { + table_id: 1, + row_id: 2, + }, + data: "testme2".to_string(), + }, + ) + .unwrap(); + db.insert( + tx4, + Row { + id: RowID { + table_id: 1, + row_id: 3, + }, + data: "testme3".to_string(), + }, + ) + .unwrap(); + + assert_eq!( + db.read( + tx4, + RowID { + table_id: 1, + row_id: 1 + } + ) + .unwrap() + .unwrap() + .data, + "testme" + ); + assert_eq!( + db.read( + tx4, + RowID { + table_id: 1, + row_id: 2 + } + ) + .unwrap() + .unwrap() + .data, + "testme2" + ); + assert_eq!( + db.read( + tx4, + RowID { + table_id: 1, + row_id: 3 + } + ) + .unwrap() + .unwrap() + .data, + "testme3" + ); + db.commit_tx(tx4).unwrap(); + + let clock = LocalClock::new(); + let storage = crate::persistent_storage::Storage::new_json_on_disk(path); + let db = Database::new(clock, storage); + db.recover().unwrap(); + println!("{:#?}", db); + + let tx5 = db.begin_tx(); + println!( + "{:#?}", + db.read( + tx5, + RowID { + table_id: 1, + row_id: 1 + } + ) + ); + assert_eq!( + db.read( + tx5, + RowID { + table_id: 1, + row_id: 1 + } + ) + .unwrap() + .unwrap() + .data, + "testme" + ); + assert_eq!( + db.read( + tx5, + RowID { + table_id: 1, + row_id: 2 + } + ) + .unwrap() + .unwrap() + .data, + "testme2" + ); + assert_eq!( + db.read( + tx5, + RowID { + table_id: 1, + row_id: 3 + } + ) + .unwrap() + .unwrap() + .data, + "testme3" + ); +}