Skip to main content

akar_storage/
version_info.rs

1//! VersionInfo — per-node-group tracking of insert/delete visibility.
2//!
3//! Each `NodeGroup` can have an optional `VersionInfo` that tracks which
4//! transactions have inserted or deleted rows within it. When reading at
5//! a given snapshot timestamp, the `VersionInfo` determines whether a
6//! specific row is visible.
7//!
8//! This is the Rust port of Vela C++'s `VersionInfo` / `VectorVersionInfo`.
9
10use std::collections::HashMap;
11use std::sync::Mutex;
12
13/// Tracks insert/delete visibility for a single vector (1024 rows).
14///
15/// Uses a `Mutex`-protected map from transaction ID to a bitmap of
16/// affected row indices within this vector.
17#[derive(Debug)]
18pub struct VectorVersionInfo {
19    /// Map: transaction_id → set of inserted row indices (relative to vector).
20    inserted: Mutex<HashMap<u64, Vec<u32>>>,
21    /// Map: transaction_id → set of deleted row indices.
22    deleted: Mutex<HashMap<u64, Vec<u32>>>,
23}
24
25impl Clone for VectorVersionInfo {
26    fn clone(&self) -> Self {
27        Self {
28            inserted: Mutex::new(self.inserted.lock().unwrap().clone()),
29            deleted: Mutex::new(self.deleted.lock().unwrap().clone()),
30        }
31    }
32}
33
34impl Default for VectorVersionInfo {
35    fn default() -> Self {
36        Self {
37            inserted: Mutex::new(HashMap::new()),
38            deleted: Mutex::new(HashMap::new()),
39        }
40    }
41}
42
43impl VectorVersionInfo {
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Record that `txn_id` inserted a row at `row_in_vector`.
49    pub fn insert(&self, txn_id: u64, row_in_vector: u32) {
50        let mut ins = self.inserted.lock().unwrap();
51        ins.entry(txn_id).or_default().push(row_in_vector);
52    }
53
54    /// Record that `txn_id` deleted a row at `row_in_vector`.
55    pub fn delete(&self, txn_id: u64, row_in_vector: u32) {
56        let mut del = self.deleted.lock().unwrap();
57        del.entry(txn_id).or_default().push(row_in_vector);
58    }
59
60    /// Drop all recorded inserts and deletes for this vector.
61    ///
62    /// Called when the owning node group's in-memory buffer is cleared for
63    /// reuse (spill/restore), so stale row-offset records do not collide
64    /// with rows appended afterwards at the same offsets.
65    pub fn reset(&self) {
66        self.inserted.lock().unwrap().clear();
67        self.deleted.lock().unwrap().clear();
68    }
69
70    /// Check whether a specific row is visible at the given snapshot.
71    ///
72    /// A row is visible if it was inserted by a committed transaction
73    /// whose commit_ts ≤ snapshot_ts, and not deleted by any committed
74    /// transaction whose commit_ts ≤ snapshot_ts.
75    ///
76    /// `commit_history` is used to look up commit timestamps for
77    /// transaction IDs.
78    pub fn is_visible(&self, row_in_vector: u32, snapshot_ts: u64, commit_history: &HashMap<u64, u64>) -> bool {
79        // Check deletions: if any committed txn with commit_ts ≤ snapshot_ts
80        // deleted this row, it's not visible.
81        if let Ok(del) = self.deleted.lock() {
82            for (&txn_id, rows) in del.iter() {
83                if rows.contains(&row_in_vector) && is_txn_committed_before(txn_id, snapshot_ts, commit_history) {
84                    return false;
85                }
86            }
87        }
88
89        // Check insertions: if any committed txn with commit_ts ≤ snapshot_ts
90        // inserted this row, it's visible. If no insert record, the row is
91        // visible by default (pre-existing data).
92        if let Ok(ins) = self.inserted.lock() {
93            for (&txn_id, rows) in ins.iter() {
94                if rows.contains(&row_in_vector) {
95                    return is_txn_committed_before(txn_id, snapshot_ts, commit_history);
96                }
97            }
98        }
99
100        // No insert record — row existed before any tracked transaction.
101        true
102    }
103
104    /// Number of unique transactions that inserted rows.
105    pub fn num_inserters(&self) -> usize {
106        self.inserted.lock().map(|m| m.len()).unwrap_or(0)
107    }
108
109    /// Number of unique transactions that deleted rows.
110    pub fn num_deleters(&self) -> usize {
111        self.deleted.lock().map(|m| m.len()).unwrap_or(0)
112    }
113}
114
115/// Helper: check if a transaction's commit timestamp ≤ snapshot_ts (O(1) lookup).
116fn is_txn_committed_before(txn_id: u64, snapshot_ts: u64, commit_history: &HashMap<u64, u64>) -> bool {
117    commit_history
118        .get(&txn_id)
119        .is_some_and(|&commit_ts| commit_ts <= snapshot_ts)
120}
121
122/// Per-node-group version tracking.
123///
124/// Contains one `VectorVersionInfo` per vector-sized chunk of the node group.
125/// A "vector" is `NODE_GROUP_SIZE / vectors` rows — typically 1024 rows each.
126///
127/// Note: `Clone` is implemented manually because `Mutex` is not `Clone`.
128#[derive(Debug)]
129pub struct VersionInfo {
130    /// One VectorVersionInfo per vector in the node group.
131    vectors: Vec<VectorVersionInfo>,
132    /// Number of rows per vector.
133    vector_size: u32,
134}
135
136impl Clone for VersionInfo {
137    fn clone(&self) -> Self {
138        Self {
139            vectors: self.vectors.clone(),
140            vector_size: self.vector_size,
141        }
142    }
143}
144
145impl VersionInfo {
146    /// Create a new VersionInfo for a node group of `total_rows` capacity.
147    pub fn new(total_rows: usize) -> Self {
148        // Default vector size: 1024 (matching Vela C++ convention)
149        let vector_size = 1024u32;
150        let num_vectors = total_rows.div_ceil(vector_size as usize);
151        let vectors = (0..num_vectors).map(|_| VectorVersionInfo::new()).collect();
152        Self { vectors, vector_size }
153    }
154
155    fn vector_idx(&self, row: u32) -> usize {
156        (row / self.vector_size) as usize
157    }
158
159    fn row_in_vector(&self, row: u32) -> u32 {
160        row % self.vector_size
161    }
162
163    /// Record an insert by `txn_id` at global `row` index.
164    pub fn insert(&self, txn_id: u64, row: u32) {
165        let v_idx = self.vector_idx(row);
166        if v_idx < self.vectors.len() {
167            self.vectors[v_idx].insert(txn_id, self.row_in_vector(row));
168        }
169    }
170
171    /// Record a delete by `txn_id` at global `row` index.
172    pub fn delete(&self, txn_id: u64, row: u32) {
173        let v_idx = self.vector_idx(row);
174        if v_idx < self.vectors.len() {
175            self.vectors[v_idx].delete(txn_id, self.row_in_vector(row));
176        }
177    }
178
179    /// Drop all recorded inserts and deletes across every vector.
180    ///
181    /// Called when the owning node group's in-memory buffer is cleared for
182    /// reuse (spill/restore), so stale row-offset records do not collide
183    /// with rows appended afterwards at the same offsets.
184    pub fn reset(&self) {
185        for v in &self.vectors {
186            v.reset();
187        }
188    }
189
190    /// Total number of unique inserting transactions across all vectors.
191    pub fn num_inserters(&self) -> usize {
192        self.vectors.iter().map(|v| v.num_inserters()).sum()
193    }
194
195    /// Total number of unique deleting transactions across all vectors.
196    pub fn num_deleters(&self) -> usize {
197        self.vectors.iter().map(|v| v.num_deleters()).sum()
198    }
199
200    /// Check whether global `row` is visible at `snapshot_ts`.
201    pub fn is_visible(&self, row: u32, snapshot_ts: u64, commit_history: &HashMap<u64, u64>) -> bool {
202        let v_idx = self.vector_idx(row);
203        if v_idx < self.vectors.len() {
204            self.vectors[v_idx].is_visible(self.row_in_vector(row), snapshot_ts, commit_history)
205        } else {
206            true // Row beyond tracked range is visible (pre-existing)
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_version_info_basic() {
217        let vi = VersionInfo::new(NODE_GROUP_SIZE);
218        let history = HashMap::from([(1u64, 10u64), (2u64, 20u64)]);
219
220        vi.insert(1, 5);
221        vi.delete(2, 10);
222
223        // Row 5 was inserted by txn#1 (commit_ts=10) — visible at ts=10
224        assert!(vi.is_visible(5, 10, &history));
225        // Row 10 was deleted by txn#2 (commit_ts=20) — not visible at ts=20
226        assert!(!vi.is_visible(10, 20, &history));
227        // Row 10 visible at ts=15 (before txn#2 committed)
228        assert!(vi.is_visible(10, 15, &history));
229        // Row 0 has no records — visible by default
230        assert!(vi.is_visible(0, 10, &history));
231    }
232
233    #[test]
234    fn test_vector_version_info_insert() {
235        let vvi = VectorVersionInfo::new();
236        vvi.insert(1, 42);
237        let history = HashMap::from([(1u64, 5u64)]);
238
239        assert!(vvi.is_visible(42, 5, &history));
240        assert!(!vvi.is_visible(42, 3, &history)); // Before commit
241    }
242
243    #[test]
244    fn test_vector_version_info_delete() {
245        let vvi = VectorVersionInfo::new();
246        vvi.delete(1, 7);
247        let history = HashMap::from([(1u64, 10u64)]);
248
249        assert!(!vvi.is_visible(7, 10, &history)); // Deleted
250        assert!(vvi.is_visible(7, 5, &history)); // Before delete committed
251    }
252
253    #[test]
254    fn test_version_info_history_lookup_semantics() {
255        // Commit history is now a HashMap: absent txn, committed-before, and
256        // committed-after must resolve identically to the old slice scan.
257        let vi = VersionInfo::new(NODE_GROUP_SIZE);
258        vi.insert(1, 5);
259        vi.delete(2, 10);
260
261        // txn#1 committed at ts=10: visible at 10, invisible before.
262        assert!(vi.is_visible(5, 10, &HashMap::from([(1u64, 10u64), (2u64, 20u64)])));
263        assert!(!vi.is_visible(5, 9, &HashMap::from([(1u64, 10u64), (2u64, 20u64)])));
264        // txn#1 absent from history: invisible at any snapshot.
265        assert!(!vi.is_visible(5, 10, &HashMap::from([(2u64, 20u64)])));
266        // Row 10 was deleted by txn#2 (commit_ts=20): invisible once committed.
267        assert!(!vi.is_visible(10, 20, &HashMap::from([(1u64, 10u64), (2u64, 20u64)])));
268        // Empty history: inserted row invisible; deleted row still visible
269        // (delete txn absent => not committed, so no tombstone applies).
270        assert!(!vi.is_visible(5, 10, &HashMap::new()));
271        assert!(vi.is_visible(10, 10, &HashMap::new()));
272    }
273
274    #[test]
275    fn test_version_info_reset_clears_all_records() {
276        let vi = VersionInfo::new(NODE_GROUP_SIZE);
277        let history = HashMap::from([(1u64, 10u64), (2u64, 20u64)]);
278
279        vi.insert(1, 5);
280        vi.delete(2, 10);
281        assert_eq!(vi.num_inserters(), 1);
282        assert_eq!(vi.num_deleters(), 1);
283
284        vi.reset();
285
286        assert_eq!(vi.num_inserters(), 0);
287        assert_eq!(vi.num_deleters(), 0);
288        // No records remain — every row is visible by default.
289        assert!(vi.is_visible(5, 0, &history));
290        assert!(vi.is_visible(5, 10, &history));
291        assert!(vi.is_visible(10, 0, &history));
292    }
293
294    #[test]
295    fn test_vector_version_info_reset_clears_all_records() {
296        let vvi = VectorVersionInfo::new();
297        vvi.insert(1, 42);
298        vvi.delete(2, 7);
299        assert_eq!(vvi.num_inserters(), 1);
300        assert_eq!(vvi.num_deleters(), 1);
301
302        vvi.reset();
303        assert_eq!(vvi.num_inserters(), 0);
304        assert_eq!(vvi.num_deleters(), 0);
305        assert!(vvi.is_visible(42, 0, &HashMap::from([(1u64, 10u64)])));
306        assert!(vvi.is_visible(7, 0, &HashMap::from([(2u64, 10u64)])));
307    }
308
309    // Need NODE_GROUP_SIZE for the VersionInfo::new() test
310    use crate::column_chunk::NODE_GROUP_SIZE;
311}
312
313/// Storage format version, returned by CALL storage_version().
314pub const STORAGE_VERSION: u32 = 1;