Skip to main content

featherdb_mvcc/
snapshot.rs

1//! Snapshot isolation implementation
2
3use featherdb_core::TransactionId;
4use std::collections::HashSet;
5
6/// A consistent view of the database at a point in time
7#[derive(Debug, Clone)]
8pub struct Snapshot {
9    /// Transaction ID of this snapshot
10    txn_id: TransactionId,
11    /// High-water mark: all transactions with ID >= this were not committed
12    high_water_mark: TransactionId,
13    /// Transactions with ID < high_water_mark that were NOT committed
14    /// (in-progress when snapshot was taken)
15    in_progress_at_start: HashSet<TransactionId>,
16    /// Ancestor transactions whose changes should also be visible
17    /// (used for sub-transactions/savepoints to see parent's uncommitted changes)
18    visible_ancestors: HashSet<TransactionId>,
19    /// Fast-path flag: true when both in_progress_at_start and visible_ancestors
20    /// are empty, allowing can_see() to skip HashSet lookups entirely.
21    is_clean: bool,
22}
23
24impl Snapshot {
25    /// Create a new snapshot
26    pub fn new(
27        txn_id: TransactionId,
28        high_water_mark: TransactionId,
29        in_progress: HashSet<TransactionId>,
30    ) -> Self {
31        let is_clean = in_progress.is_empty();
32        Snapshot {
33            txn_id,
34            high_water_mark,
35            in_progress_at_start: in_progress,
36            visible_ancestors: HashSet::new(),
37            is_clean,
38        }
39    }
40
41    /// Create a snapshot with visible ancestor transactions
42    ///
43    /// This is used for sub-transactions (savepoints) that need to see
44    /// their parent transaction's uncommitted changes.
45    pub fn with_ancestors(
46        txn_id: TransactionId,
47        high_water_mark: TransactionId,
48        in_progress: HashSet<TransactionId>,
49        ancestors: HashSet<TransactionId>,
50    ) -> Self {
51        let is_clean = in_progress.is_empty() && ancestors.is_empty();
52        Snapshot {
53            txn_id,
54            high_water_mark,
55            in_progress_at_start: in_progress,
56            visible_ancestors: ancestors,
57            is_clean,
58        }
59    }
60
61    /// Add an ancestor transaction whose changes should be visible
62    pub fn add_ancestor(&mut self, ancestor: TransactionId) {
63        self.visible_ancestors.insert(ancestor);
64        self.is_clean = false;
65    }
66
67    /// Check if a transaction is an ancestor (part of the same logical transaction)
68    pub fn is_ancestor(&self, txn_id: TransactionId) -> bool {
69        self.visible_ancestors.contains(&txn_id)
70    }
71
72    /// Get the transaction ID of this snapshot
73    pub fn txn_id(&self) -> TransactionId {
74        self.txn_id
75    }
76
77    /// Returns true when all committed transactions are visible.
78    /// This enables the ultra-fast scan path that skips per-row MVCC checks.
79    #[inline]
80    pub fn sees_all_committed(&self) -> bool {
81        self.is_clean
82    }
83
84    /// Get the high-water mark
85    #[inline]
86    pub fn high_water_mark(&self) -> TransactionId {
87        self.high_water_mark
88    }
89
90    /// Check if a transaction's changes are visible to this snapshot
91    #[inline]
92    pub fn can_see(&self, created_by: TransactionId) -> bool {
93        // Own changes are always visible
94        if created_by == self.txn_id {
95            return true;
96        }
97
98        // Fast path: when no in-progress txns and no ancestors exist,
99        // visibility is purely a range check (no HashSet lookups)
100        if self.is_clean {
101            return created_by < self.high_water_mark;
102        }
103
104        // Ancestor transactions are also visible (for savepoint support)
105        if self.visible_ancestors.contains(&created_by) {
106            return true;
107        }
108
109        // Future transactions not visible
110        if created_by >= self.high_water_mark {
111            return false;
112        }
113
114        // Transactions that were in-progress when we started are not visible
115        !self.in_progress_at_start.contains(&created_by)
116    }
117
118    /// Check if a version is visible based on creation and deletion timestamps
119    #[inline]
120    pub fn is_visible(&self, created_by: TransactionId, deleted_by: Option<TransactionId>) -> bool {
121        // Must be able to see when it was created
122        if !self.can_see(created_by) {
123            return false;
124        }
125
126        // If deleted, must not be able to see the deletion
127        if let Some(deleted_by) = deleted_by {
128            if self.can_see(deleted_by) {
129                return false;
130            }
131        }
132
133        true
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_snapshot_own_changes_visible() {
143        let snapshot = Snapshot::new(TransactionId(5), TransactionId(10), HashSet::new());
144
145        assert!(snapshot.can_see(TransactionId(5)));
146    }
147
148    #[test]
149    fn test_snapshot_old_committed_visible() {
150        let snapshot = Snapshot::new(TransactionId(5), TransactionId(10), HashSet::new());
151
152        // Committed before snapshot
153        assert!(snapshot.can_see(TransactionId(3)));
154    }
155
156    #[test]
157    fn test_snapshot_future_not_visible() {
158        let snapshot = Snapshot::new(TransactionId(5), TransactionId(10), HashSet::new());
159
160        // Future transaction
161        assert!(!snapshot.can_see(TransactionId(15)));
162    }
163
164    #[test]
165    fn test_snapshot_in_progress_not_visible() {
166        let mut in_progress = HashSet::new();
167        in_progress.insert(TransactionId(3));
168
169        let snapshot = Snapshot::new(TransactionId(5), TransactionId(10), in_progress);
170
171        // Transaction 3 was in progress when snapshot was taken
172        assert!(!snapshot.can_see(TransactionId(3)));
173    }
174
175    #[test]
176    fn test_snapshot_deleted_visibility() {
177        let snapshot = Snapshot::new(TransactionId(5), TransactionId(10), HashSet::new());
178
179        // Created before, not deleted -> visible
180        assert!(snapshot.is_visible(TransactionId(2), None));
181
182        // Created before, deleted before -> not visible
183        assert!(!snapshot.is_visible(TransactionId(2), Some(TransactionId(3))));
184
185        // Created before, deleted after snapshot -> visible
186        assert!(snapshot.is_visible(TransactionId(2), Some(TransactionId(15))));
187    }
188}