From 75eaf7c1262e94e04242daf3fb0e8d79627f5d67 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Wed, 12 Jun 2024 23:24:05 +0200 Subject: [PATCH] storage: use goldenscripts for MVCC tests --- src/raft/log.rs | 1 + src/storage/engine.rs | 4 +- src/storage/golden/mvcc/anomaly_dirty_read | 22 - src/storage/golden/mvcc/anomaly_dirty_write | 22 - src/storage/golden/mvcc/anomaly_fuzzy_read | 31 - src/storage/golden/mvcc/anomaly_lost_update | 33 - src/storage/golden/mvcc/anomaly_phantom_read | 45 - src/storage/golden/mvcc/anomaly_read_skew | 39 - src/storage/golden/mvcc/anomaly_write_skew | 41 - src/storage/golden/mvcc/begin | 30 - src/storage/golden/mvcc/begin_as_of | 89 - src/storage/golden/mvcc/begin_read_only | 15 - src/storage/golden/mvcc/delete | 42 - src/storage/golden/mvcc/delete_conflict | 54 - src/storage/golden/mvcc/get | 27 - src/storage/golden/mvcc/get_isolation | 97 -- src/storage/golden/mvcc/resume | 110 -- src/storage/golden/mvcc/rollback | 94 -- src/storage/golden/mvcc/scan | 109 -- src/storage/golden/mvcc/scan_isolation | 91 -- .../golden/mvcc/scan_key_version_encoding | 53 - src/storage/golden/mvcc/scan_prefix | 117 -- src/storage/golden/mvcc/set | 42 - src/storage/golden/mvcc/set_conflict | 54 - src/storage/golden/mvcc/unversioned | 59 - src/storage/mvcc.rs | 1434 ++++------------- src/storage/testscripts/engine/keys | 61 - .../testscripts/mvcc/anomaly_dirty_read | 12 + .../testscripts/mvcc/anomaly_dirty_write | 12 + .../testscripts/mvcc/anomaly_fuzzy_read | 25 + .../testscripts/mvcc/anomaly_lost_update | 17 + .../testscripts/mvcc/anomaly_phantom_read | 31 + .../testscripts/mvcc/anomaly_read_skew | 26 + .../testscripts/mvcc/anomaly_write_skew | 36 + src/storage/testscripts/mvcc/begin | 49 + src/storage/testscripts/mvcc/begin_as_of | 108 ++ src/storage/testscripts/mvcc/begin_readonly | 40 + src/storage/testscripts/mvcc/delete | 62 + src/storage/testscripts/mvcc/delete_conflict | 39 + src/storage/testscripts/mvcc/get | 21 + src/storage/testscripts/mvcc/get_isolation | 46 + src/storage/testscripts/mvcc/resume | 89 + src/storage/testscripts/mvcc/rollback | 95 ++ src/storage/testscripts/mvcc/scan | 101 ++ src/storage/testscripts/mvcc/scan_isolation | 43 + .../mvcc/scan_key_version_encoding | 37 + src/storage/testscripts/mvcc/scan_prefix | 104 ++ src/storage/testscripts/mvcc/set | 51 + src/storage/testscripts/mvcc/set_conflict | 39 + src/storage/testscripts/mvcc/unversioned | 55 + 50 files changed, 1481 insertions(+), 2473 deletions(-) delete mode 100644 src/storage/golden/mvcc/anomaly_dirty_read delete mode 100644 src/storage/golden/mvcc/anomaly_dirty_write delete mode 100644 src/storage/golden/mvcc/anomaly_fuzzy_read delete mode 100644 src/storage/golden/mvcc/anomaly_lost_update delete mode 100644 src/storage/golden/mvcc/anomaly_phantom_read delete mode 100644 src/storage/golden/mvcc/anomaly_read_skew delete mode 100644 src/storage/golden/mvcc/anomaly_write_skew delete mode 100644 src/storage/golden/mvcc/begin delete mode 100644 src/storage/golden/mvcc/begin_as_of delete mode 100644 src/storage/golden/mvcc/begin_read_only delete mode 100644 src/storage/golden/mvcc/delete delete mode 100644 src/storage/golden/mvcc/delete_conflict delete mode 100644 src/storage/golden/mvcc/get delete mode 100644 src/storage/golden/mvcc/get_isolation delete mode 100644 src/storage/golden/mvcc/resume delete mode 100644 src/storage/golden/mvcc/rollback delete mode 100644 src/storage/golden/mvcc/scan delete mode 100644 src/storage/golden/mvcc/scan_isolation delete mode 100644 src/storage/golden/mvcc/scan_key_version_encoding delete mode 100644 src/storage/golden/mvcc/scan_prefix delete mode 100644 src/storage/golden/mvcc/set delete mode 100644 src/storage/golden/mvcc/set_conflict delete mode 100644 src/storage/golden/mvcc/unversioned create mode 100644 src/storage/testscripts/mvcc/anomaly_dirty_read create mode 100644 src/storage/testscripts/mvcc/anomaly_dirty_write create mode 100644 src/storage/testscripts/mvcc/anomaly_fuzzy_read create mode 100644 src/storage/testscripts/mvcc/anomaly_lost_update create mode 100644 src/storage/testscripts/mvcc/anomaly_phantom_read create mode 100644 src/storage/testscripts/mvcc/anomaly_read_skew create mode 100644 src/storage/testscripts/mvcc/anomaly_write_skew create mode 100644 src/storage/testscripts/mvcc/begin create mode 100644 src/storage/testscripts/mvcc/begin_as_of create mode 100644 src/storage/testscripts/mvcc/begin_readonly create mode 100644 src/storage/testscripts/mvcc/delete create mode 100644 src/storage/testscripts/mvcc/delete_conflict create mode 100644 src/storage/testscripts/mvcc/get create mode 100644 src/storage/testscripts/mvcc/get_isolation create mode 100644 src/storage/testscripts/mvcc/resume create mode 100644 src/storage/testscripts/mvcc/rollback create mode 100644 src/storage/testscripts/mvcc/scan create mode 100644 src/storage/testscripts/mvcc/scan_isolation create mode 100644 src/storage/testscripts/mvcc/scan_key_version_encoding create mode 100644 src/storage/testscripts/mvcc/scan_prefix create mode 100644 src/storage/testscripts/mvcc/set create mode 100644 src/storage/testscripts/mvcc/set_conflict create mode 100644 src/storage/testscripts/mvcc/unversioned diff --git a/src/raft/log.rs b/src/raft/log.rs index 2afa56f08..9b5e5e3b8 100644 --- a/src/raft/log.rs +++ b/src/raft/log.rs @@ -365,6 +365,7 @@ mod tests { impl goldenscript::Runner for TestRunner { fn run(&mut self, command: &goldenscript::Command) -> Result> { let mut output = String::new(); + // TODO: use [ops] tag instead of oplog= parameters. See MVCC runner. match command.name.as_str() { // append [COMMAND] [oplog=BOOL] "append" => { diff --git a/src/storage/engine.rs b/src/storage/engine.rs index 529e0c45d..0adf76e4c 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -207,7 +207,7 @@ pub mod test { format!( "{} → {}", Self::format_bytes(key), - value.map(|v| Self::format_bytes(v)).unwrap_or("None".to_string()) + value.map(Self::format_bytes).unwrap_or("None".to_string()) ) } @@ -300,7 +300,7 @@ pub mod test { /// panicking if they produce different results. Engine implementations /// should not have any observable differences in behavior. /// - /// TODO: use this in more tests, e.g. MVCC tests. + /// TODO: use this in more tests, e.g. SQL tests. pub struct Mirror { pub a: A, pub b: B, diff --git a/src/storage/golden/mvcc/anomaly_dirty_read b/src/storage/golden/mvcc/anomaly_dirty_read deleted file mode 100644 index 56643fa4a..000000000 --- a/src/storage/golden/mvcc/anomaly_dirty_read +++ /dev/null @@ -1,22 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T1: set "key" = 0x01 - set TxnWrite(1, "key") = [] - set Version("key", 1) = 0x01 - -T2: begin → v2 read-write active={1} - set NextVersion = 3 - set TxnActiveSnapshot(2) = {1} - set TxnActive(2) = [] - -T2: get "key" → None - -Engine state: -NextVersion = 3 -TxnActive(1) = [] -TxnActive(2) = [] -TxnActiveSnapshot(2) = {1} -TxnWrite(1, "key") = [] -Version("key", 1) = 0x01 diff --git a/src/storage/golden/mvcc/anomaly_dirty_write b/src/storage/golden/mvcc/anomaly_dirty_write deleted file mode 100644 index 9f1a65d22..000000000 --- a/src/storage/golden/mvcc/anomaly_dirty_write +++ /dev/null @@ -1,22 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T1: set "key" = 0x01 - set TxnWrite(1, "key") = [] - set Version("key", 1) = 0x01 - -T2: begin → v2 read-write active={1} - set NextVersion = 3 - set TxnActiveSnapshot(2) = {1} - set TxnActive(2) = [] - -T2: set "key" = 0x02 → Error::Serialization - -Engine state: -NextVersion = 3 -TxnActive(1) = [] -TxnActive(2) = [] -TxnActiveSnapshot(2) = {1} -TxnWrite(1, "key") = [] -Version("key", 1) = 0x01 diff --git a/src/storage/golden/mvcc/anomaly_fuzzy_read b/src/storage/golden/mvcc/anomaly_fuzzy_read deleted file mode 100644 index c56df2f03..000000000 --- a/src/storage/golden/mvcc/anomaly_fuzzy_read +++ /dev/null @@ -1,31 +0,0 @@ -Engine state: -NextVersion = 2 -Version("key", 1) = 0x00 - -T1: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T2: begin → v3 read-write active={2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {2} - set TxnActive(3) = [] - -T2: get "key" → 0x00 - -T1: set "key" = "t1" - set TxnWrite(2, "key") = [] - set Version("key", 2) = "t1" - -T1: commit - del TxnWrite(2, "key") - del TxnActive(2) - -T2: get "key" → 0x00 - -Engine state: -NextVersion = 4 -TxnActive(3) = [] -TxnActiveSnapshot(3) = {2} -Version("key", 1) = 0x00 -Version("key", 2) = "t1" diff --git a/src/storage/golden/mvcc/anomaly_lost_update b/src/storage/golden/mvcc/anomaly_lost_update deleted file mode 100644 index 2e7baf4cb..000000000 --- a/src/storage/golden/mvcc/anomaly_lost_update +++ /dev/null @@ -1,33 +0,0 @@ -Engine state: -NextVersion = 2 -Version("key", 1) = 0x00 - -T1: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T2: begin → v3 read-write active={2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {2} - set TxnActive(3) = [] - -T1: get "key" → 0x00 - -T2: get "key" → 0x00 - -T1: set "key" = 0x01 - set TxnWrite(2, "key") = [] - set Version("key", 2) = 0x01 - -T2: set "key" = 0x02 → Error::Serialization - -T1: commit - del TxnWrite(2, "key") - del TxnActive(2) - -Engine state: -NextVersion = 4 -TxnActive(3) = [] -TxnActiveSnapshot(3) = {2} -Version("key", 1) = 0x00 -Version("key", 2) = 0x01 diff --git a/src/storage/golden/mvcc/anomaly_phantom_read b/src/storage/golden/mvcc/anomaly_phantom_read deleted file mode 100644 index 598662c2e..000000000 --- a/src/storage/golden/mvcc/anomaly_phantom_read +++ /dev/null @@ -1,45 +0,0 @@ -Engine state: -NextVersion = 2 -Version("a", 1) = 0x00 -Version("ba", 1) = 0x00 -Version("bb", 1) = 0x00 - -T1: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T2: begin → v3 read-write active={2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {2} - set TxnActive(3) = [] - -T1: scan prefix "b" - "ba" = 0x00 - "bb" = 0x00 - -T2: del "ba" - set TxnWrite(3, "ba") = [] - set Version("ba", 3) = None - -T2: set "bc" = 0x02 - set TxnWrite(3, "bc") = [] - set Version("bc", 3) = 0x02 - -T2: commit - del TxnWrite(3, "ba") - del TxnWrite(3, "bc") - del TxnActive(3) - -T1: scan prefix "b" - "ba" = 0x00 - "bb" = 0x00 - -Engine state: -NextVersion = 4 -TxnActive(2) = [] -TxnActiveSnapshot(3) = {2} -Version("a", 1) = 0x00 -Version("ba", 1) = 0x00 -Version("ba", 3) = None -Version("bb", 1) = 0x00 -Version("bc", 3) = 0x02 diff --git a/src/storage/golden/mvcc/anomaly_read_skew b/src/storage/golden/mvcc/anomaly_read_skew deleted file mode 100644 index 7d7ddddf0..000000000 --- a/src/storage/golden/mvcc/anomaly_read_skew +++ /dev/null @@ -1,39 +0,0 @@ -Engine state: -NextVersion = 2 -Version("a", 1) = 0x00 -Version("b", 1) = 0x00 - -T1: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T2: begin → v3 read-write active={2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {2} - set TxnActive(3) = [] - -T1: get "a" → 0x00 - -T2: set "a" = 0x02 - set TxnWrite(3, "a") = [] - set Version("a", 3) = 0x02 - -T2: set "b" = 0x02 - set TxnWrite(3, "b") = [] - set Version("b", 3) = 0x02 - -T2: commit - del TxnWrite(3, "a") - del TxnWrite(3, "b") - del TxnActive(3) - -T1: get "b" → 0x00 - -Engine state: -NextVersion = 4 -TxnActive(2) = [] -TxnActiveSnapshot(3) = {2} -Version("a", 1) = 0x00 -Version("a", 3) = 0x02 -Version("b", 1) = 0x00 -Version("b", 3) = 0x02 diff --git a/src/storage/golden/mvcc/anomaly_write_skew b/src/storage/golden/mvcc/anomaly_write_skew deleted file mode 100644 index e232191d9..000000000 --- a/src/storage/golden/mvcc/anomaly_write_skew +++ /dev/null @@ -1,41 +0,0 @@ -Engine state: -NextVersion = 2 -Version("a", 1) = 0x01 -Version("b", 1) = 0x02 - -T1: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T2: begin → v3 read-write active={2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {2} - set TxnActive(3) = [] - -T1: get "a" → 0x01 - -T2: get "b" → 0x02 - -T1: set "b" = 0x01 - set TxnWrite(2, "b") = [] - set Version("b", 2) = 0x01 - -T2: set "a" = 0x02 - set TxnWrite(3, "a") = [] - set Version("a", 3) = 0x02 - -T1: commit - del TxnWrite(2, "b") - del TxnActive(2) - -T2: commit - del TxnWrite(3, "a") - del TxnActive(3) - -Engine state: -NextVersion = 4 -TxnActiveSnapshot(3) = {2} -Version("a", 1) = 0x01 -Version("a", 3) = 0x02 -Version("b", 1) = 0x02 -Version("b", 2) = 0x01 diff --git a/src/storage/golden/mvcc/begin b/src/storage/golden/mvcc/begin deleted file mode 100644 index 2911623a1..000000000 --- a/src/storage/golden/mvcc/begin +++ /dev/null @@ -1,30 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T2: begin → v2 read-write active={1} - set NextVersion = 3 - set TxnActiveSnapshot(2) = {1} - set TxnActive(2) = [] - -T3: begin → v3 read-write active={1,2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {1,2} - set TxnActive(3) = [] - -T2: commit - del TxnActive(2) - -T4: begin → v4 read-write active={1,3} - set NextVersion = 5 - set TxnActiveSnapshot(4) = {1,3} - set TxnActive(4) = [] - -Engine state: -NextVersion = 5 -TxnActive(1) = [] -TxnActive(3) = [] -TxnActive(4) = [] -TxnActiveSnapshot(2) = {1} -TxnActiveSnapshot(3) = {1,2} -TxnActiveSnapshot(4) = {1,3} diff --git a/src/storage/golden/mvcc/begin_as_of b/src/storage/golden/mvcc/begin_as_of deleted file mode 100644 index b795842be..000000000 --- a/src/storage/golden/mvcc/begin_as_of +++ /dev/null @@ -1,89 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T1: set "other" = 0x01 - set TxnWrite(1, "other") = [] - set Version("other", 1) = 0x01 - -T2: begin → v2 read-write active={1} - set NextVersion = 3 - set TxnActiveSnapshot(2) = {1} - set TxnActive(2) = [] - -T2: set "key" = 0x02 - set TxnWrite(2, "key") = [] - set Version("key", 2) = 0x02 - -T2: commit - del TxnWrite(2, "key") - del TxnActive(2) - -T3: begin → v3 read-write active={1} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {1} - set TxnActive(3) = [] - -T3: set "key" = 0x03 - set TxnWrite(3, "key") = [] - set Version("key", 3) = 0x03 - -T4: begin as of 3 → v3 read-only active={1} - -T4: scan .. - "key" = 0x02 - -T4: set "foo" = 0x01 → Error::ReadOnly - -T4: del "foo" → Error::ReadOnly - -T1: commit - del TxnWrite(1, "other") - del TxnActive(1) - -T3: commit - del TxnWrite(3, "key") - del TxnActive(3) - -T4: scan .. - "key" = 0x02 - -T5: begin as of 3 → v3 read-only active={1} - -T5: scan .. - "key" = 0x02 - -T4: rollback - -T5: commit - -T6: begin → v4 read-write active={} - set NextVersion = 5 - set TxnActive(4) = [] - -T6: set "key" = 0x04 - set TxnWrite(4, "key") = [] - set Version("key", 4) = 0x04 - -T6: commit - del TxnWrite(4, "key") - del TxnActive(4) - -T7: begin as of 4 → v4 read-only active={} - -T7: scan .. - "key" = 0x03 - "other" = 0x01 - -T8: begin as of 5 → Error::InvalidInput("version 5 does not exist") - -T9: begin as of 9 → Error::InvalidInput("version 9 does not exist") - -Engine state: -NextVersion = 5 -TxnActiveSnapshot(2) = {1} -TxnActiveSnapshot(3) = {1} -Version("key", 2) = 0x02 -Version("key", 3) = 0x03 -Version("key", 4) = 0x04 -Version("other", 1) = 0x01 diff --git a/src/storage/golden/mvcc/begin_read_only b/src/storage/golden/mvcc/begin_read_only deleted file mode 100644 index 7fa039a5e..000000000 --- a/src/storage/golden/mvcc/begin_read_only +++ /dev/null @@ -1,15 +0,0 @@ -T1: begin read-only → v1 read-only active={} - -T1: set "foo" = 0x01 → Error::ReadOnly - -T1: del "foo" → Error::ReadOnly - -T2: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T3: begin read-only → v2 read-only active={1} - -Engine state: -NextVersion = 2 -TxnActive(1) = [] diff --git a/src/storage/golden/mvcc/delete b/src/storage/golden/mvcc/delete deleted file mode 100644 index c435fb9c0..000000000 --- a/src/storage/golden/mvcc/delete +++ /dev/null @@ -1,42 +0,0 @@ -Engine state: -NextVersion = 2 -Version("key", 1) = 0x01 -Version("tombstone", 1) = None - -T1: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T1: set "key" = 0x02 - set TxnWrite(2, "key") = [] - set Version("key", 2) = 0x02 - -T1: del "key" - set TxnWrite(2, "key") = [] - set Version("key", 2) = None - -T1: del "key" - set TxnWrite(2, "key") = [] - set Version("key", 2) = None - -T1: del "tombstone" - set TxnWrite(2, "tombstone") = [] - set Version("tombstone", 2) = None - -T1: del "missing" - set TxnWrite(2, "missing") = [] - set Version("missing", 2) = None - -T1: commit - del TxnWrite(2, "key") - del TxnWrite(2, "missing") - del TxnWrite(2, "tombstone") - del TxnActive(2) - -Engine state: -NextVersion = 3 -Version("key", 1) = 0x01 -Version("key", 2) = None -Version("missing", 2) = None -Version("tombstone", 1) = None -Version("tombstone", 2) = None diff --git a/src/storage/golden/mvcc/delete_conflict b/src/storage/golden/mvcc/delete_conflict deleted file mode 100644 index 916c456b5..000000000 --- a/src/storage/golden/mvcc/delete_conflict +++ /dev/null @@ -1,54 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T2: begin → v2 read-write active={1} - set NextVersion = 3 - set TxnActiveSnapshot(2) = {1} - set TxnActive(2) = [] - -T3: begin → v3 read-write active={1,2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {1,2} - set TxnActive(3) = [] - -T4: begin → v4 read-write active={1,2,3} - set NextVersion = 5 - set TxnActiveSnapshot(4) = {1,2,3} - set TxnActive(4) = [] - -T1: set "a" = 0x01 - set TxnWrite(1, "a") = [] - set Version("a", 1) = 0x01 - -T3: set "c" = 0x03 - set TxnWrite(3, "c") = [] - set Version("c", 3) = 0x03 - -T4: set "d" = 0x04 - set TxnWrite(4, "d") = [] - set Version("d", 4) = 0x04 - -T4: commit - del TxnWrite(4, "d") - del TxnActive(4) - -T2: del "a" → Error::Serialization - -T2: del "c" → Error::Serialization - -T2: del "d" → Error::Serialization - -Engine state: -NextVersion = 5 -TxnActive(1) = [] -TxnActive(2) = [] -TxnActive(3) = [] -TxnActiveSnapshot(2) = {1} -TxnActiveSnapshot(3) = {1,2} -TxnActiveSnapshot(4) = {1,2,3} -TxnWrite(1, "a") = [] -TxnWrite(3, "c") = [] -Version("a", 1) = 0x01 -Version("c", 3) = 0x03 -Version("d", 4) = 0x04 diff --git a/src/storage/golden/mvcc/get b/src/storage/golden/mvcc/get deleted file mode 100644 index 53e4814da..000000000 --- a/src/storage/golden/mvcc/get +++ /dev/null @@ -1,27 +0,0 @@ -Engine state: -NextVersion = 3 -Version("deleted", 1) = 0x01 -Version("deleted", 2) = None -Version("key", 1) = 0x01 -Version("tombstone", 1) = None -Version("updated", 1) = 0x01 -Version("updated", 2) = 0x02 - -T1: begin read-only → v3 read-only active={} - -T1: get "key" → 0x01 - -T1: get "updated" → 0x02 - -T1: get "deleted" → None - -T1: get "tombstone" → None - -Engine state: -NextVersion = 3 -Version("deleted", 1) = 0x01 -Version("deleted", 2) = None -Version("key", 1) = 0x01 -Version("tombstone", 1) = None -Version("updated", 1) = 0x01 -Version("updated", 2) = 0x02 diff --git a/src/storage/golden/mvcc/get_isolation b/src/storage/golden/mvcc/get_isolation deleted file mode 100644 index ab5dc9954..000000000 --- a/src/storage/golden/mvcc/get_isolation +++ /dev/null @@ -1,97 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T1: set "a" = 0x01 - set TxnWrite(1, "a") = [] - set Version("a", 1) = 0x01 - -T1: set "b" = 0x01 - set TxnWrite(1, "b") = [] - set Version("b", 1) = 0x01 - -T1: set "d" = 0x01 - set TxnWrite(1, "d") = [] - set Version("d", 1) = 0x01 - -T1: set "e" = 0x01 - set TxnWrite(1, "e") = [] - set Version("e", 1) = 0x01 - -T1: commit - del TxnWrite(1, "a") - del TxnWrite(1, "b") - del TxnWrite(1, "d") - del TxnWrite(1, "e") - del TxnActive(1) - -T2: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T2: set "a" = 0x02 - set TxnWrite(2, "a") = [] - set Version("a", 2) = 0x02 - -T2: del "b" - set TxnWrite(2, "b") = [] - set Version("b", 2) = None - -T2: set "c" = 0x02 - set TxnWrite(2, "c") = [] - set Version("c", 2) = 0x02 - -T3: begin read-only → v3 read-only active={2} - -T4: begin → v3 read-write active={2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {2} - set TxnActive(3) = [] - -T4: set "d" = 0x03 - set TxnWrite(3, "d") = [] - set Version("d", 3) = 0x03 - -T4: del "e" - set TxnWrite(3, "e") = [] - set Version("e", 3) = None - -T4: set "f" = 0x03 - set TxnWrite(3, "f") = [] - set Version("f", 3) = 0x03 - -T4: commit - del TxnWrite(3, "d") - del TxnWrite(3, "e") - del TxnWrite(3, "f") - del TxnActive(3) - -T3: get "a" → 0x01 - -T3: get "b" → 0x01 - -T3: get "c" → None - -T3: get "d" → 0x01 - -T3: get "e" → 0x01 - -T3: get "f" → None - -Engine state: -NextVersion = 4 -TxnActive(2) = [] -TxnActiveSnapshot(3) = {2} -TxnWrite(2, "a") = [] -TxnWrite(2, "b") = [] -TxnWrite(2, "c") = [] -Version("a", 1) = 0x01 -Version("a", 2) = 0x02 -Version("b", 1) = 0x01 -Version("b", 2) = None -Version("c", 2) = 0x02 -Version("d", 1) = 0x01 -Version("d", 3) = 0x03 -Version("e", 1) = 0x01 -Version("e", 3) = None -Version("f", 3) = 0x03 diff --git a/src/storage/golden/mvcc/resume b/src/storage/golden/mvcc/resume deleted file mode 100644 index 6299a643c..000000000 --- a/src/storage/golden/mvcc/resume +++ /dev/null @@ -1,110 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T1: set "a" = 0x01 - set TxnWrite(1, "a") = [] - set Version("a", 1) = 0x01 - -T1: set "b" = 0x01 - set TxnWrite(1, "b") = [] - set Version("b", 1) = 0x01 - -T1: commit - del TxnWrite(1, "a") - del TxnWrite(1, "b") - del TxnActive(1) - -T2: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T3: begin → v3 read-write active={2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {2} - set TxnActive(3) = [] - -T4: begin → v4 read-write active={2,3} - set NextVersion = 5 - set TxnActiveSnapshot(4) = {2,3} - set TxnActive(4) = [] - -T2: set "a" = 0x02 - set TxnWrite(2, "a") = [] - set Version("a", 2) = 0x02 - -T3: set "b" = 0x03 - set TxnWrite(3, "b") = [] - set Version("b", 3) = 0x03 - -T4: set "c" = 0x04 - set TxnWrite(4, "c") = [] - set Version("c", 4) = 0x04 - -T2: commit - del TxnWrite(2, "a") - del TxnActive(2) - -T4: commit - del TxnWrite(4, "c") - del TxnActive(4) - -T5: resume → v3 read-write active={2} - -T5: scan .. - "a" = 0x01 - "b" = 0x03 - -T6: begin → v5 read-write active={3} - set NextVersion = 6 - set TxnActiveSnapshot(5) = {3} - set TxnActive(5) = [] - -T6: scan .. - "a" = 0x02 - "b" = 0x01 - "c" = 0x04 - -T6: rollback - del TxnActive(5) - -T5: commit - del TxnWrite(3, "b") - del TxnActive(3) - -T7: begin → v6 read-write active={} - set NextVersion = 7 - set TxnActive(6) = [] - -T7: scan .. - "a" = 0x02 - "b" = 0x03 - "c" = 0x04 - -T7: rollback - del TxnActive(6) - -T8: resume → Error::InvalidInput("no active transaction at version 3") - -T9: begin as of 3 → v3 read-only active={2} - -T9: scan .. - "a" = 0x01 - "b" = 0x01 - -T10: resume → v3 read-only active={2} - -T10: scan .. - "a" = 0x01 - "b" = 0x01 - -Engine state: -NextVersion = 7 -TxnActiveSnapshot(3) = {2} -TxnActiveSnapshot(4) = {2,3} -TxnActiveSnapshot(5) = {3} -Version("a", 1) = 0x01 -Version("a", 2) = 0x02 -Version("b", 1) = 0x01 -Version("b", 3) = 0x03 -Version("c", 4) = 0x04 diff --git a/src/storage/golden/mvcc/rollback b/src/storage/golden/mvcc/rollback deleted file mode 100644 index 6d116a5ad..000000000 --- a/src/storage/golden/mvcc/rollback +++ /dev/null @@ -1,94 +0,0 @@ -Engine state: -NextVersion = 2 -Version("a", 1) = 0x00 -Version("b", 1) = 0x00 -Version("c", 1) = 0x00 -Version("d", 1) = 0x00 - -T1: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T2: begin → v3 read-write active={2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {2} - set TxnActive(3) = [] - -T3: begin → v4 read-write active={2,3} - set NextVersion = 5 - set TxnActiveSnapshot(4) = {2,3} - set TxnActive(4) = [] - -T1: set "a" = 0x01 - set TxnWrite(2, "a") = [] - set Version("a", 2) = 0x01 - -T2: set "b" = 0x02 - set TxnWrite(3, "b") = [] - set Version("b", 3) = 0x02 - -T2: del "c" - set TxnWrite(3, "c") = [] - set Version("c", 3) = None - -T3: set "d" = 0x03 - set TxnWrite(4, "d") = [] - set Version("d", 4) = 0x03 - -T1: set "b" = 0x01 → Error::Serialization - -T3: set "c" = 0x03 → Error::Serialization - -T2: rollback - del Version("b", 3) - del TxnWrite(3, "b") - del Version("c", 3) - del TxnWrite(3, "c") - del TxnActive(3) - -T4: begin read-only → v5 read-only active={2,4} - -T4: scan .. - "a" = 0x00 - "b" = 0x00 - "c" = 0x00 - "d" = 0x00 - -T1: set "b" = 0x01 - set TxnWrite(2, "b") = [] - set Version("b", 2) = 0x01 - -T3: set "c" = 0x03 - set TxnWrite(4, "c") = [] - set Version("c", 4) = 0x03 - -T1: commit - del TxnWrite(2, "a") - del TxnWrite(2, "b") - del TxnActive(2) - -T3: commit - del TxnWrite(4, "c") - del TxnWrite(4, "d") - del TxnActive(4) - -T5: begin read-only → v5 read-only active={} - -T5: scan .. - "a" = 0x01 - "b" = 0x01 - "c" = 0x03 - "d" = 0x03 - -Engine state: -NextVersion = 5 -TxnActiveSnapshot(3) = {2} -TxnActiveSnapshot(4) = {2,3} -Version("a", 1) = 0x00 -Version("a", 2) = 0x01 -Version("b", 1) = 0x00 -Version("b", 2) = 0x01 -Version("c", 1) = 0x00 -Version("c", 4) = 0x03 -Version("d", 1) = 0x00 -Version("d", 4) = 0x03 diff --git a/src/storage/golden/mvcc/scan b/src/storage/golden/mvcc/scan deleted file mode 100644 index 0dc850f29..000000000 --- a/src/storage/golden/mvcc/scan +++ /dev/null @@ -1,109 +0,0 @@ -Engine state: -NextVersion = 5 -Version("B", 1) = 0x0001 -Version("B", 3) = None -Version("a", 1) = 0x0a01 -Version("a", 2) = None -Version("a", 3) = 0x0a03 -Version("b", 1) = None -Version("b", 3) = 0x0b03 -Version("b", 4) = None -Version("ba", 2) = 0xba02 -Version("ba", 4) = 0xba04 -Version("bb", 2) = 0xbb02 -Version("bb", 3) = None -Version("bc", 2) = 0xbc02 -Version("c", 1) = 0x0c01 - -T1: begin as of 1 → v1 read-only active={} - -T1: scan .. - -T2: begin as of 2 → v2 read-only active={} - -T2: scan .. - "B" = 0x0001 - "a" = 0x0a01 - "c" = 0x0c01 - -T3: begin as of 3 → v3 read-only active={} - -T3: scan .. - "B" = 0x0001 - "ba" = 0xba02 - "bb" = 0xbb02 - "bc" = 0xbc02 - "c" = 0x0c01 - -T4: begin as of 4 → v4 read-only active={} - -T4: scan .. - "a" = 0x0a03 - "b" = 0x0b03 - "ba" = 0xba02 - "bc" = 0xbc02 - "c" = 0x0c01 - -T5: begin as of 3 → v3 read-only active={} - -T5: scan .. - "B" = 0x0001 - "ba" = 0xba02 - "bb" = 0xbb02 - "bc" = 0xbc02 - "c" = 0x0c01 - -T5: scan .."bc"] - "B" = 0x0001 - "ba" = 0xba02 - "bb" = 0xbb02 - "bc" = 0xbc02 - -T5: scan .."bc") - "B" = 0x0001 - "ba" = 0xba02 - "bb" = 0xbb02 - -T5: scan ["ba".. - "ba" = 0xba02 - "bb" = 0xbb02 - "bc" = 0xbc02 - "c" = 0x0c01 - -T5: scan ["ba".."bc"] - "ba" = 0xba02 - "bb" = 0xbb02 - "bc" = 0xbc02 - -T5: scan ["ba".."bc") - "ba" = 0xba02 - "bb" = 0xbb02 - -T5: scan ("ba".. - "bb" = 0xbb02 - "bc" = 0xbc02 - "c" = 0x0c01 - -T5: scan ("ba".."bc"] - "bb" = 0xbb02 - "bc" = 0xbc02 - -T5: scan ("ba".."bc") - "bb" = 0xbb02 - -Engine state: -NextVersion = 5 -Version("B", 1) = 0x0001 -Version("B", 3) = None -Version("a", 1) = 0x0a01 -Version("a", 2) = None -Version("a", 3) = 0x0a03 -Version("b", 1) = None -Version("b", 3) = 0x0b03 -Version("b", 4) = None -Version("ba", 2) = 0xba02 -Version("ba", 4) = 0xba04 -Version("bb", 2) = 0xbb02 -Version("bb", 3) = None -Version("bc", 2) = 0xbc02 -Version("c", 1) = 0x0c01 diff --git a/src/storage/golden/mvcc/scan_isolation b/src/storage/golden/mvcc/scan_isolation deleted file mode 100644 index bde2e14ea..000000000 --- a/src/storage/golden/mvcc/scan_isolation +++ /dev/null @@ -1,91 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T1: set "a" = 0x01 - set TxnWrite(1, "a") = [] - set Version("a", 1) = 0x01 - -T1: set "b" = 0x01 - set TxnWrite(1, "b") = [] - set Version("b", 1) = 0x01 - -T1: set "d" = 0x01 - set TxnWrite(1, "d") = [] - set Version("d", 1) = 0x01 - -T1: set "e" = 0x01 - set TxnWrite(1, "e") = [] - set Version("e", 1) = 0x01 - -T1: commit - del TxnWrite(1, "a") - del TxnWrite(1, "b") - del TxnWrite(1, "d") - del TxnWrite(1, "e") - del TxnActive(1) - -T2: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T2: set "a" = 0x02 - set TxnWrite(2, "a") = [] - set Version("a", 2) = 0x02 - -T2: del "b" - set TxnWrite(2, "b") = [] - set Version("b", 2) = None - -T2: set "c" = 0x02 - set TxnWrite(2, "c") = [] - set Version("c", 2) = 0x02 - -T3: begin read-only → v3 read-only active={2} - -T4: begin → v3 read-write active={2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {2} - set TxnActive(3) = [] - -T4: set "d" = 0x03 - set TxnWrite(3, "d") = [] - set Version("d", 3) = 0x03 - -T4: del "e" - set TxnWrite(3, "e") = [] - set Version("e", 3) = None - -T4: set "f" = 0x03 - set TxnWrite(3, "f") = [] - set Version("f", 3) = 0x03 - -T4: commit - del TxnWrite(3, "d") - del TxnWrite(3, "e") - del TxnWrite(3, "f") - del TxnActive(3) - -T3: scan .. - "a" = 0x01 - "b" = 0x01 - "d" = 0x01 - "e" = 0x01 - -Engine state: -NextVersion = 4 -TxnActive(2) = [] -TxnActiveSnapshot(3) = {2} -TxnWrite(2, "a") = [] -TxnWrite(2, "b") = [] -TxnWrite(2, "c") = [] -Version("a", 1) = 0x01 -Version("a", 2) = 0x02 -Version("b", 1) = 0x01 -Version("b", 2) = None -Version("c", 2) = 0x02 -Version("d", 1) = 0x01 -Version("d", 3) = 0x03 -Version("e", 1) = 0x01 -Version("e", 3) = None -Version("f", 3) = 0x03 diff --git a/src/storage/golden/mvcc/scan_key_version_encoding b/src/storage/golden/mvcc/scan_key_version_encoding deleted file mode 100644 index 3f00e7d24..000000000 --- a/src/storage/golden/mvcc/scan_key_version_encoding +++ /dev/null @@ -1,53 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T1: set 0x00 = 0x01 - set TxnWrite(1, 0x00) = [] - set Version(0x00, 1) = 0x01 - -T1: commit - del TxnWrite(1, 0x00) - del TxnActive(1) - -T2: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T2: set 0x00 = 0x02 - set TxnWrite(2, 0x00) = [] - set Version(0x00, 2) = 0x02 - -T2: set 0x000000000000000002 = 0x02 - set TxnWrite(2, 0x000000000000000002) = [] - set Version(0x000000000000000002, 2) = 0x02 - -T2: commit - del TxnWrite(2, 0x00) - del TxnWrite(2, 0x000000000000000002) - del TxnActive(2) - -T3: begin → v3 read-write active={} - set NextVersion = 4 - set TxnActive(3) = [] - -T3: set 0x00 = 0x03 - set TxnWrite(3, 0x00) = [] - set Version(0x00, 3) = 0x03 - -T3: commit - del TxnWrite(3, 0x00) - del TxnActive(3) - -T4: begin read-only → v4 read-only active={} - -T4: scan .. - 0x00 = 0x03 - 0x000000000000000002 = 0x02 - -Engine state: -NextVersion = 4 -Version(0x00, 1) = 0x01 -Version(0x00, 2) = 0x02 -Version(0x00, 3) = 0x03 -Version(0x000000000000000002, 2) = 0x02 diff --git a/src/storage/golden/mvcc/scan_prefix b/src/storage/golden/mvcc/scan_prefix deleted file mode 100644 index a17adde82..000000000 --- a/src/storage/golden/mvcc/scan_prefix +++ /dev/null @@ -1,117 +0,0 @@ -Engine state: -NextVersion = 5 -Version("B", 1) = 0x0001 -Version("B", 3) = None -Version("a", 1) = 0x0a01 -Version("a", 2) = None -Version("a", 3) = 0x0a03 -Version("b", 1) = None -Version("b", 3) = 0x0b03 -Version("b", 4) = None -Version("ba", 2) = 0xba02 -Version("ba", 4) = 0xba04 -Version("bb", 2) = 0xbb02 -Version("bb", 3) = None -Version("bc", 2) = 0xbc02 -Version("c", 1) = 0x0c01 - -T1: begin as of 1 → v1 read-only active={} - -T1: scan prefix [] - -T2: begin as of 2 → v2 read-only active={} - -T2: scan prefix [] - "B" = 0x0001 - "a" = 0x0a01 - "c" = 0x0c01 - -T3: begin as of 3 → v3 read-only active={} - -T3: scan prefix [] - "B" = 0x0001 - "ba" = 0xba02 - "bb" = 0xbb02 - "bc" = 0xbc02 - "c" = 0x0c01 - -T4: begin as of 4 → v4 read-only active={} - -T4: scan prefix [] - "a" = 0x0a03 - "b" = 0x0b03 - "ba" = 0xba02 - "bc" = 0xbc02 - "c" = 0x0c01 - -T5: begin as of 3 → v3 read-only active={} - -T5: scan prefix "B" - "B" = 0x0001 - -T5: scan prefix "a" - -T5: scan prefix "b" - "ba" = 0xba02 - "bb" = 0xbb02 - "bc" = 0xbc02 - -T5: scan prefix "ba" - "ba" = 0xba02 - -T5: scan prefix "bb" - "bb" = 0xbb02 - -T5: scan prefix "bbb" - -T5: scan prefix "bc" - "bc" = 0xbc02 - -T5: scan prefix "c" - "c" = 0x0c01 - -T5: scan prefix "d" - -T6: begin as of 4 → v4 read-only active={} - -T6: scan prefix "B" - -T6: scan prefix "a" - "a" = 0x0a03 - -T6: scan prefix "b" - "b" = 0x0b03 - "ba" = 0xba02 - "bc" = 0xbc02 - -T6: scan prefix "ba" - "ba" = 0xba02 - -T6: scan prefix "bb" - -T6: scan prefix "bbb" - -T6: scan prefix "bc" - "bc" = 0xbc02 - -T6: scan prefix "c" - "c" = 0x0c01 - -T6: scan prefix "d" - -Engine state: -NextVersion = 5 -Version("B", 1) = 0x0001 -Version("B", 3) = None -Version("a", 1) = 0x0a01 -Version("a", 2) = None -Version("a", 3) = 0x0a03 -Version("b", 1) = None -Version("b", 3) = 0x0b03 -Version("b", 4) = None -Version("ba", 2) = 0xba02 -Version("ba", 4) = 0xba04 -Version("bb", 2) = 0xbb02 -Version("bb", 3) = None -Version("bc", 2) = 0xbc02 -Version("c", 1) = 0x0c01 diff --git a/src/storage/golden/mvcc/set b/src/storage/golden/mvcc/set deleted file mode 100644 index 36f2e9408..000000000 --- a/src/storage/golden/mvcc/set +++ /dev/null @@ -1,42 +0,0 @@ -Engine state: -NextVersion = 2 -Version("key", 1) = 0x01 -Version("tombstone", 1) = None - -T1: begin → v2 read-write active={} - set NextVersion = 3 - set TxnActive(2) = [] - -T1: set "key" = 0x02 - set TxnWrite(2, "key") = [] - set Version("key", 2) = 0x02 - -T1: set "tombstone" = 0x02 - set TxnWrite(2, "tombstone") = [] - set Version("tombstone", 2) = 0x02 - -T1: set "new" = 0x01 - set TxnWrite(2, "new") = [] - set Version("new", 2) = 0x01 - -T1: set "new" = 0x01 - set TxnWrite(2, "new") = [] - set Version("new", 2) = 0x01 - -T1: set "new" = 0x02 - set TxnWrite(2, "new") = [] - set Version("new", 2) = 0x02 - -T1: commit - del TxnWrite(2, "key") - del TxnWrite(2, "new") - del TxnWrite(2, "tombstone") - del TxnActive(2) - -Engine state: -NextVersion = 3 -Version("key", 1) = 0x01 -Version("key", 2) = 0x02 -Version("new", 2) = 0x02 -Version("tombstone", 1) = None -Version("tombstone", 2) = 0x02 diff --git a/src/storage/golden/mvcc/set_conflict b/src/storage/golden/mvcc/set_conflict deleted file mode 100644 index f5bd3e3d2..000000000 --- a/src/storage/golden/mvcc/set_conflict +++ /dev/null @@ -1,54 +0,0 @@ -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T2: begin → v2 read-write active={1} - set NextVersion = 3 - set TxnActiveSnapshot(2) = {1} - set TxnActive(2) = [] - -T3: begin → v3 read-write active={1,2} - set NextVersion = 4 - set TxnActiveSnapshot(3) = {1,2} - set TxnActive(3) = [] - -T4: begin → v4 read-write active={1,2,3} - set NextVersion = 5 - set TxnActiveSnapshot(4) = {1,2,3} - set TxnActive(4) = [] - -T1: set "a" = 0x01 - set TxnWrite(1, "a") = [] - set Version("a", 1) = 0x01 - -T3: set "c" = 0x03 - set TxnWrite(3, "c") = [] - set Version("c", 3) = 0x03 - -T4: set "d" = 0x04 - set TxnWrite(4, "d") = [] - set Version("d", 4) = 0x04 - -T4: commit - del TxnWrite(4, "d") - del TxnActive(4) - -T2: set "a" = 0x02 → Error::Serialization - -T2: set "c" = 0x02 → Error::Serialization - -T2: set "d" = 0x02 → Error::Serialization - -Engine state: -NextVersion = 5 -TxnActive(1) = [] -TxnActive(2) = [] -TxnActive(3) = [] -TxnActiveSnapshot(2) = {1} -TxnActiveSnapshot(3) = {1,2} -TxnActiveSnapshot(4) = {1,2,3} -TxnWrite(1, "a") = [] -TxnWrite(3, "c") = [] -Version("a", 1) = 0x01 -Version("c", 3) = 0x03 -Version("d", 4) = 0x04 diff --git a/src/storage/golden/mvcc/unversioned b/src/storage/golden/mvcc/unversioned deleted file mode 100644 index c5857b93d..000000000 --- a/src/storage/golden/mvcc/unversioned +++ /dev/null @@ -1,59 +0,0 @@ -T_: set unversioned "a" = 0x00 - set Unversioned("a") = 0x00 - -T1: begin → v1 read-write active={} - set NextVersion = 2 - set TxnActive(1) = [] - -T1: set "a" = 0x01 - set TxnWrite(1, "a") = [] - set Version("a", 1) = 0x01 - -T1: set "b" = 0x01 - set TxnWrite(1, "b") = [] - set Version("b", 1) = 0x01 - -T1: set "c" = 0x01 - set TxnWrite(1, "c") = [] - set Version("c", 1) = 0x01 - -T1: commit - del TxnWrite(1, "a") - del TxnWrite(1, "b") - del TxnWrite(1, "c") - del TxnActive(1) - -T_: set unversioned "b" = 0x00 - set Unversioned("b") = 0x00 - -T_: set unversioned "d" = 0x00 - set Unversioned("d") = 0x00 - -T2: begin read-only → v2 read-only active={} - -T2: scan .. - "a" = 0x01 - "b" = 0x01 - "c" = 0x01 - -T_: get unversioned "a" → 0x00 - -T_: get unversioned "b" → 0x00 - -T_: get unversioned "c" → None - -T_: get unversioned "d" → 0x00 - -T_: set unversioned "a" = 0x01 - set Unversioned("a") = 0x01 - -T_: get unversioned "a" → 0x01 - -Engine state: -NextVersion = 2 -Version("a", 1) = 0x01 -Version("b", 1) = 0x01 -Version("c", 1) = 0x01 -Unversioned("a") = 0x01 -Unversioned("b") = 0x00 -Unversioned("d") = 0x00 diff --git a/src/storage/mvcc.rs b/src/storage/mvcc.rs index 5c59b5838..d7ba88b5a 100644 --- a/src/storage/mvcc.rs +++ b/src/storage/mvcc.rs @@ -783,1150 +783,396 @@ impl<'a, E: Engine> DoubleEndedIterator for VersionIterator<'a, E> { #[cfg(test)] pub mod tests { - use super::super::debug; - use super::super::engine::test::{self as testengine, Emit}; - use super::super::Memory; + use super::super::engine::test::{Emit, Mirror, Operation}; + use super::super::{debug, BitCask, Memory}; use super::*; use crossbeam::channel::Receiver; + use itertools::Itertools as _; + use regex::Regex; use std::collections::HashMap; - use std::io::Write as _; - - const GOLDEN_DIR: &str = "src/storage/golden/mvcc"; - - /// An MVCC wrapper that records transaction schedules to golden masters. - /// TODO: migrate this to goldenscript. - struct Schedule { - mvcc: MVCC>, - op_rx: Receiver, - mint: goldenfile::Mint, - file: Arc>, - next_id: u8, - } + use std::error::Error as StdError; + use std::fmt::Write as _; + use std::result::Result as StdResult; + use test_case::test_case; + use test_each_file::test_each_path; - impl Schedule { - /// Creates a new schedule using the given golden master filename. - fn new(name: &str) -> Result { - let (op_tx, op_rx) = crossbeam::channel::unbounded(); - let mvcc = MVCC::new(Emit::new(Memory::new(), op_tx)); - let mut mint = goldenfile::Mint::new(GOLDEN_DIR); - let file = Arc::new(Mutex::new(mint.new_goldenfile(name)?)); - Ok(Self { mvcc, op_rx, mint, file, next_id: 1 }) - } + // Run goldenscript tests in src/storage/testscripts/mvcc. + test_each_path! { in "src/storage/testscripts/mvcc" as scripts => test_goldenscript } - /// Sets up an initial, versioned dataset from the given data as a - /// vector of key,version,value tuples. These transactions are not - /// assigned transaction IDs, nor are the writes logged, except for the - /// initial engine state. - #[allow(clippy::type_complexity)] - fn setup(&mut self, data: Vec<(&[u8], Version, Option<&[u8]>)>) -> Result<()> { - // Segment the writes by version. - let mut writes = HashMap::new(); - for (key, version, value) in data { - writes - .entry(version) - .or_insert(Vec::new()) - .push((key.to_vec(), value.map(|v| v.to_vec()))); + fn test_goldenscript(path: &std::path::Path) { + goldenscript::run(&mut MVCCRunner::new(), path).expect("goldenscript failed") + } + + /// Tests that key prefixes are actually prefixes of keys. + #[test_case(KeyPrefix::NextVersion, Key::NextVersion; "NextVersion")] + #[test_case(KeyPrefix::TxnActive, Key::TxnActive(1); "TxnActive")] + #[test_case(KeyPrefix::TxnActiveSnapshot, Key::TxnActiveSnapshot(1); "TxnActiveSnapshot")] + #[test_case(KeyPrefix::TxnWrite(1), Key::TxnWrite(1, b"foo".as_slice().into()); "TxnWrite")] + #[test_case(KeyPrefix::Version(b"foo".as_slice().into()), Key::Version(b"foo".as_slice().into(), 1); "Version")] + #[test_case(KeyPrefix::Unversioned, Key::Unversioned(b"foo".as_slice().into()); "Unversioned")] + fn key_prefix(prefix: KeyPrefix, key: Key) { + let prefix = prefix.encode(); + let key = key.encode(); + assert_eq!(prefix, key[..prefix.len()]) + } + + /// Runs MVCC goldenscript tests. + pub struct MVCCRunner { + mvcc: MVCC, + txns: HashMap>, + op_rx: Receiver, + #[allow(dead_code)] + tempdir: tempfile::TempDir, + } + + type TestEngine = Emit>; + + impl goldenscript::Runner for MVCCRunner { + fn run(&mut self, command: &goldenscript::Command) -> StdResult> { + // Validate tags. + if let Some(tag) = command.tags.iter().find(|t| *t != "ops") { + return Err(format!("invalid tag {tag}").into()); } - // Insert the writes with individual transactions. - for i in 1..=writes.keys().max().copied().unwrap_or(0) { - let txn = self.mvcc.begin()?; - for (key, value) in writes.get(&i).unwrap_or(&Vec::new()) { - if let Some(value) = value { - txn.set(key, value.clone())?; - } else { - txn.delete(key)?; + + // Execute command. + let mut output = String::new(); + match command.name.as_str() { + // txn: begin [readonly] [as_of=VERSION] + "begin" => { + let name = Self::txn_name(&command.prefix)?; + if self.txns.contains_key(name) { + return Err(format!("txn {name} already exists").into()); } + let mut args = command.consume_args(); + let readonly = match args.next_pos().map(|a| a.value.as_str()) { + Some("readonly") => true, + None => false, + Some(v) => return Err(format!("invalid argument {v}").into()), + }; + let as_of = args.lookup_parse("as_of")?; + args.reject_rest()?; + let txn = match (readonly, as_of) { + (false, None) => self.mvcc.begin()?, + (true, None) => self.mvcc.begin_read_only()?, + (true, Some(v)) => self.mvcc.begin_as_of(v)?, + (false, Some(_)) => return Err("as_of only valid for read-only txn".into()), + }; + self.txns.insert(name.to_string(), txn); } - txn.commit()?; - } - // Flush the write log, but dump the engine contents. - while self.op_rx.try_recv().is_ok() {} - self.print_engine()?; - writeln!(&mut self.file.lock()?)?; - Ok(()) - } - fn begin(&mut self) -> Result { - self.new_txn("begin", self.mvcc.begin()) - } + // txn: commit + "commit" => { + let name = Self::txn_name(&command.prefix)?; + let txn = self.txns.remove(name).ok_or(format!("unknown txn {name}"))?; + command.consume_args().reject_rest()?; + txn.commit()?; + } - fn begin_read_only(&mut self) -> Result { - self.new_txn("begin read-only", self.mvcc.begin_read_only()) - } + // txn: delete KEY... + "delete" => { + let txn = self.get_txn(&command.prefix)?; + let mut args = command.consume_args(); + for arg in args.rest_pos() { + let key = Self::decode_binary(&arg.value); + txn.delete(&key)?; + } + args.reject_rest()?; + } - fn begin_as_of(&mut self, version: Version) -> Result { - self.new_txn(&format!("begin as of {}", version), self.mvcc.begin_as_of(version)) - } + // dump + "dump" => { + command.consume_args().reject_rest()?; + let mut engine = self.mvcc.engine.lock().unwrap(); + let mut scan = engine.scan(..); + while let Some((key, value)) = scan.next().transpose()? { + let (fkey, Some(fvalue)) = debug::format_key_value(&key, &Some(value)) + else { + panic!("expected option"); + }; + writeln!(output, "{fkey} → {fvalue}")?; + } + } - fn resume(&mut self, state: TransactionState) -> Result { - self.new_txn("resume", self.mvcc.resume(state)) - } + // txn: get KEY... + "get" => { + let txn = self.get_txn(&command.prefix)?; + let mut args = command.consume_args(); + for arg in args.rest_pos() { + let key = Self::decode_binary(&arg.value); + let value = txn.get(&key)?; + writeln!(output, "{}", Self::format_key_value(&key, value.as_deref()))?; + } + args.reject_rest()?; + } - /// Processes a begin/resume result. - fn new_txn( - &mut self, - name: &str, - result: Result>>, - ) -> Result { - let id = self.next_id; - self.next_id += 1; - self.print_begin(id, name, &result)?; - result.map(|txn| ScheduleTransaction { - id, - txn, - file: self.file.clone(), - op_rx: self.op_rx.clone(), - }) - } + // get_unversioned KEY... + "get_unversioned" => { + Self::no_txn(command)?; + let mut args = command.consume_args(); + for arg in args.rest_pos() { + let key = Self::decode_binary(&arg.value); + let value = self.mvcc.get_unversioned(&key)?; + writeln!(output, "{}", Self::format_key_value(&key, value.as_deref()))?; + } + args.reject_rest()?; + } - /// Prints a transaction begin to the golden file. - fn print_begin( - &mut self, - id: u8, - name: &str, - result: &Result>>, - ) -> Result<()> { - let mut f = self.file.lock()?; - write!(f, "T{}: {} → ", id, name)?; - match result { - Ok(txn) => writeln!(f, "{}", debug::format_txn(txn.state()))?, - Err(err) => writeln!(f, "Error::{:?}", err)?, - }; - Self::print_log(&mut f, &self.op_rx)?; - writeln!(f)?; - Ok(()) - } - /// Prints the engine write log since the last call to the golden file. - fn print_log( - f: &mut MutexGuard<'_, std::fs::File>, - op_rx: &Receiver, - ) -> Result<()> { - while let Ok(op) = op_rx.try_recv() { - match op { - testengine::Operation::Delete { key } => { - let (fkey, _) = debug::format_key_value(&key, &None); - writeln!(f, " del {fkey}") + // import [VERSION] KEY=VALUE... + "import" => { + Self::no_txn(command)?; + let mut args = command.consume_args(); + let version = args.next_pos().map(|a| a.parse()).transpose()?; + let mut txn = self.mvcc.begin()?; + if let Some(version) = version { + if txn.version() > version { + return Err(format!("version {version} already used").into()); + } + while txn.version() < version { + txn = self.mvcc.begin()?; + } } - testengine::Operation::Flush => writeln!(f, " flush"), - testengine::Operation::Set { key, value } => { - let (fkey, fvalue) = debug::format_key_value(&key, &Some(value)); - writeln!(f, " set {} = {}", fkey, fvalue.unwrap()) + for kv in args.rest_key() { + let key = Self::decode_binary(kv.key.as_ref().unwrap()); + let value = Self::decode_binary(&kv.value); + if value.is_empty() { + txn.delete(&key)?; + } else { + txn.set(&key, value)?; + } } - }?; - } - Ok(()) - } - - /// Prints the engine contents to the golden file. - fn print_engine(&self) -> Result<()> { - let mut f = self.file.lock()?; - let mut engine = self.mvcc.engine.lock()?; - let mut scan = engine.scan(..); - writeln!(f, "Engine state:")?; - while let Some((key, value)) = scan.next().transpose()? { - if let (fkey, Some(fvalue)) = debug::format_key_value(&key, &Some(value)) { - writeln!(f, "{} = {}", fkey, fvalue)?; + args.reject_rest()?; + txn.commit()?; } - } - Ok(()) - } - fn get_unversioned(&self, key: &[u8]) -> Result>> { - let value = self.mvcc.get_unversioned(key)?; - write!( - self.file.lock()?, - "T_: get unversioned {} → {}\n\n", - debug::format_raw(key), - if let Some(ref value) = value { - debug::format_raw(value) - } else { - String::from("None") + // txn: resume JSON + "resume" => { + let name = Self::txn_name(&command.prefix)?; + let mut args = command.consume_args(); + let raw = &args.next_pos().ok_or("state not given")?.value; + args.reject_rest()?; + let state: TransactionState = serde_json::from_str(raw)?; + let txn = self.mvcc.resume(state)?; + self.txns.insert(name.to_string(), txn); } - )?; - Ok(value) - } - - fn set_unversioned(&self, key: &[u8], value: Vec) -> Result<()> { - let mut f = self.file.lock()?; - write!( - f, - "T_: set unversioned {} = {}", - debug::format_raw(key), - debug::format_raw(&value) - )?; - let result = self.mvcc.set_unversioned(key, value); - match &result { - Ok(_) => writeln!(f)?, - Err(err) => writeln!(f, " → Error::{:?}", err)?, - } - Schedule::print_log(&mut f, &self.op_rx)?; - writeln!(f)?; - result - } - } - impl Drop for Schedule { - /// Print engine contents when the schedule is dropped. - fn drop(&mut self) { - _ = self.print_engine(); - _ = self.mint; // goldenfile assertions run when mint is dropped - } - } - - struct ScheduleTransaction { - id: u8, - txn: Transaction>, - file: Arc>, - op_rx: Receiver, - } - - impl Clone for ScheduleTransaction { - /// Allow cloning a schedule transaction, to simplify handling when - /// commit/rollback consumes it. We don't want to allow this in general, - /// since a commit/rollback will invalidate the cloned transactions. - fn clone(&self) -> Self { - let txn = Transaction { engine: self.txn.engine.clone(), st: self.txn.st.clone() }; - Self { id: self.id, op_rx: self.op_rx.clone(), txn, file: self.file.clone() } - } - } - - impl ScheduleTransaction { - fn state(&self) -> TransactionState { - self.txn.state().clone() - } - - fn commit(self) -> Result<()> { - let result = self.clone().txn.commit(); // clone to retain self.txn for printing - self.print_mutation("commit", &result)?; - result - } + // txn: rollback + "rollback" => { + let name = Self::txn_name(&command.prefix)?; + let txn = self.txns.remove(name).ok_or(format!("unknown txn {name}"))?; + command.consume_args().reject_rest()?; + txn.rollback()?; + } - fn rollback(self) -> Result<()> { - let result = self.clone().txn.rollback(); // clone to retain self.txn for printing - self.print_mutation("rollback", &result)?; - result - } + // txn: scan [reverse=BOOL] [RANGE] + "scan" => { + let txn = self.get_txn(&command.prefix)?; + let mut args = command.consume_args(); + let reverse = args.lookup_parse("reverse")?.unwrap_or(false); + let range = Self::parse_key_range( + args.next_pos().map(|a| a.value.as_str()).unwrap_or(".."), + )?; + args.reject_rest()?; + + let mut scan = txn.scan(range)?; + let kvs: Vec<_> = match reverse { + false => scan.iter().collect::>()?, + true => scan.iter().rev().collect::>()?, + }; + for (key, value) in kvs { + writeln!(output, "{}", Self::format_key_value(&key, Some(&value)))?; + } + } - fn delete(&self, key: &[u8]) -> Result<()> { - let result = self.txn.delete(key); - self.print_mutation(&format!("del {}", debug::format_raw(key)), &result)?; - result - } + // txn: scan_prefix [reverse=BOOL] PREFIX + "scan_prefix" => { + let txn = self.get_txn(&command.prefix)?; + let mut args = command.consume_args(); + let prefix = + Self::decode_binary(&args.next_pos().ok_or("prefix not given")?.value); + let reverse = args.lookup_parse("reverse")?.unwrap_or(false); + args.reject_rest()?; + + let mut scan = txn.scan_prefix(&prefix)?; + let kvs: Vec<_> = match reverse { + false => scan.iter().collect::>()?, + true => scan.iter().rev().collect::>()?, + }; + for (key, value) in kvs { + writeln!(output, "{}", Self::format_key_value(&key, Some(&value)))?; + } + } - fn set(&self, key: &[u8], value: Vec) -> Result<()> { - let result = self.txn.set(key, value.clone()); - self.print_mutation( - &format!("set {} = {}", debug::format_raw(key), debug::format_raw(&value)), - &result, - )?; - result - } + // txn: set KEY=VALUE... + "set" => { + let txn = self.get_txn(&command.prefix)?; + let mut args = command.consume_args(); + for kv in args.rest_key() { + let key = Self::decode_binary(kv.key.as_ref().unwrap()); + let value = Self::decode_binary(&kv.value); + txn.set(&key, value)?; + } + args.reject_rest()?; + } - fn get(&self, key: &[u8]) -> Result>> { - let value = self.txn.get(key)?; - write!( - self.file.lock()?, - "T{}: get {} → {}\n\n", - self.id, - debug::format_raw(key), - if let Some(ref value) = value { - debug::format_raw(value) - } else { - String::from("None") + // set_unversioned KEY=VALUE... + "set_unversioned" => { + Self::no_txn(command)?; + let mut args = command.consume_args(); + for kv in args.rest_key() { + let key = Self::decode_binary(kv.key.as_ref().unwrap()); + let value = Self::decode_binary(&kv.value); + self.mvcc.set_unversioned(&key, value)?; + } + args.reject_rest()?; } - )?; - Ok(value) - } - fn scan>>(&self, range: R) -> Result>> { - let name = format!( - "scan {}..{}", - match range.start_bound() { - Bound::Excluded(k) => format!("({}", debug::format_raw(k)), - Bound::Included(k) => format!("[{}", debug::format_raw(k)), - Bound::Unbounded => "".to_string(), - }, - match range.end_bound() { - Bound::Excluded(k) => format!("{})", debug::format_raw(k)), - Bound::Included(k) => format!("{}]", debug::format_raw(k)), - Bound::Unbounded => "".to_string(), - }, - ); - let mut scan = self.txn.scan(range)?; - self.print_scan(&name, scan.to_vec()?)?; - Ok(scan) - } + // txn: state + "state" => { + command.consume_args().reject_rest()?; + let txn = self.get_txn(&command.prefix)?; + let state = txn.state(); + write!( + output, + "v{} {} active={{{}}}", + state.version, + if state.read_only { "ro" } else { "rw" }, + state.active.iter().sorted().join(",") + )?; + } - fn scan_prefix(&self, prefix: &[u8]) -> Result>> { - let mut scan = self.txn.scan_prefix(prefix)?; - self.print_scan(&format!("scan prefix {}", debug::format_raw(prefix)), scan.to_vec()?)?; - Ok(scan) - } + // status + "status" => { + let status = self.mvcc.status()?; + writeln!(output, "{status:#?}")?; + } - /// Prints the result of a mutation to the golden file. - fn print_mutation(&self, name: &str, result: &Result<()>) -> Result<()> { - let mut f = self.file.lock()?; - write!(f, "T{}: {}", self.id, name)?; - match result { - Ok(_) => writeln!(f)?, - Err(err) => writeln!(f, " → Error::{:?}", err)?, + name => return Err(format!("invalid command {name}").into()), } - Schedule::print_log(&mut f, &self.op_rx)?; - writeln!(f)?; - Ok(()) + Ok(output) } - /// Prints the results of a scan to the golden file. - fn print_scan(&self, name: &str, scan: Vec<(Vec, Vec)>) -> Result<()> { - let mut f = self.file.lock()?; - writeln!(f, "T{}: {}", self.id, name)?; - for (key, value) in scan { - writeln!(f, " {} = {}", debug::format_raw(&key), debug::format_raw(&value))? + fn end_command( + &mut self, + command: &goldenscript::Command, + ) -> StdResult> { + let mut output = String::new(); + + // Output engine operations, if requested. Otherwise, drain them. + let show_ops = command.tags.contains(&"ops".to_string()); + while let Ok(op) = self.op_rx.try_recv() { + if !show_ops { + continue; + } + match op { + Operation::Delete { key } => { + let (fkey, _) = debug::format_key_value(&key, &None); + writeln!(output, "engine delete {fkey}")? + } + Operation::Flush => writeln!(output, "engine flush")?, + Operation::Set { key, value } => { + let (fkey, fvalue) = debug::format_key_value(&key, &Some(value)); + writeln!(output, "engine set {} → {}", fkey, fvalue.unwrap())? + } + } } - writeln!(f)?; - Ok(()) + Ok(output) } } - /// Asserts that a scan yields the expected result. - macro_rules! assert_scan { - ( $scan:expr => { $( $key:expr => $value:expr),* $(,)? } ) => { - let result = $scan.to_vec()?; - let expect = vec![ - $( ($key.to_vec(), $value.to_vec()), )* - ]; - assert_eq!(result, expect); - }; - } + impl MVCCRunner { + fn new() -> Self { + // Use both a BitCask and a Memory engine, and mirror operations + // across them. Emit engine operations to op_rx. + let (op_tx, op_rx) = crossbeam::channel::unbounded(); + let tempdir = tempfile::TempDir::with_prefix("toydb").expect("tempdir failed"); + let bitcask = BitCask::new(tempdir.path().join("bitcask")).expect("bitcask failed"); + let memory = Memory::new(); + let engine = Emit::new(Mirror::new(bitcask, memory), op_tx); + let mvcc = MVCC::new(engine); + Self { mvcc, op_rx, txns: HashMap::new(), tempdir } + } - // Asserts scan invariants. - #[track_caller] - fn assert_scan_invariants(scan: &mut Scan>) -> Result<()> { - // Iterator and vec should yield same results. - let result = scan.to_vec()?; - assert_eq!(scan.iter().collect::>>()?, result); - - // Forward and reverse scans should give the same results. - let mut forward = result.clone(); - forward.reverse(); - let reverse = scan.iter().rev().collect::>>()?; - assert_eq!(reverse, forward); - - // Alternating next/next_back calls should give the same results. - let mut forward = Vec::new(); - let mut reverse = Vec::new(); - let mut iter = scan.iter(); - while let Some(b) = iter.next().transpose()? { - forward.push(b); - if let Some(b) = iter.next_back().transpose()? { - reverse.push(b); + /// Decodes a raw byte vector from a Unicode string. Code points in the + /// range U+0080 to U+00FF are converted back to bytes 0x80 to 0xff. + /// This allows using e.g. \xff in the input string literal, and getting + /// back a 0xff byte in the byte vector. Otherwise, char(0xff) yields + /// the UTF-8 bytes 0xc3bf, which is the U+00FF code point as UTF-8. + /// These characters are effectively represented as ISO-8859-1 rather + /// than UTF-8, but it allows precise use of the entire u8 value range. + /// + /// TODO: share this with engine::test::Runner. + pub fn decode_binary(s: &str) -> Vec { + let mut buf = [0; 4]; + let mut bytes = Vec::new(); + for c in s.chars() { + // u32 is the Unicode code point, not the UTF-8 encoding. + match c as u32 { + b @ 0x80..=0xff => bytes.push(b as u8), + _ => bytes.extend(c.encode_utf8(&mut buf).as_bytes()), + } } + bytes } - reverse.reverse(); - forward.extend_from_slice(&reverse); - assert_eq!(forward, result); - Ok(()) - } - - #[test] - /// Tests that key prefixes are actually prefixes of keys. - fn key_prefix() -> Result<()> { - let cases = vec![ - (KeyPrefix::NextVersion, Key::NextVersion), - (KeyPrefix::TxnActive, Key::TxnActive(1)), - (KeyPrefix::TxnActiveSnapshot, Key::TxnActiveSnapshot(1)), - (KeyPrefix::TxnWrite(1), Key::TxnWrite(1, b"foo".as_slice().into())), - ( - KeyPrefix::Version(b"foo".as_slice().into()), - Key::Version(b"foo".as_slice().into(), 1), - ), - (KeyPrefix::Unversioned, Key::Unversioned(b"foo".as_slice().into())), - ]; - - for (prefix, key) in cases { - let prefix = prefix.encode(); - let key = key.encode(); - assert_eq!(prefix, key[..prefix.len()]) + /// Formats a raw binary byte vector, escaping special characters. + /// TODO: find a better way to manage and share formatting functions. + fn format_bytes(bytes: &[u8]) -> String { + let b: Vec = bytes.iter().copied().flat_map(std::ascii::escape_default).collect(); + String::from_utf8_lossy(&b).to_string() } - Ok(()) - } - - #[test] - /// Begin should create txns with new versions and current active sets. - fn begin() -> Result<()> { - let mut mvcc = Schedule::new("begin")?; - - let t1 = mvcc.begin()?; - assert_eq!( - t1.state(), - TransactionState { version: 1, read_only: false, active: HashSet::new() } - ); - - let t2 = mvcc.begin()?; - assert_eq!( - t2.state(), - TransactionState { version: 2, read_only: false, active: HashSet::from([1]) } - ); - - let t3 = mvcc.begin()?; - assert_eq!( - t3.state(), - TransactionState { version: 3, read_only: false, active: HashSet::from([1, 2]) } - ); - - t2.commit()?; // commit to remove from active set - - let t4 = mvcc.begin()?; - assert_eq!( - t4.state(), - TransactionState { version: 4, read_only: false, active: HashSet::from([1, 3]) } - ); - - Ok(()) - } - - #[test] - /// Begin read-only should not create a new version, instead using the - /// next one, but it should use the current active set. - fn begin_read_only() -> Result<()> { - let mut mvcc = Schedule::new("begin_read_only")?; - - // Start an initial read-only transaction, and make sure it's actually - // read-only. - let t1 = mvcc.begin_read_only()?; - assert_eq!( - t1.state(), - TransactionState { version: 1, read_only: true, active: HashSet::new() } - ); - assert_eq!(t1.set(b"foo", vec![1]), Err(Error::ReadOnly)); - assert_eq!(t1.delete(b"foo"), Err(Error::ReadOnly)); - - // Start a new read-write transaction, then another read-only - // transaction which should have it in its active set. t1 should not be - // in the active set, because it's read-only. - let t2 = mvcc.begin()?; - assert_eq!( - t2.state(), - TransactionState { version: 1, read_only: false, active: HashSet::new() } - ); - - let t3 = mvcc.begin_read_only()?; - assert_eq!( - t3.state(), - TransactionState { version: 2, read_only: true, active: HashSet::from([1]) } - ); - - Ok(()) - } - - #[test] - /// Begin as of should provide a read-only view of a historical version. - fn begin_as_of() -> Result<()> { - let mut mvcc = Schedule::new("begin_as_of")?; - - // Start a concurrent transaction that should be invisible. - let t1 = mvcc.begin()?; - t1.set(b"other", vec![1])?; - - // Write a couple of versions for a key. - let t2 = mvcc.begin()?; - t2.set(b"key", vec![2])?; - t2.commit()?; - - let t3 = mvcc.begin()?; - t3.set(b"key", vec![3])?; - - // Reading as of version 3 should only see key=2, because t1 and - // t3 haven't committed yet. - let t4 = mvcc.begin_as_of(3)?; - assert_eq!( - t4.state(), - TransactionState { version: 3, read_only: true, active: HashSet::from([1]) } - ); - assert_scan!(t4.scan(..)? => {b"key" => [2]}); - - // Writes should error. - assert_eq!(t4.set(b"foo", vec![1]), Err(Error::ReadOnly)); - assert_eq!(t4.delete(b"foo"), Err(Error::ReadOnly)); - - // Once we commit t1 and t3, neither the existing as of transaction nor - // a new one should see their writes, since versions must be stable. - t1.commit()?; - t3.commit()?; - - assert_scan!(t4.scan(..)? => {b"key" => [2]}); - - let t5 = mvcc.begin_as_of(3)?; - assert_scan!(t5.scan(..)? => {b"key" => [2]}); - - // Rolling back and committing read-only transactions is noops. - t4.rollback()?; - t5.commit()?; - - // Commit a new value. - let t6 = mvcc.begin()?; - t6.set(b"key", vec![4])?; - t6.commit()?; - - // A snapshot as of version 4 should see key=3 and other=1. - let t7 = mvcc.begin_as_of(4)?; - assert_eq!( - t7.state(), - TransactionState { version: 4, read_only: true, active: HashSet::new() } - ); - assert_scan!(t7.scan(..)? => {b"key" => [3], b"other" => [1]}); - - // Check that future versions are invalid, including the next. - assert_eq!( - mvcc.begin_as_of(5).err(), - Some(Error::InvalidInput("version 5 does not exist".into())) - ); - assert_eq!( - mvcc.begin_as_of(9).err(), - Some(Error::InvalidInput("version 9 does not exist".into())) - ); - - Ok(()) - } - - #[test] - /// Resume should resume a transaction with the same state. - fn resume() -> Result<()> { - let mut mvcc = Schedule::new("resume")?; - - // We first write a set of values that should be visible. - let t1 = mvcc.begin()?; - t1.set(b"a", vec![1])?; - t1.set(b"b", vec![1])?; - t1.commit()?; - - // We then start three transactions, of which we will resume t3. - // We commit t2 and t4's changes, which should not be visible, - // and write a change for t3 which should be visible. - let t2 = mvcc.begin()?; - let t3 = mvcc.begin()?; - let t4 = mvcc.begin()?; - - t2.set(b"a", vec![2])?; - t3.set(b"b", vec![3])?; - t4.set(b"c", vec![4])?; - - t2.commit()?; - t4.commit()?; - - // We now resume t3, who should only see its own changes. - let state = t3.state().clone(); - assert_eq!( - state, - TransactionState { version: 3, read_only: false, active: HashSet::from([2]) } - ); - drop(t3); - - let t5 = mvcc.resume(state.clone())?; - assert_eq!(t5.state(), state); - - assert_scan!(t5.scan(..)? => { - b"a" => [1], - b"b" => [3], - }); - - // A separate transaction should not see t3's changes. - let t6 = mvcc.begin()?; - assert_scan!(t6.scan(..)? => { - b"a" => [2], - b"b" => [1], // not 3 - b"c" => [4], - }); - t6.rollback()?; - - // Once t5 commits, a separate transaction should see its changes. - t5.commit()?; - - let t7 = mvcc.begin()?; - assert_scan!(t7.scan(..)? => { - b"a" => [2], - b"b" => [3], // now 3 - b"c" => [4], - }); - t7.rollback()?; - - // Resuming an inactive transaction should error. - assert_eq!( - mvcc.resume(state).err(), - Some(Error::InvalidInput("no active transaction at version 3".into())) - ); - - // It should also be possible to start a snapshot transaction in t3 - // and resume it. It should not see t3's writes, nor t2's. - let t8 = mvcc.begin_as_of(3)?; - assert_eq!( - t8.state(), - TransactionState { version: 3, read_only: true, active: HashSet::from([2]) } - ); - - assert_scan!(t8.scan(..)? => { - b"a" => [1], - b"b" => [1], - }); - - let state = t8.state().clone(); - drop(t8); - - let t9 = mvcc.resume(state.clone())?; - assert_eq!(t9.state(), state); - assert_scan!(t9.scan(..)? => { - b"a" => [1], - b"b" => [1], - }); - - Ok(()) - } - - #[test] - /// Deletes should work on both existing, missing, and deleted keys, be - /// idempotent. - fn delete() -> Result<()> { - let mut mvcc = Schedule::new("delete")?; - mvcc.setup(vec![(b"key", 1, Some(&[1])), (b"tombstone", 1, None)])?; - - let t1 = mvcc.begin()?; - t1.set(b"key", vec![2])?; - t1.delete(b"key")?; // delete uncommitted version - t1.delete(b"key")?; // idempotent - t1.delete(b"tombstone")?; - t1.delete(b"missing")?; - t1.commit()?; - - Ok(()) - } - - #[test] - /// Delete should return serialization errors both for uncommitted versions - /// (past and future), and future committed versions. - fn delete_conflict() -> Result<()> { - let mut mvcc = Schedule::new("delete_conflict")?; - - let t1 = mvcc.begin()?; - let t2 = mvcc.begin()?; - let t3 = mvcc.begin()?; - let t4 = mvcc.begin()?; - - t1.set(b"a", vec![1])?; - t3.set(b"c", vec![3])?; - t4.set(b"d", vec![4])?; - t4.commit()?; - - assert_eq!(t2.delete(b"a"), Err(Error::Serialization)); // past uncommitted - assert_eq!(t2.delete(b"c"), Err(Error::Serialization)); // future uncommitted - assert_eq!(t2.delete(b"d"), Err(Error::Serialization)); // future committed - - Ok(()) - } - - #[test] - /// Get should return the correct latest value. - fn get() -> Result<()> { - let mut mvcc = Schedule::new("get")?; - mvcc.setup(vec![ - (b"key", 1, Some(&[1])), - (b"updated", 1, Some(&[1])), - (b"updated", 2, Some(&[2])), - (b"deleted", 1, Some(&[1])), - (b"deleted", 2, None), - (b"tombstone", 1, None), - ])?; - - let t1 = mvcc.begin_read_only()?; - assert_eq!(t1.get(b"key")?, Some(vec![1])); - assert_eq!(t1.get(b"updated")?, Some(vec![2])); - assert_eq!(t1.get(b"deleted")?, None); - assert_eq!(t1.get(b"tombstone")?, None); - - Ok(()) - } - #[test] - /// Get should be isolated from future and uncommitted transactions. - fn get_isolation() -> Result<()> { - let mut mvcc = Schedule::new("get_isolation")?; - - let t1 = mvcc.begin()?; - t1.set(b"a", vec![1])?; - t1.set(b"b", vec![1])?; - t1.set(b"d", vec![1])?; - t1.set(b"e", vec![1])?; - t1.commit()?; - - let t2 = mvcc.begin()?; - t2.set(b"a", vec![2])?; - t2.delete(b"b")?; - t2.set(b"c", vec![2])?; - - let t3 = mvcc.begin_read_only()?; - - let t4 = mvcc.begin()?; - t4.set(b"d", vec![3])?; - t4.delete(b"e")?; - t4.set(b"f", vec![3])?; - t4.commit()?; - - assert_eq!(t3.get(b"a")?, Some(vec![1])); // uncommitted update - assert_eq!(t3.get(b"b")?, Some(vec![1])); // uncommitted delete - assert_eq!(t3.get(b"c")?, None); // uncommitted write - assert_eq!(t3.get(b"d")?, Some(vec![1])); // future update - assert_eq!(t3.get(b"e")?, Some(vec![1])); // future delete - assert_eq!(t3.get(b"f")?, None); // future write - - Ok(()) - } - - #[test] - /// Scans should use correct key and time bounds. Sets up an initial data - /// set as follows, and asserts results via the golden file. - /// - /// T - /// 4 x ba,4 - /// 3 x a,3 b,3 x - /// 2 x ba,2 bb,2 bc,2 - /// 1 0,1 a,1 x c,1 - /// B a b ba bb bc c - fn scan() -> Result<()> { - let mut mvcc = Schedule::new("scan")?; - mvcc.setup(vec![ - (b"B", 1, Some(&[0, 1])), - (b"B", 3, None), - (b"a", 1, Some(&[0x0a, 1])), - (b"a", 2, None), - (b"a", 3, Some(&[0x0a, 3])), - (b"b", 1, None), - (b"b", 3, Some(&[0x0b, 3])), - (b"b", 4, None), - (b"ba", 2, Some(&[0xba, 2])), - (b"ba", 4, Some(&[0xba, 4])), - (b"bb", 2, Some(&[0xbb, 2])), - (b"bb", 3, None), - (b"bc", 2, Some(&[0xbc, 2])), - (b"c", 1, Some(&[0x0c, 1])), - ])?; - - // Full scans at all timestamps. - for version in 1..5 { - let txn = match version { - 5 => mvcc.begin_read_only()?, - v => mvcc.begin_as_of(v)?, - }; - let mut scan = txn.scan(..)?; // see golden master - assert_scan_invariants(&mut scan)?; + /// Formats a key/value pair, or None if the value does not exist. + /// TODO: share with engine::test::Runner. + fn format_key_value(key: &[u8], value: Option<&[u8]>) -> String { + format!( + "{} → {}", + Self::format_bytes(key), + value.map(Self::format_bytes).unwrap_or("None".to_string()) + ) } - // All bounded scans around ba-bc at version 3. - let txn = mvcc.begin_as_of(3)?; - let starts = - [Bound::Unbounded, Bound::Included(b"ba".to_vec()), Bound::Excluded(b"ba".to_vec())]; - let ends = - [Bound::Unbounded, Bound::Included(b"bc".to_vec()), Bound::Excluded(b"bc".to_vec())]; - for start in &starts { - for end in &ends { - let mut scan = txn.scan((start.to_owned(), end.to_owned()))?; // see golden master - assert_scan_invariants(&mut scan)?; + /// Parses an binary key range, using Rust range syntax. + /// TODO: share with engine::test:Runner. + fn parse_key_range( + s: &str, + ) -> StdResult>, Box> { + let mut bound = (Bound::>::Unbounded, Bound::>::Unbounded); + let re = Regex::new(r"^(\S+)?\.\.(=)?(\S+)?").expect("invalid regex"); + let groups = re.captures(s).ok_or_else(|| format!("invalid range {s}"))?; + if let Some(start) = groups.get(1) { + bound.0 = Bound::Included(Self::decode_binary(start.as_str())); } + if let Some(end) = groups.get(3) { + let end = Self::decode_binary(end.as_str()); + if groups.get(2).is_some() { + bound.1 = Bound::Included(end) + } else { + bound.1 = Bound::Excluded(end) + } + } + Ok(bound) } - Ok(()) - } + /// Fetches the named transaction from a command prefix. + fn get_txn( + &mut self, + prefix: &Option, + ) -> StdResult<&'_ mut Transaction, Box> { + let name = Self::txn_name(prefix)?; + self.txns.get_mut(name).ok_or(format!("unknown txn {name}").into()) + } - #[test] - /// Prefix scans should use correct key and time bounds. Sets up an initial - /// data set as follows, and asserts results via the golden file. - /// - /// T - /// 4 x ba,4 - /// 3 x a,3 b,3 x - /// 2 x ba,2 bb,2 bc,2 - /// 1 0,1 a,1 x c,1 - /// B a b ba bb bc c - fn scan_prefix() -> Result<()> { - let mut mvcc = Schedule::new("scan_prefix")?; - mvcc.setup(vec![ - (b"B", 1, Some(&[0, 1])), - (b"B", 3, None), - (b"a", 1, Some(&[0x0a, 1])), - (b"a", 2, None), - (b"a", 3, Some(&[0x0a, 3])), - (b"b", 1, None), - (b"b", 3, Some(&[0x0b, 3])), - (b"b", 4, None), - (b"ba", 2, Some(&[0xba, 2])), - (b"ba", 4, Some(&[0xba, 4])), - (b"bb", 2, Some(&[0xbb, 2])), - (b"bb", 3, None), - (b"bc", 2, Some(&[0xbc, 2])), - (b"c", 1, Some(&[0x0c, 1])), - ])?; - - // Full scans at all timestamps. - for version in 1..5 { - let txn = match version { - 5 => mvcc.begin_read_only()?, - v => mvcc.begin_as_of(v)?, - }; - let mut scan = txn.scan_prefix(&[])?; // see golden master - assert_scan_invariants(&mut scan)?; + /// Fetches the txn name from a command prefix, or errors. + fn txn_name(prefix: &Option) -> StdResult<&str, Box> { + prefix.as_deref().ok_or("no txn name".into()) } - // All prefixes at version 3 and version 4. - for version in 3..=4 { - let txn = mvcc.begin_as_of(version)?; - for prefix in [b"B" as &[u8], b"a", b"b", b"ba", b"bb", b"bbb", b"bc", b"c", b"d"] { - let mut scan = txn.scan_prefix(prefix)?; // see golden master - assert_scan_invariants(&mut scan)?; + /// Errors if a txn prefix is given. + fn no_txn(command: &goldenscript::Command) -> StdResult<(), Box> { + if let Some(name) = &command.prefix { + return Err(format!("can't run {} with txn {name}", command.name).into()); } + Ok(()) } - - Ok(()) - } - - #[test] - /// Scan should be isolated from future and uncommitted transactions. - fn scan_isolation() -> Result<()> { - let mut mvcc = Schedule::new("scan_isolation")?; - - let t1 = mvcc.begin()?; - t1.set(b"a", vec![1])?; - t1.set(b"b", vec![1])?; - t1.set(b"d", vec![1])?; - t1.set(b"e", vec![1])?; - t1.commit()?; - - let t2 = mvcc.begin()?; - t2.set(b"a", vec![2])?; - t2.delete(b"b")?; - t2.set(b"c", vec![2])?; - - let t3 = mvcc.begin_read_only()?; - - let t4 = mvcc.begin()?; - t4.set(b"d", vec![3])?; - t4.delete(b"e")?; - t4.set(b"f", vec![3])?; - t4.commit()?; - - assert_scan!(t3.scan(..)? => { - b"a" => [1], // uncommitted update - b"b" => [1], // uncommitted delete - // b"c" is uncommitted write - b"d" => [1], // future update - b"e" => [1], // future delete - // b"f" is future write - }); - - Ok(()) - } - - #[test] - /// Tests that the key encoding is resistant to key/version overlap. - /// For example, a naïve concatenation of keys and versions would - /// produce incorrect ordering in this case: - /// - // 00|00 00 00 00 00 00 00 01 - // 00 00 00 00 00 00 00 00 02|00 00 00 00 00 00 00 02 - // 00|00 00 00 00 00 00 00 03 - fn scan_key_version_encoding() -> Result<()> { - let mut mvcc = Schedule::new("scan_key_version_encoding")?; - - let t1 = mvcc.begin()?; - t1.set(&[0], vec![1])?; - t1.commit()?; - - let t2 = mvcc.begin()?; - t2.set(&[0], vec![2])?; - t2.set(&[0, 0, 0, 0, 0, 0, 0, 0, 2], vec![2])?; - t2.commit()?; - - let t3 = mvcc.begin()?; - t3.set(&[0], vec![3])?; - t3.commit()?; - - let t4 = mvcc.begin_read_only()?; - assert_scan!(t4.scan(..)? => { - b"\x00" => [3], - b"\x00\x00\x00\x00\x00\x00\x00\x00\x02" => [2], - }); - Ok(()) - } - - #[test] - /// Sets should work on both existing, missing, and deleted keys, and be - /// idempotent. - fn set() -> Result<()> { - let mut mvcc = Schedule::new("set")?; - mvcc.setup(vec![(b"key", 1, Some(&[1])), (b"tombstone", 1, None)])?; - - let t1 = mvcc.begin()?; - t1.set(b"key", vec![2])?; // update - t1.set(b"tombstone", vec![2])?; // update tombstone - t1.set(b"new", vec![1])?; // new write - t1.set(b"new", vec![1])?; // idempotent - t1.set(b"new", vec![2])?; // update own - t1.commit()?; - - Ok(()) - } - - #[test] - /// Set should return serialization errors both for uncommitted versions - /// (past and future), and future committed versions. - fn set_conflict() -> Result<()> { - let mut mvcc = Schedule::new("set_conflict")?; - - let t1 = mvcc.begin()?; - let t2 = mvcc.begin()?; - let t3 = mvcc.begin()?; - let t4 = mvcc.begin()?; - - t1.set(b"a", vec![1])?; - t3.set(b"c", vec![3])?; - t4.set(b"d", vec![4])?; - t4.commit()?; - - assert_eq!(t2.set(b"a", vec![2]), Err(Error::Serialization)); // past uncommitted - assert_eq!(t2.set(b"c", vec![2]), Err(Error::Serialization)); // future uncommitted - assert_eq!(t2.set(b"d", vec![2]), Err(Error::Serialization)); // future committed - - Ok(()) - } - - #[test] - /// Tests that transaction rollback properly rolls back uncommitted writes, - /// allowing other concurrent transactions to write the keys. - fn rollback() -> Result<()> { - let mut mvcc = Schedule::new("rollback")?; - mvcc.setup(vec![ - (b"a", 1, Some(&[0])), - (b"b", 1, Some(&[0])), - (b"c", 1, Some(&[0])), - (b"d", 1, Some(&[0])), - ])?; - - // t2 will be rolled back. t1 and t3 are concurrent transactions. - let t1 = mvcc.begin()?; - let t2 = mvcc.begin()?; - let t3 = mvcc.begin()?; - - t1.set(b"a", vec![1])?; - t2.set(b"b", vec![2])?; - t2.delete(b"c")?; - t3.set(b"d", vec![3])?; - - // Both t1 and t3 will get serialization errors with t2. - assert_eq!(t1.set(b"b", vec![1]), Err(Error::Serialization)); - assert_eq!(t3.set(b"c", vec![3]), Err(Error::Serialization)); - - // When t2 is rolled back, none of its writes will be visible, and t1 - // and t3 can perform their writes and successfully commit. - t2.rollback()?; - - let t4 = mvcc.begin_read_only()?; - assert_scan!(t4.scan(..)? => { - b"a" => [0], - b"b" => [0], - b"c" => [0], - b"d" => [0], - }); - - t1.set(b"b", vec![1])?; - t3.set(b"c", vec![3])?; - t1.commit()?; - t3.commit()?; - - let t5 = mvcc.begin_read_only()?; - assert_scan!(t5.scan(..)? => { - b"a" => [1], - b"b" => [1], - b"c" => [3], - b"d" => [3], - }); - - Ok(()) - } - - #[test] - // A dirty write is when t2 overwrites an uncommitted value written by t1. - // Snapshot isolation prevents this. - fn anomaly_dirty_write() -> Result<()> { - let mut mvcc = Schedule::new("anomaly_dirty_write")?; - - let t1 = mvcc.begin()?; - t1.set(b"key", vec![1])?; - - let t2 = mvcc.begin()?; - assert_eq!(t2.set(b"key", vec![2]), Err(Error::Serialization)); - - Ok(()) - } - - #[test] - // A dirty read is when t2 can read an uncommitted value set by t1. - // Snapshot isolation prevents this. - fn anomaly_dirty_read() -> Result<()> { - let mut mvcc = Schedule::new("anomaly_dirty_read")?; - - let t1 = mvcc.begin()?; - t1.set(b"key", vec![1])?; - - let t2 = mvcc.begin()?; - assert_eq!(t2.get(b"key")?, None); - - Ok(()) - } - - #[test] - // A lost update is when t1 and t2 both read a value and update it, where - // t2's update replaces t1. Snapshot isolation prevents this. - fn anomaly_lost_update() -> Result<()> { - let mut mvcc = Schedule::new("anomaly_lost_update")?; - mvcc.setup(vec![(b"key", 1, Some(&[0]))])?; - - let t1 = mvcc.begin()?; - let t2 = mvcc.begin()?; - - t1.get(b"key")?; - t2.get(b"key")?; - - t1.set(b"key", vec![1])?; - assert_eq!(t2.set(b"key", vec![2]), Err(Error::Serialization)); - t1.commit()?; - - Ok(()) - } - - #[test] - // A fuzzy (or unrepeatable) read is when t2 sees a value change after t1 - // updates it. Snapshot isolation prevents this. - fn anomaly_fuzzy_read() -> Result<()> { - let mut mvcc = Schedule::new("anomaly_fuzzy_read")?; - mvcc.setup(vec![(b"key", 1, Some(&[0]))])?; - - let t1 = mvcc.begin()?; - let t2 = mvcc.begin()?; - - assert_eq!(t2.get(b"key")?, Some(vec![0])); - t1.set(b"key", b"t1".to_vec())?; - t1.commit()?; - assert_eq!(t2.get(b"key")?, Some(vec![0])); - - Ok(()) - } - - #[test] - // Read skew is when t1 reads a and b, but t2 modifies b in between the - // reads. Snapshot isolation prevents this. - fn anomaly_read_skew() -> Result<()> { - let mut mvcc = Schedule::new("anomaly_read_skew")?; - mvcc.setup(vec![(b"a", 1, Some(&[0])), (b"b", 1, Some(&[0]))])?; - - let t1 = mvcc.begin()?; - let t2 = mvcc.begin()?; - - assert_eq!(t1.get(b"a")?, Some(vec![0])); - t2.set(b"a", vec![2])?; - t2.set(b"b", vec![2])?; - t2.commit()?; - assert_eq!(t1.get(b"b")?, Some(vec![0])); - - Ok(()) - } - - #[test] - // A phantom read is when t1 reads entries matching some predicate, but a - // modification by t2 changes which entries that match the predicate such - // that a later read by t1 returns them. Snapshot isolation prevents this. - // - // We use a prefix scan as our predicate. - fn anomaly_phantom_read() -> Result<()> { - let mut mvcc = Schedule::new("anomaly_phantom_read")?; - mvcc.setup(vec![(b"a", 1, Some(&[0])), (b"ba", 1, Some(&[0])), (b"bb", 1, Some(&[0]))])?; - - let t1 = mvcc.begin()?; - let t2 = mvcc.begin()?; - - assert_scan!(t1.scan_prefix(b"b")? => { - b"ba" => [0], - b"bb" => [0], - }); - - t2.delete(b"ba")?; - t2.set(b"bc", vec![2])?; - t2.commit()?; - - assert_scan!(t1.scan_prefix(b"b")? => { - b"ba" => [0], - b"bb" => [0], - }); - - Ok(()) - } - - #[test] - // Write skew is when t1 reads a and writes it to b while t2 reads b and - // writes it to a. Snapshot isolation DOES NOT prevent this, which is - // expected, so we assert the current behavior. Fixing this requires - // implementing serializable snapshot isolation. - fn anomaly_write_skew() -> Result<()> { - let mut mvcc = Schedule::new("anomaly_write_skew")?; - mvcc.setup(vec![(b"a", 1, Some(&[1])), (b"b", 1, Some(&[2]))])?; - - let t1 = mvcc.begin()?; - let t2 = mvcc.begin()?; - - assert_eq!(t1.get(b"a")?, Some(vec![1])); - assert_eq!(t2.get(b"b")?, Some(vec![2])); - - t1.set(b"b", vec![1])?; - t2.set(b"a", vec![2])?; - - t1.commit()?; - t2.commit()?; - - Ok(()) - } - - #[test] - /// Tests unversioned key/value pairs, via set/get_unversioned(). - fn unversioned() -> Result<()> { - let mut mvcc = Schedule::new("unversioned")?; - - // Interleave versioned and unversioned writes. - mvcc.set_unversioned(b"a", vec![0])?; - - let t1 = mvcc.begin()?; - t1.set(b"a", vec![1])?; - t1.set(b"b", vec![1])?; - t1.set(b"c", vec![1])?; - t1.commit()?; - - mvcc.set_unversioned(b"b", vec![0])?; - mvcc.set_unversioned(b"d", vec![0])?; - - // Scans should not see the unversioned writes. - let t2 = mvcc.begin_read_only()?; - assert_scan!(t2.scan(..)? => { - b"a" => [1], - b"b" => [1], - b"c" => [1], - }); - - // Unversioned gets should not see MVCC writes. - assert_eq!(mvcc.get_unversioned(b"a")?, Some(vec![0])); - assert_eq!(mvcc.get_unversioned(b"b")?, Some(vec![0])); - assert_eq!(mvcc.get_unversioned(b"c")?, None); - assert_eq!(mvcc.get_unversioned(b"d")?, Some(vec![0])); - - // Replacing an unversioned key should be fine. - mvcc.set_unversioned(b"a", vec![1])?; - assert_eq!(mvcc.get_unversioned(b"a")?, Some(vec![1])); - - Ok(()) } } diff --git a/src/storage/testscripts/engine/keys b/src/storage/testscripts/engine/keys index 3701c9e68..e69de29bb 100644 --- a/src/storage/testscripts/engine/keys +++ b/src/storage/testscripts/engine/keys @@ -1,61 +0,0 @@ -# Tests various keys. - -# Keys are case-sensitive. -set a=1 -get a -get A ---- -a → 1 -A → None - -set A=2 -get a -get A ---- -a → 1 -A → 2 - -delete a -delete A -scan ---- -ok - -# Empty keys and values are valid. -set ""="" -get "" -scan -delete "" ---- - → - → - -scan ---- -ok - -# NUL keys and values are valid. -set "\0"="\0" -get "\0" -scan -delete "\0" ---- -\x00 → \x00 -\x00 → \x00 - -scan ---- -ok - -# Unicode keys and values work, but are shown as raw UTF-8 bytes. -set "👋"="👋" -get "👋" -scan -delete "👋" ---- -\xf0\x9f\x91\x8b → \xf0\x9f\x91\x8b -\xf0\x9f\x91\x8b → \xf0\x9f\x91\x8b - -scan ---- -ok diff --git a/src/storage/testscripts/mvcc/anomaly_dirty_read b/src/storage/testscripts/mvcc/anomaly_dirty_read new file mode 100644 index 000000000..53ab27293 --- /dev/null +++ b/src/storage/testscripts/mvcc/anomaly_dirty_read @@ -0,0 +1,12 @@ +# A dirty read is when t2 can read an uncommitted value set by t1.Snapshot +# isolation prevents this. + +t1: begin +t1: set key=1 +--- +ok + +t2: begin +t2: get key +--- +t2: key → None diff --git a/src/storage/testscripts/mvcc/anomaly_dirty_write b/src/storage/testscripts/mvcc/anomaly_dirty_write new file mode 100644 index 000000000..e9d7a41d2 --- /dev/null +++ b/src/storage/testscripts/mvcc/anomaly_dirty_write @@ -0,0 +1,12 @@ +# A dirty write is when t2 overwrites an uncommitted value written by t1. +# Snapshot isolation prevents this. + +t1: begin +t1: set key=1 +--- +ok + +t2: begin +t2: !set key=2 +--- +t2: Error: serialization failure, retry transaction diff --git a/src/storage/testscripts/mvcc/anomaly_fuzzy_read b/src/storage/testscripts/mvcc/anomaly_fuzzy_read new file mode 100644 index 000000000..e7d8fff73 --- /dev/null +++ b/src/storage/testscripts/mvcc/anomaly_fuzzy_read @@ -0,0 +1,25 @@ +# A fuzzy (or unrepeatable) read is when t2 sees a value change after t1 +# updates it. Snapshot isolation prevents this. + +# Set up some initial data. +import key=0 +--- +ok + +t1: begin +t2: begin +--- +ok + +t2: get key +--- +t2: key → 0 + +t1: set key=1 +t1: commit +--- +ok + +t2: get key +--- +t2: key → 0 diff --git a/src/storage/testscripts/mvcc/anomaly_lost_update b/src/storage/testscripts/mvcc/anomaly_lost_update new file mode 100644 index 000000000..ed0efa20b --- /dev/null +++ b/src/storage/testscripts/mvcc/anomaly_lost_update @@ -0,0 +1,17 @@ +# A lost update is when t1 and t2 both read a value and update it, where +# t2's update replaces t1. Snapshot isolation prevents this. + +t1: begin +t1: get key +--- +t1: key → None + +t2: begin +t2: get key +--- +t2: key → None + +t1: set key=1 +t2: !set key=2 +--- +t2: Error: serialization failure, retry transaction diff --git a/src/storage/testscripts/mvcc/anomaly_phantom_read b/src/storage/testscripts/mvcc/anomaly_phantom_read new file mode 100644 index 000000000..89ebce96d --- /dev/null +++ b/src/storage/testscripts/mvcc/anomaly_phantom_read @@ -0,0 +1,31 @@ +# A phantom read is when t1 reads entries matching some predicate, but a +# modification by t2 changes which entries match the predicate such that a later +# read by t1 returns them. Snapshot isolation prevents this. +# +# We use a prefix scan as our predicate. + +# Write some initial data. +import a=0 ba=0 bb=0 +--- +ok + +t1: begin +t2: begin +--- +ok + +t1: scan_prefix b +--- +t1: ba → 0 +t1: bb → 0 + +t2: delete ba +t2: set bc=2 +t2: commit +--- +ok + +t1: scan_prefix b +--- +t1: ba → 0 +t1: bb → 0 diff --git a/src/storage/testscripts/mvcc/anomaly_read_skew b/src/storage/testscripts/mvcc/anomaly_read_skew new file mode 100644 index 000000000..8a003c116 --- /dev/null +++ b/src/storage/testscripts/mvcc/anomaly_read_skew @@ -0,0 +1,26 @@ +# Read skew is when t1 reads a and b, but t2 modifies b in between the +# reads. Snapshot isolation prevents this. + +# Set up some initial data. +import a=0 b=0 +--- +ok + +t1: begin +t2: begin +--- +ok + +t1: get a +--- +t1: a → 0 + +t2: set a=2 +t2: set b=2 +t2: commit +--- +ok + +t1: get b +--- +t1: b → 0 diff --git a/src/storage/testscripts/mvcc/anomaly_write_skew b/src/storage/testscripts/mvcc/anomaly_write_skew new file mode 100644 index 000000000..2fb2f0745 --- /dev/null +++ b/src/storage/testscripts/mvcc/anomaly_write_skew @@ -0,0 +1,36 @@ +# Write skew is when t1 reads a and writes it to b while t2 reads b and writes +# it to a. Snapshot isolation does prevent this, which is expected, so we assert +# the anomalous behavior. Fixing this would require implementing serializable +# snapshot isolation. + +# Write some initial data. +import a=1 b=2 +--- +ok + +t1: begin +t2: begin +--- +ok + +t1: get a +t2: get b +--- +t1: a → 1 +t2: b → 2 + +t1: set b=1 +t2: set a=2 +--- +ok + +t1: commit +t2: commit +--- +ok + +t3: begin readonly +t3: scan +--- +t3: a → 2 +t3: b → 1 diff --git a/src/storage/testscripts/mvcc/begin b/src/storage/testscripts/mvcc/begin new file mode 100644 index 000000000..fd389d182 --- /dev/null +++ b/src/storage/testscripts/mvcc/begin @@ -0,0 +1,49 @@ +# Begin creates new transactions at increasing versions, with concurrent +# transactions in their active sets. + +# Start t1 at v1, with an empty active set. Dump raw engine operations to ensure +# it bumps the next version and registers itself as active. +t1: begin [ops] +t1: state +--- +t1: engine set NextVersion → 2 +t1: engine set TxnActive(1) → [] +t1: v1 rw active={} + +# t2 should have v2, and t1 in its active set. It should persist a snapshot of +# its active set. +t2: begin [ops] +t2: state +--- +t2: engine set NextVersion → 3 +t2: engine set TxnActiveSnapshot(2) → {1} +t2: engine set TxnActive(2) → [] +t2: v2 rw active={1} + +# Similarly for t3. +t3: begin [ops] +t3: state +--- +t3: engine set NextVersion → 4 +t3: engine set TxnActiveSnapshot(3) → {1,2} +t3: engine set TxnActive(3) → [] +t3: v3 rw active={1,2} + +# Now, commit t2, which unregisters it. +t2: commit [ops] +--- +t2: engine delete TxnActive(2) + +# It should still be in t3's active set. +t3: state +--- +t3: v3 rw active={1,2} + +# But not in a new t4. +t4: begin [ops] +t4: state +--- +t4: engine set NextVersion → 5 +t4: engine set TxnActiveSnapshot(4) → {1,3} +t4: engine set TxnActive(4) → [] +t4: v4 rw active={1,3} diff --git a/src/storage/testscripts/mvcc/begin_as_of b/src/storage/testscripts/mvcc/begin_as_of new file mode 100644 index 000000000..9832e4ab9 --- /dev/null +++ b/src/storage/testscripts/mvcc/begin_as_of @@ -0,0 +1,108 @@ +# Begin read-only as-of should provide a view of a historical version. + +# Start a concurrent transaction at v1 that should be invisible. +t1: begin +t1: set other=1 +--- +ok + +# Write and commit a key at v2. +t2: begin +t2: set key=2 +t2: commit +--- +ok + +# Write another version at v3, but don't commit it yet. +t3: begin +t3: set key=3 +--- +ok + +dump +--- +NextVersion → 4 +TxnActive(1) → [] +TxnActive(3) → [] +TxnActiveSnapshot(2) → {1} +TxnActiveSnapshot(3) → {1} +TxnWrite(1, "other") → [] +TxnWrite(3, "key") → [] +Version("key", 2) → "2" +Version("key", 3) → "3" +Version("other", 1) → "1" + +# Start a read-only transaction as-of version 3. It should only see key=2 +# because t1 and t3 haven't committed yet. It shouldn't write any state. +t4: begin readonly as_of=3 [ops] +t4: state +--- +t4: v3 ro active={1} + +t4: scan +--- +t4: key → 2 + +# Writes should error. +t4: !set foo=bar +t4: !delete foo +--- +t4: Error: read-only transaction +t4: Error: read-only transaction + +# t1 and t3 commit. Their writes still shouldn't be visible to t4, since +# versions must be stable. +t1: commit +t3: commit +--- +ok + +t4: scan +--- +t4: key → 2 + +# A new transaction t5 running as-of v3 shouldn't see them either. +t5: begin readonly as_of=3 +t5: state +--- +t5: v3 ro active={1} + +t5: scan +--- +t5: key → 2 + +# Committing and rolling back readonly txns is a noop. +t4: commit [ops] +t5: rollback [ops] +--- +ok + +# Commit a new value at version 4. +t6: begin +t6: state +t6: set key=4 +t6: commit +--- +t6: v4 rw active={} + +# A snapshot at version 4 should see the old writes, but not those of t6 at v4 +# because as_of is at the start of the version. +t7: begin readonly as_of=4 +t7: scan +--- +t7: key → 3 +t7: other → 1 + +# Running as_of future versions should error, including the next version. +t8: !begin readonly as_of=5 +t8: !begin readonly as_of=9 +--- +t8: Error: invalid input: version 5 does not exist +t8: Error: invalid input: version 9 does not exist + +# Version 0 works though, but doesn't show anything. +t8: begin readonly as_of=0 +t8: state +t8: scan +--- +t8: v0 ro active={} diff --git a/src/storage/testscripts/mvcc/begin_readonly b/src/storage/testscripts/mvcc/begin_readonly new file mode 100644 index 000000000..19243adc6 --- /dev/null +++ b/src/storage/testscripts/mvcc/begin_readonly @@ -0,0 +1,40 @@ +# Begin read-only should not create a new version, it should run in the next +# version but using the current active set. + +# Start t1 read-only at v1. It shouldn't bump the version nor write any state. +t1: begin readonly [ops] +t1: state +--- +t1: v1 ro active={} + +# Writes should error. +t1: !set foo=bar +t1: !delete foo +--- +t1: Error: read-only transaction +t1: Error: read-only transaction + +# Start a new read-write transaction, then another read-only transaction which +# should have it in its active set. t1 should not be in the active set, because +# it's read-only. +t2: begin +t2: state +--- +t2: v1 rw active={} + +t3: begin readonly [ops] +t3: state +--- +t3: v2 ro active={1} + +# t2 also shouldn't be in t1's active set. Visibility for t2's writes are +# handled explicitly for t1. +t2: state +--- +t2: v1 rw active={} + +# Both committing and rolling back read-only transactions are noops. +t1: commit [ops] +t3: commit [ops] +--- +ok diff --git a/src/storage/testscripts/mvcc/delete b/src/storage/testscripts/mvcc/delete new file mode 100644 index 000000000..69b00a033 --- /dev/null +++ b/src/storage/testscripts/mvcc/delete @@ -0,0 +1,62 @@ +# Deletes should work on both existing, missing, and deleted keys. + +import 1 a=1 b=1 x= +--- +ok + +# Delete an existing, missing, and deleted key. Show engine operations. +t1: begin +t1: delete a m x [ops] +--- +t1: engine set TxnWrite(2, "a") → [] +t1: engine set Version("a", 2) → None +t1: engine set TxnWrite(2, "m") → [] +t1: engine set Version("m", 2) → None +t1: engine set TxnWrite(2, "x") → [] +t1: engine set Version("x", 2) → None + +t1: scan +--- +t1: b → 1 + +# Set and then delete a key, both an existing an missing one. +t1: set b=2 c=2 [ops] +--- +t1: engine set TxnWrite(2, "b") → [] +t1: engine set Version("b", 2) → "2" +t1: engine set TxnWrite(2, "c") → [] +t1: engine set Version("c", 2) → "2" + +t1: scan +--- +t1: b → 2 +t1: c → 2 + +t1: delete b c [ops] +--- +t1: engine set TxnWrite(2, "b") → [] +t1: engine set Version("b", 2) → None +t1: engine set TxnWrite(2, "c") → [] +t1: engine set Version("c", 2) → None + +t1: scan +--- +ok + +dump +--- +NextVersion → 3 +TxnActive(2) → [] +TxnWrite(2, "a") → [] +TxnWrite(2, "b") → [] +TxnWrite(2, "c") → [] +TxnWrite(2, "m") → [] +TxnWrite(2, "x") → [] +Version("a", 1) → "1" +Version("a", 2) → None +Version("b", 1) → "1" +Version("b", 2) → None +Version("c", 2) → None +Version("m", 2) → None +Version("x", 1) → None +Version("x", 2) → None diff --git a/src/storage/testscripts/mvcc/delete_conflict b/src/storage/testscripts/mvcc/delete_conflict new file mode 100644 index 000000000..573b4eaa8 --- /dev/null +++ b/src/storage/testscripts/mvcc/delete_conflict @@ -0,0 +1,39 @@ +# Delete should return serialization errors both for uncommitted versions +# (past and future), and future committed versions. + +t1: begin +t2: begin +t3: begin +t4: begin +--- +ok + +t1: set a=1 +t3: set c=3 +t4: set d=4 +t4: commit +--- +ok + +t2: !delete a # past uncommitted +t2: !delete c # future uncommitted +t2: !delete d # future committed +--- +t2: Error: serialization failure, retry transaction +t2: Error: serialization failure, retry transaction +t2: Error: serialization failure, retry transaction + +dump +--- +NextVersion → 5 +TxnActive(1) → [] +TxnActive(2) → [] +TxnActive(3) → [] +TxnActiveSnapshot(2) → {1} +TxnActiveSnapshot(3) → {1,2} +TxnActiveSnapshot(4) → {1,2,3} +TxnWrite(1, "a") → [] +TxnWrite(3, "c") → [] +Version("a", 1) → "1" +Version("c", 3) → "3" +Version("d", 4) → "4" diff --git a/src/storage/testscripts/mvcc/get b/src/storage/testscripts/mvcc/get new file mode 100644 index 000000000..a49d77a58 --- /dev/null +++ b/src/storage/testscripts/mvcc/get @@ -0,0 +1,21 @@ +# Get should return the correct latest value. + +import 1 key=1 updated=1 deleted=1 tombstone= +import 2 updated=2 deleted= +--- +ok + +t1: begin readonly +t1: scan +--- +t1: key → 1 +t1: updated → 2 + +# Get results should mirror scan. +t1: get key updated deleted tombstone missing +--- +t1: key → 1 +t1: updated → 2 +t1: deleted → None +t1: tombstone → None +t1: missing → None diff --git a/src/storage/testscripts/mvcc/get_isolation b/src/storage/testscripts/mvcc/get_isolation new file mode 100644 index 000000000..4edbf9fba --- /dev/null +++ b/src/storage/testscripts/mvcc/get_isolation @@ -0,0 +1,46 @@ +# Get should be isolated from concurrent transactions. + +# Past committed. +t1: begin +t1: set a=1 b=1 d=1 e=1 +t1: commit +--- +ok + +# Past uncommitted. +t2: begin +t2: set a=2 c=2 +t2: delete b +--- +ok + +# Begin the read transaction. +t3: begin readonly +--- +ok + +# Future committed. +t4: begin +t4: set d=3 f=3 +t4: delete e +t4: commit +--- +ok + +# Future uncommitted. +t5: begin +t5: set d=4 g=4 +t5: delete f +--- +ok + +# Get each key. +t3: get a b c d e f g +--- +t3: a → 1 +t3: b → 1 +t3: c → None +t3: d → 1 +t3: e → 1 +t3: f → None +t3: g → None diff --git a/src/storage/testscripts/mvcc/resume b/src/storage/testscripts/mvcc/resume new file mode 100644 index 000000000..6a772da30 --- /dev/null +++ b/src/storage/testscripts/mvcc/resume @@ -0,0 +1,89 @@ +# Resume should resume a transaction with the same state. + +# Commit some visible values. +t1: begin +t1: set a=1 b=1 +t1: commit +--- +ok + +# We then start three transactions, of which we will resume t3. We commit t2 +# and t4's changes, which should not be visible, and write a change for t3 which +# should be visible. +t2: begin +t3: begin +t4: begin +--- +ok + +t2: set a=2 +t3: set b=3 +t4: set c=4 +t2: commit +t4: commit +--- +ok + +# We now resume t3 as t5. +t3: state +--- +t3: v3 rw active={2} + +t5: resume '{"version":3, "read_only":false, "active":[2]}' +t5: state +--- +t5: v3 rw active={2} + +# t5 can see its own changes, but not the others. +t5: scan +--- +t5: a → 1 +t5: b → 3 + +# A new transaction should not see t3/5's uncommitted changes. +t6: begin +t6: scan +--- +t6: a → 2 +t6: b → 1 +t6: c → 4 + +# Once t5 commits, a separate transaction should see its changes. +t5: commit [ops] +--- +t5: engine delete TxnWrite(3, "b") +t5: engine delete TxnActive(3) + +t7: begin +t7: scan +--- +t7: a → 2 +t7: b → 3 +t7: c → 4 + +# Resuming a committed transaction should error. +t8: !resume '{"version":3, "read_only":false, "active":[2]}' +--- +t8: Error: invalid input: no active transaction at version 3 + +# It should also be possible to start a snapshot transaction in t3 and resume +# it. It should not see t3's writes, nor t2's. +t8: begin readonly as_of=3 +t8: state +--- +t8: v3 ro active={2} + +t8: scan +--- +t8: a → 1 +t8: b → 1 + +t9: resume '{"version":3, "read_only":true, "active":[2]}' +t9: state +--- +t9: v3 ro active={2} + +t9: scan +--- +t9: a → 1 +t9: b → 1 diff --git a/src/storage/testscripts/mvcc/rollback b/src/storage/testscripts/mvcc/rollback new file mode 100644 index 000000000..03faedfef --- /dev/null +++ b/src/storage/testscripts/mvcc/rollback @@ -0,0 +1,95 @@ +# Tests that transaction rollback properly rolls back uncommitted writes +# allowing other concurrent transactions to write the keys. + +import 1 a=0 b=0 c=0 d=0 +--- +ok + +# t2 will be rolled back. t1 and t3 are concurrent transactions. +t1: begin +t2: begin +t3: begin +--- +ok + +t1: set a=1 +t2: set b=2 +t2: delete c +t3: set d=3 +--- +ok + +dump +--- +NextVersion → 5 +TxnActive(2) → [] +TxnActive(3) → [] +TxnActive(4) → [] +TxnActiveSnapshot(3) → {2} +TxnActiveSnapshot(4) → {2,3} +TxnWrite(2, "a") → [] +TxnWrite(3, "b") → [] +TxnWrite(3, "c") → [] +TxnWrite(4, "d") → [] +Version("a", 1) → "0" +Version("a", 2) → "1" +Version("b", 1) → "0" +Version("b", 3) → "2" +Version("c", 1) → "0" +Version("c", 3) → None +Version("d", 1) → "0" +Version("d", 4) → "3" + +# Both t1 and t3 will conflict with t2. +t1: !set b=1 +t3: !set c=3 +--- +t1: Error: serialization failure, retry transaction +t3: Error: serialization failure, retry transaction + +# When t2 is rolled back, none of its writes will be visible, and t1 and t3 can +# perform their writes and successfully commit. +t2: rollback [ops] +--- +t2: engine delete Version("b", 3) +t2: engine delete TxnWrite(3, "b") +t2: engine delete Version("c", 3) +t2: engine delete TxnWrite(3, "c") +t2: engine delete TxnActive(3) + +t4: begin readonly +t4: scan +--- +t4: a → 0 +t4: b → 0 +t4: c → 0 +t4: d → 0 + +t1: set b=1 +t1: commit +t3: set c=3 +t3: commit +--- +ok + +t5: begin readonly +t5: scan +--- +t5: a → 1 +t5: b → 1 +t5: c → 3 +t5: d → 3 + +dump +--- +NextVersion → 5 +TxnActiveSnapshot(3) → {2} +TxnActiveSnapshot(4) → {2,3} +Version("a", 1) → "0" +Version("a", 2) → "1" +Version("b", 1) → "0" +Version("b", 2) → "1" +Version("c", 1) → "0" +Version("c", 4) → "3" +Version("d", 1) → "0" +Version("d", 4) → "3" diff --git a/src/storage/testscripts/mvcc/scan b/src/storage/testscripts/mvcc/scan new file mode 100644 index 000000000..190144430 --- /dev/null +++ b/src/storage/testscripts/mvcc/scan @@ -0,0 +1,101 @@ +# Scans should use correct key and time bounds. Sets up this dataset: +# +# T +# 4 x ba4 +# 3 x a3 b3 x +# 2 x ba2 bb2 bc2 +# 1 B1 a1 x c1 +# B a b ba bb bc c + +import 1 B=B1 a=a1 b= c=c1 +import 2 a= ba=ba2 bb=bb2 bc=bc2 +import 3 B= a=a3 b=b3 bb= +import 4 b= ba=ba4 +--- +ok + +# Full scans at all timestamps. +t1: begin readonly as_of=1 +t1: scan +--- +ok + +t2: begin readonly as_of=2 +t2: scan +--- +t2: B → B1 +t2: a → a1 +t2: c → c1 + +t3: begin readonly as_of=3 +t3: scan +--- +t3: B → B1 +t3: ba → ba2 +t3: bb → bb2 +t3: bc → bc2 +t3: c → c1 + +t4: begin readonly as_of=4 +t4: scan +--- +t4: a → a3 +t4: b → b3 +t4: ba → ba2 +t4: bc → bc2 +t4: c → c1 + +t5: begin readonly +t5: scan +--- +t5: a → a3 +t5: ba → ba4 +t5: bc → bc2 +t5: c → c1 + +# Full reverse scan at version 3. +t3: scan reverse=true +--- +t3: c → c1 +t3: bc → bc2 +t3: bb → bb2 +t3: ba → ba2 +t3: B → B1 + +# Various bounded scans around ba-bc at version 3. +t3: scan ba..bc +--- +t3: ba → ba2 +t3: bb → bb2 + +t3: scan "ba..=bc" +--- +t3: ba → ba2 +t3: bb → bb2 +t3: bc → bc2 + +t3: scan "ba..=bc" reverse=true +--- +t3: bc → bc2 +t3: bb → bb2 +t3: ba → ba2 + +t3: scan ba.. +--- +t3: ba → ba2 +t3: bb → bb2 +t3: bc → bc2 +t3: c → c1 + +t3: scan "..bc" +--- +t3: B → B1 +t3: ba → ba2 +t3: bb → bb2 + +t3: scan "..=bc" +--- +t3: B → B1 +t3: ba → ba2 +t3: bb → bb2 +t3: bc → bc2 diff --git a/src/storage/testscripts/mvcc/scan_isolation b/src/storage/testscripts/mvcc/scan_isolation new file mode 100644 index 000000000..395f54c17 --- /dev/null +++ b/src/storage/testscripts/mvcc/scan_isolation @@ -0,0 +1,43 @@ +# Scan should be isolated from concurrent transactions. + +# Past committed. +t1: begin +t1: set a=1 b=1 d=1 e=1 +t1: commit +--- +ok + +# Past uncommitted. +t2: begin +t2: set a=2 c=2 +t2: delete b +--- +ok + +# Begin the read transaction. +t3: begin readonly +--- +ok + +# Future committed. +t4: begin +t4: set d=3 f=3 +t4: delete e +t4: commit +--- +ok + +# Future uncommitted. +t5: begin +t5: set d=4 g=4 +t5: delete f +--- +ok + +# Scan keys. +t3: scan +--- +t3: a → 1 +t3: b → 1 +t3: d → 1 +t3: e → 1 diff --git a/src/storage/testscripts/mvcc/scan_key_version_encoding b/src/storage/testscripts/mvcc/scan_key_version_encoding new file mode 100644 index 000000000..a9ed2593b --- /dev/null +++ b/src/storage/testscripts/mvcc/scan_key_version_encoding @@ -0,0 +1,37 @@ +# Tests that the key encoding is resistant to key/version overlap. +# For example, a naïve concatenation of keys and versions would +# produce incorrect ordering in this case: +# +# 00|00 00 00 00 00 00 00 01 +# 00 00 00 00 00 00 00 00 02|00 00 00 00 00 00 00 02 +# 00|00 00 00 00 00 00 00 03 + +t1: begin +t1: set "\x00"="\x01" [ops] +t1: commit +--- +t1: engine set TxnWrite(1, 0x00) → [] +t1: engine set Version(0x00, 1) → 0x01 + +t2: begin +t2: set "\x00"="\x02" [ops] +t2: set "\x00\x00\x00\x00\x00\x00\x00\x00\x02"="\x02" [ops] +t2: commit +--- +t2: engine set TxnWrite(2, 0x00) → [] +t2: engine set Version(0x00, 2) → 0x02 +t2: engine set TxnWrite(2, 0x000000000000000002) → [] +t2: engine set Version(0x000000000000000002, 2) → 0x02 + +t3: begin +t3: set "\x00"="\x03" [ops] +t3: commit +--- +t3: engine set TxnWrite(3, 0x00) → [] +t3: engine set Version(0x00, 3) → 0x03 + +t4: begin readonly +t4: scan +--- +t4: \x00 → \x03 +t4: \x00\x00\x00\x00\x00\x00\x00\x00\x02 → \x02 diff --git a/src/storage/testscripts/mvcc/scan_prefix b/src/storage/testscripts/mvcc/scan_prefix new file mode 100644 index 000000000..9edad5fdf --- /dev/null +++ b/src/storage/testscripts/mvcc/scan_prefix @@ -0,0 +1,104 @@ +# Prefix scans should use correct key and time bounds. Sets up this dataset: +# +# T +# 4 x ba4 +# 3 x a3 b3 x +# 2 x ba2 bb2 bc2 +# 1 B1 a1 x c1 +# B a b ba bb bc c + +import 1 B=B1 a=a1 b= c=c1 +import 2 a= ba=ba2 bb=bb2 bc=bc2 +import 3 B= a=a3 b=b3 bb= +import 4 b= ba=ba4 +--- +ok + +# Full scans at all timestamps. +t1: begin readonly as_of=1 +t1: scan_prefix "" +--- +ok + +t2: begin readonly as_of=2 +t2: scan_prefix "" +--- +t2: B → B1 +t2: a → a1 +t2: c → c1 + +t3: begin readonly as_of=3 +t3: scan_prefix "" +--- +t3: B → B1 +t3: ba → ba2 +t3: bb → bb2 +t3: bc → bc2 +t3: c → c1 + +t4: begin readonly as_of=4 +t4: scan_prefix "" +--- +t4: a → a3 +t4: b → b3 +t4: ba → ba2 +t4: bc → bc2 +t4: c → c1 + +t5: begin readonly +t5: scan_prefix "" +--- +t5: a → a3 +t5: ba → ba4 +t5: bc → bc2 +t5: c → c1 + +# Full reverse scan at version 3. +t3: scan_prefix reverse=true "" +--- +t3: c → c1 +t3: bc → bc2 +t3: bb → bb2 +t3: ba → ba2 +t3: B → B1 + +# Various prefixes at version 3. +t3: scan_prefix B +--- +t3: B → B1 + +t3: scan_prefix b +--- +t3: ba → ba2 +t3: bb → bb2 +t3: bc → bc2 + +t3: scan_prefix bb +--- +t3: bb → bb2 + +t3: scan_prefix bbb +--- +ok + +# Various prefixes at version 4. +t4: scan_prefix B +--- +ok + +t4: scan_prefix b +--- +t4: b → b3 +t4: ba → ba2 +t4: bc → bc2 + +t4: scan_prefix bb +--- +ok + +# Reverse prefix scan at version 4. +t4: scan_prefix reverse=true b +--- +t4: bc → bc2 +t4: ba → ba2 +t4: b → b3 diff --git a/src/storage/testscripts/mvcc/set b/src/storage/testscripts/mvcc/set new file mode 100644 index 000000000..2ec7449f4 --- /dev/null +++ b/src/storage/testscripts/mvcc/set @@ -0,0 +1,51 @@ +# Sets should work on both existing, missing, and deleted keys. + +import a=1 b=1 x= +--- +ok + +# Can replace an existing key and tombstone. +t1: begin +t1: set a=2 x=2 [ops] +--- +t1: engine set TxnWrite(2, "a") → [] +t1: engine set Version("a", 2) → "2" +t1: engine set TxnWrite(2, "x") → [] +t1: engine set Version("x", 2) → "2" + +t1: scan +--- +t1: a → 2 +t1: b → 1 +t1: x → 2 + +# Can write a new key, replace it, and be idempotent. +t1: set c=1 c=2 c=2 [ops] +--- +t1: engine set TxnWrite(2, "c") → [] +t1: engine set Version("c", 2) → "1" +t1: engine set TxnWrite(2, "c") → [] +t1: engine set Version("c", 2) → "2" +t1: engine set TxnWrite(2, "c") → [] +t1: engine set Version("c", 2) → "2" + +t1: scan +--- +t1: a → 2 +t1: b → 1 +t1: c → 2 +t1: x → 2 + +dump +--- +NextVersion → 3 +TxnActive(2) → [] +TxnWrite(2, "a") → [] +TxnWrite(2, "c") → [] +TxnWrite(2, "x") → [] +Version("a", 1) → "1" +Version("a", 2) → "2" +Version("b", 1) → "1" +Version("c", 2) → "2" +Version("x", 1) → None +Version("x", 2) → "2" diff --git a/src/storage/testscripts/mvcc/set_conflict b/src/storage/testscripts/mvcc/set_conflict new file mode 100644 index 000000000..5af9b2a61 --- /dev/null +++ b/src/storage/testscripts/mvcc/set_conflict @@ -0,0 +1,39 @@ +# Set should return serialization errors both for uncommitted versions +# (past and future), and future committed versions. + +t1: begin +t2: begin +t3: begin +t4: begin +--- +ok + +t1: set a=1 +t3: set c=3 +t4: set d=4 +t4: commit +--- +ok + +t2: !set a=2 # past uncommitted +t2: !set c=2 # future uncommitted +t2: !set d=2 # future committed +--- +t2: Error: serialization failure, retry transaction +t2: Error: serialization failure, retry transaction +t2: Error: serialization failure, retry transaction + +dump +--- +NextVersion → 5 +TxnActive(1) → [] +TxnActive(2) → [] +TxnActive(3) → [] +TxnActiveSnapshot(2) → {1} +TxnActiveSnapshot(3) → {1,2} +TxnActiveSnapshot(4) → {1,2,3} +TxnWrite(1, "a") → [] +TxnWrite(3, "c") → [] +Version("a", 1) → "1" +Version("c", 3) → "3" +Version("d", 4) → "4" diff --git a/src/storage/testscripts/mvcc/unversioned b/src/storage/testscripts/mvcc/unversioned new file mode 100644 index 000000000..9a82cfe19 --- /dev/null +++ b/src/storage/testscripts/mvcc/unversioned @@ -0,0 +1,55 @@ +# Tests unversioned keys. + +# Getting a missing unversioned key returns None. +get_unversioned a +--- +a → None + +# Setting and getting an unversioned key should work. Dump engine operations. +set_unversioned a=0 [ops] +get_unversioned a +--- +engine set Unversioned("a") → "0" +a → 0 + +# Write some versioned keys with the same keys, interleaved between unversioned. +# The raw engine writes show that the internal keys are different. +t1: begin +t1: set a=1 b=1 c=1 [ops] +t1: commit +--- +t1: engine set TxnWrite(1, "a") → [] +t1: engine set Version("a", 1) → "1" +t1: engine set TxnWrite(1, "b") → [] +t1: engine set Version("b", 1) → "1" +t1: engine set TxnWrite(1, "c") → [] +t1: engine set Version("c", 1) → "1" + +# Set another unversioned key overlapping a versioned key. +set_unversioned b=0 d=0 [ops] +--- +engine set Unversioned("b") → "0" +engine set Unversioned("d") → "0" + +# An MVCC scan shouldn't see the unversioned keys. +t2: begin readonly +t2: scan +--- +t2: a → 1 +t2: b → 1 +t2: c → 1 + +# Unversioned gets should not see versioned keys. +get_unversioned a b c d +--- +a → 0 +b → 0 +c → None +d → 0 + +# Replacing an unversioned key should work too. +set_unversioned a=2 [ops] +get_unversioned a +--- +engine set Unversioned("a") → "2" +a → 2