1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
//! Snapshot isolation tests
//!
//! Tests for verifying that snapshots provide consistent, isolated views
//! of the database state at a point in time.
//!
//! **Hard Rule:** No API may observe state not bound to a committed snapshot_id.
//!
//! # Implementation Status
//!
//! - ✅ **Phase 38-02 COMPLETE**: SnapshotId type created
//! - ✅ **Phase 38-03 COMPLETE**: GraphBackend trait updated with snapshot_id parameters
//! - ⏳ **Phase 38-04 PENDING**: WAL filtering not yet implemented
//! - ❌ **Native/SQLite implementations**: Need to be updated to match trait signature
use SnapshotId;
// ============================================================================
// SnapshotId Type Tests (Phase 38-02 - COMPLETE)
// ============================================================================
/// Verify SnapshotId values can be compared for ordering
///
/// Snapshot IDs must be monotonically increasing to support:
/// - Time-based snapshot queries
/// - Historical data navigation
/// - Consistent snapshot ordering
// ============================================================================
// Snapshot Isolation Test Specifications (Blocked until 38-04 complete)
// ============================================================================
// Note: The following test specifications are documented but commented out
// because the GraphBackend implementations haven't been updated to match
// the new trait signature (with snapshot_id parameters).
//
// Once 38-04 is complete, uncomment these tests to verify snapshot isolation.
/*
/// **SNAPSHOT ISOLATION TEST**: Multiple snapshots see different, consistent views
///
/// # Expected Behavior
///
/// 1. Create snapshot_1 (empty graph)
/// 2. Insert 5 nodes → snapshot_2
/// 3. Create snapshot_3 (sees 5 nodes)
/// 4. Insert 5 more nodes → snapshot_4
/// 5. snapshot_2 should see 0 nodes
/// 6. snapshot_3 should see 5 nodes
/// 7. snapshot_4 should see 10 nodes
///
/// Each snapshot provides a consistent, isolated view of the database at that point in time.
///
/// # Implementation Required
///
/// - [ ] WAL filtering by tx_id
/// - [ ] Snapshot monotonicity tracking
/// - [ ] count_nodes() method with snapshot_id parameter
#[test]
fn test_multiple_snapshots_isolated() {
use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
use tempfile::NamedTempFile;
let temp = NamedTempFile::new().unwrap();
let cfg = GraphConfig::native();
let graph = open_graph(temp.path(), &cfg).unwrap();
// Insert first batch of nodes
let snapshot_1 = SnapshotId::current();
for i in 0..5 {
graph.insert_node(NodeSpec {
kind: "test".to_string(),
name: format!("node_{}", i),
data: None,
}).unwrap();
}
let snapshot_2 = SnapshotId::current();
// Insert second batch
for i in 5..10 {
graph.insert_node(NodeSpec {
kind: "test".to_string(),
name: format!("node_{}", i),
data: None,
}).unwrap();
}
let snapshot_3 = SnapshotId::current();
// snapshot_1 should see 0 nodes (before any inserts)
let count_1 = graph.count_nodes(snapshot_1).unwrap();
assert_eq!(count_1, 0, "snapshot_1 should see 0 nodes");
// snapshot_2 should see 5 nodes (first batch only)
let count_2 = graph.count_nodes(snapshot_2).unwrap();
assert_eq!(count_2, 5, "snapshot_2 should see 5 nodes");
// snapshot_3 should see 10 nodes (both batches)
let count_3 = graph.count_nodes(snapshot_3).unwrap();
assert_eq!(count_3, 10, "snapshot_3 should see 10 nodes");
}
/// **WAL FILTERING TEST**: WAL records filtered by snapshot_id
///
/// # Expected Behavior
///
/// This test verifies the core WAL filtering mechanism:
/// 1. Perform 10 commits (each creates new snapshot)
/// 2. Snapshot at commit 5 should see exactly 5 nodes
/// 3. Snapshot at commit 10 should see exactly 10 nodes
/// 4. Each snapshot filters WAL records by tx_id <= snapshot_id
///
/// # Implementation Required
///
/// - [ ] WAL stores tx_id with each record
/// - [ ] Read path filters records by tx_id <= snapshot_id
/// - [ ] SnapshotId tracking returns max committed tx_id
#[test]
fn test_wal_records_filtered_by_snapshot() {
use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
use tempfile::NamedTempFile;
let temp = NamedTempFile::new().unwrap();
let cfg = GraphConfig::native();
let graph = open_graph(temp.path(), &cfg).unwrap();
let mut snapshot_ids = Vec::new();
// Write 10 nodes (each commit creates new snapshot)
for i in 0..10 {
let current = SnapshotId::current();
graph.insert_node(NodeSpec {
kind: "test".to_string(),
name: format!("node_{}", i),
data: None,
}).unwrap();
snapshot_ids.push(SnapshotId::current());
}
// Snapshot at commit 5 should see exactly 5 nodes
let snapshot_at_5 = snapshot_ids[4]; // 0-indexed, so 4 = 5th commit
let count_5 = graph.count_nodes(snapshot_at_5).unwrap();
assert_eq!(count_5, 5, "Snapshot at commit 5 should see 5 nodes");
// Snapshot at commit 10 should see exactly 10 nodes
let snapshot_at_10 = snapshot_ids[9];
let count_10 = graph.count_nodes(snapshot_at_10).unwrap();
assert_eq!(count_10, 10, "Snapshot at commit 10 should see 10 nodes");
// Verify earlier snapshots don't see later data
let node_10 = graph.get_node(snapshot_at_5, 10);
assert!(node_10.is_err(), "Snapshot at 5 should not see node 10");
let node_10_current = graph.get_node(snapshot_at_10, 10);
assert!(node_10_current.is_ok(), "Snapshot at 10 should see node 10");
}
/// **CONCURRENT READER TEST**: Multiple readers see consistent state
///
/// # Expected Behavior
///
/// Simulates concurrent readers and writers:
/// 1. Reader 1 starts with snapshot (sees N nodes)
/// 2. Writer adds M nodes, commits
/// 3. Reader 1 should STILL see N nodes (snapshot isolated)
/// 4. Reader 2 (new snapshot) should see N+M nodes
///
/// This proves that snapshots provide true isolation - readers don't see
/// writes that occur after their snapshot was taken.
///
/// # Implementation Required
///
/// - [ ] Snapshot isolation in read path
/// - [ ] WAL filtering by snapshot_id
/// - [ ] No cross-snapshot pollution
#[test]
fn test_concurrent_readers_see_consistent_state() {
use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
use tempfile::NamedTempFile;
let temp = NamedTempFile::new().unwrap();
let cfg = GraphConfig::native();
let graph = open_graph(temp.path(), &cfg).unwrap();
// Reader 1 starts, sees N nodes
let reader1_snapshot = SnapshotId::current();
// Insert initial nodes
for i in 0..5 {
graph.insert_node(NodeSpec {
kind: "test".to_string(),
name: format!("node_{}", i),
data: None,
}).unwrap();
}
// Reader 1 should see 5 nodes
let count1 = graph.count_nodes(reader1_snapshot).unwrap();
assert_eq!(count1, 0, "Reader 1 snapshot should see 0 nodes (taken before inserts)");
// Writer adds more nodes, commits
let writer_snapshot = SnapshotId::current();
for i in 5..10 {
graph.insert_node(NodeSpec {
kind: "test".to_string(),
name: format!("node_{}", i),
data: None,
}).unwrap();
}
// Reader 1 should STILL see 0 nodes (its snapshot is isolated)
let count1_again = graph.count_nodes(reader1_snapshot).unwrap();
assert_eq!(count1_again, 0, "Reader 1 snapshot should not see later commits");
// Reader 2 (new snapshot) should see 10 nodes
let reader2_snapshot = SnapshotId::current();
let count2 = graph.count_nodes(reader2_snapshot).unwrap();
assert_eq!(count2, 10, "Reader 2 should see all committed nodes");
}
/// **TRANSACTION INTEGRATION TEST**: Commit returns usable SnapshotId
///
/// # Expected Behavior
///
/// Verify that commit operations create usable snapshots:
/// 1. Insert node (returns snapshot_id after commit)
/// 2. Read with that snapshot_id should see the node
/// 3. SnapshotId should be monotonically increasing
///
/// # Implementation Required
///
/// - [ ] insert_node() returns node_id AND new snapshot_id
/// - [ ] commit_transaction() tracks and returns SnapshotId
/// - [ ] SnapshotId reflects committed transaction ID
#[test]
fn test_commit_returns_snapshot_id() {
use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
use tempfile::NamedTempFile;
let temp = NamedTempFile::new().unwrap();
let cfg = GraphConfig::native();
let graph = open_graph(temp.path(), &cfg).unwrap();
// Insert should commit and create new snapshot
let snapshot_before = SnapshotId::current();
let node_id = graph.insert_node(NodeSpec {
kind: "test".to_string(),
name: "test_node".to_string(),
data: None,
}).unwrap();
let snapshot_after = SnapshotId::current();
// Verify the new snapshot is greater than before
assert!(snapshot_after.as_u64() > snapshot_before.as_u64());
// Verify the new snapshot is usable
let node = graph.get_node(snapshot_after, node_id).unwrap();
assert_eq!(node.name, "test_node");
}
/// **FULL ACID WORKFLOW TEST**: Complete ACID compliance verification
///
/// # Expected Behavior
///
/// End-to-end test of ACID properties:
///
/// **Atomicity**: Node A and C are atomic (either both visible or neither)
/// **Consistency**: All invariants maintained (no orphan nodes)
/// **Isolation**: snapshot_1 doesn't see snapshot_2's writes
/// **Durability**: Committed nodes survive (after 38-04 with WAL checkpoint)
///
/// # Test Scenario
///
/// 1. Create graph
/// 2. Insert node A (commit) → snapshot_1
/// 3. Insert node B (uncommitted, will be rolled back)
/// 4. Insert node C (commit) → snapshot_2
/// 5. Read with snapshot_1 → sees A only
/// 6. Read with snapshot_2 → sees A, C
/// 7. Read with current → sees A, C (B rolled back)
///
/// # Implementation Required
///
/// - [ ] Transaction API with explicit begin/commit/rollback
/// - [ ] Snapshot isolation for all reads
/// - [ ] WAL filtering by tx_id
/// - [ ] Uncommitted data not visible in any snapshot
#[test]
fn test_full_acid_workflow() {
use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
use tempfile::NamedTempFile;
let temp = NamedTempFile::new().unwrap();
let cfg = GraphConfig::native();
let graph = open_graph(temp.path(), &cfg).unwrap();
// 1. Create graph - done
// 2. Insert node A (commit) → snapshot_1
let snapshot_0 = SnapshotId::current();
let node_a = graph.insert_node(NodeSpec {
kind: "test".to_string(),
name: "A".to_string(),
data: None,
}).unwrap();
let snapshot_1 = SnapshotId::current();
// 3. Insert node B (uncommitted)
// Note: Need explicit transaction API for uncommitted writes
// For now, skip this test scenario
// 4. Insert node C (commit) → snapshot_2
let node_c = graph.insert_node(NodeSpec {
kind: "test".to_string(),
name: "C".to_string(),
data: None,
}).unwrap();
let snapshot_2 = SnapshotId::current();
// 5. Read with snapshot_0 → sees 0 nodes (before any writes)
let count_0 = graph.count_nodes(snapshot_0).unwrap();
assert_eq!(count_0, 0, "snapshot_0 should see 0 nodes");
// 6. Read with snapshot_1 → sees A only
let count_1 = graph.count_nodes(snapshot_1).unwrap();
assert_eq!(count_1, 1, "snapshot_1 should see only A");
// 7. Read with snapshot_2 → sees A, C
let count_2 = graph.count_nodes(snapshot_2).unwrap();
assert_eq!(count_2, 2, "snapshot_2 should see A and C");
// 8. Verify specific nodes visible in correct snapshots
let node_a_in_s0 = graph.get_node(snapshot_0, node_a);
assert!(node_a_in_s0.is_err(), "A should NOT be visible in snapshot_0");
let node_a_in_s1 = graph.get_node(snapshot_1, node_a);
assert!(node_a_in_s1.is_ok(), "A should be visible in snapshot_1");
let node_c_in_s1 = graph.get_node(snapshot_1, node_c);
assert!(node_c_in_s1.is_err(), "C should NOT be visible in snapshot_1");
let node_c_in_s2 = graph.get_node(snapshot_2, node_c);
assert!(node_c_in_s2.is_ok(), "C should be visible in snapshot_2");
}
*/