sochdb_core/version_chain.rs
1// Copyright 2025 Sushanth (https://github.com/sushanthpy)
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Unified MVCC Version Chain Interface
16//!
17//! This module defines the canonical interface for MVCC version chains across SochDB.
18//! Multiple implementations exist for different subsystems, but they share these traits.
19//!
20//! ## Implementations
21//!
22//! | Implementation | Location | Use Case |
23//! |---------------|----------|----------|
24//! | `VersionChain` | `sochdb_core::epoch_gc` | Epoch-based GC with VecDeque |
25//! | `VersionChain` | `sochdb_storage::mvcc_snapshot` | Snapshot-based visibility |
26//! | `VersionChain` | `sochdb_storage::version_store` | Generic key-value MVCC |
27//! | `VersionChain` | `sochdb_storage::durable_storage` | Binary-search optimized |
28//!
29//! ## Visibility Semantics
30//!
31//! All implementations follow these MVCC visibility rules:
32//!
33//! 1. **Read Committed**: A version is visible if its creating transaction has committed
34//! before the reader's start timestamp.
35//!
36//! 2. **Snapshot Isolation**: A version is visible if:
37//! - It was committed before the reader's snapshot timestamp
38//! - It was not deleted, or deleted after the snapshot timestamp
39//!
40//! 3. **Serializable (SSI)**: Adds read-write conflict detection on top of SI.
41
42/// Transaction identifier type
43pub type TxnId = u64;
44
45/// Logical timestamp type
46pub type Timestamp = u64;
47
48/// Version visibility context
49///
50/// Provides the information needed to determine if a version is visible
51/// to a particular reader/transaction.
52#[derive(Debug, Clone)]
53pub struct VisibilityContext {
54 /// Reader's transaction ID
55 pub reader_txn_id: TxnId,
56 /// Reader's snapshot timestamp
57 pub snapshot_ts: Timestamp,
58 /// Set of transaction IDs that are still active (not committed)
59 pub active_txn_ids: std::collections::HashSet<TxnId>,
60}
61
62impl VisibilityContext {
63 /// Create a new visibility context
64 pub fn new(reader_txn_id: TxnId, snapshot_ts: Timestamp) -> Self {
65 Self {
66 reader_txn_id,
67 snapshot_ts,
68 active_txn_ids: std::collections::HashSet::new(),
69 }
70 }
71
72 /// Create with active transaction set
73 pub fn with_active_txns(
74 reader_txn_id: TxnId,
75 snapshot_ts: Timestamp,
76 active_txn_ids: std::collections::HashSet<TxnId>,
77 ) -> Self {
78 Self {
79 reader_txn_id,
80 snapshot_ts,
81 active_txn_ids,
82 }
83 }
84
85 /// Check if a transaction was committed before this snapshot
86 pub fn is_committed_before(&self, txn_id: TxnId, commit_ts: Option<Timestamp>) -> bool {
87 match commit_ts {
88 Some(ts) => ts < self.snapshot_ts && !self.active_txn_ids.contains(&txn_id),
89 None => false,
90 }
91 }
92}
93
94/// Version metadata
95///
96/// Common metadata for all version chain implementations.
97#[derive(Debug, Clone)]
98pub struct VersionMeta {
99 /// Transaction that created this version
100 pub created_by: TxnId,
101 /// Timestamp when this version was created
102 pub created_ts: Timestamp,
103 /// Transaction that deleted this version (0 = not deleted)
104 pub deleted_by: TxnId,
105 /// Timestamp when this version was deleted (0 = not deleted)
106 pub deleted_ts: Timestamp,
107 /// Commit timestamp (0 = not yet committed)
108 pub commit_ts: Timestamp,
109}
110
111impl VersionMeta {
112 /// Create metadata for a new uncommitted version
113 pub fn new_uncommitted(created_by: TxnId, created_ts: Timestamp) -> Self {
114 Self {
115 created_by,
116 created_ts,
117 deleted_by: 0,
118 deleted_ts: 0,
119 commit_ts: 0,
120 }
121 }
122
123 /// Check if this version is visible according to the context
124 pub fn is_visible(&self, ctx: &VisibilityContext) -> bool {
125 // Must be committed before snapshot
126 if self.commit_ts == 0 {
127 // Uncommitted - only visible to creating transaction
128 return self.created_by == ctx.reader_txn_id;
129 }
130
131 if self.commit_ts >= ctx.snapshot_ts {
132 return false;
133 }
134
135 // Must not be deleted, or deleted after snapshot
136 if self.deleted_by != 0 && self.deleted_ts < ctx.snapshot_ts {
137 return false;
138 }
139
140 true
141 }
142
143 /// Mark as committed
144 pub fn commit(&mut self, commit_ts: Timestamp) {
145 self.commit_ts = commit_ts;
146 }
147
148 /// Mark as deleted
149 pub fn delete(&mut self, deleted_by: TxnId, deleted_ts: Timestamp) {
150 self.deleted_by = deleted_by;
151 self.deleted_ts = deleted_ts;
152 }
153
154 /// Check if version is committed
155 pub fn is_committed(&self) -> bool {
156 self.commit_ts != 0
157 }
158
159 /// Check if version is deleted
160 pub fn is_deleted(&self) -> bool {
161 self.deleted_by != 0
162 }
163}
164
165/// Trait for MVCC version chain implementations
166///
167/// Implementors store multiple versions of a value and provide
168/// visibility-based access according to MVCC semantics.
169pub trait MvccVersionChain {
170 /// The value type stored in versions
171 type Value;
172
173 /// Get the visible version for the given context
174 fn get_visible(&self, ctx: &VisibilityContext) -> Option<&Self::Value>;
175
176 /// Get the latest version (regardless of visibility)
177 fn get_latest(&self) -> Option<&Self::Value>;
178
179 /// Number of versions in the chain
180 fn version_count(&self) -> usize;
181
182 /// Check if the chain is empty
183 fn is_empty(&self) -> bool {
184 self.version_count() == 0
185 }
186}
187
188/// Trait for mutable version chain operations
189pub trait MvccVersionChainMut: MvccVersionChain {
190 /// Add a new uncommitted version
191 fn add_uncommitted(&mut self, value: Self::Value, txn_id: TxnId);
192
193 /// Commit a version
194 fn commit_version(&mut self, txn_id: TxnId, commit_ts: Timestamp) -> bool;
195
196 /// Mark the latest visible version as deleted
197 fn delete_version(&mut self, txn_id: TxnId, delete_ts: Timestamp) -> bool;
198
199 /// Garbage collect versions older than the given timestamp
200 /// Returns (versions_removed, bytes_freed)
201 fn gc(&mut self, min_visible_ts: Timestamp) -> (usize, usize);
202}
203
204/// Trait for detecting write conflicts
205pub trait WriteConflictDetection {
206 /// Check if there's a write-write conflict with another transaction
207 fn has_write_conflict(&self, txn_id: TxnId) -> bool;
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_version_meta_visibility() {
216 let mut meta = VersionMeta::new_uncommitted(1, 100);
217
218 // Uncommitted - only visible to creator
219 let ctx = VisibilityContext::new(1, 200);
220 assert!(meta.is_visible(&ctx));
221
222 let ctx2 = VisibilityContext::new(2, 200);
223 assert!(!meta.is_visible(&ctx2));
224
225 // After commit - visible to later snapshots
226 meta.commit(150);
227 assert!(meta.is_visible(&ctx2));
228
229 // Not visible to earlier snapshots
230 let ctx3 = VisibilityContext::new(3, 100);
231 assert!(!meta.is_visible(&ctx3));
232 }
233
234 #[test]
235 fn test_version_meta_deletion() {
236 let mut meta = VersionMeta::new_uncommitted(1, 100);
237 meta.commit(150);
238 meta.delete(2, 200);
239
240 // Visible before deletion
241 let ctx = VisibilityContext::new(3, 180);
242 assert!(meta.is_visible(&ctx));
243
244 // Not visible after deletion
245 let ctx2 = VisibilityContext::new(3, 250);
246 assert!(!meta.is_visible(&ctx2));
247 }
248
249 #[test]
250 fn test_visibility_context_committed_before() {
251 let mut active = std::collections::HashSet::new();
252 active.insert(5);
253
254 let ctx = VisibilityContext::with_active_txns(1, 200, active);
255
256 // Committed before snapshot
257 assert!(ctx.is_committed_before(2, Some(100)));
258
259 // Committed after snapshot
260 assert!(!ctx.is_committed_before(3, Some(250)));
261
262 // Active transaction - not committed
263 assert!(!ctx.is_committed_before(5, Some(100)));
264
265 // No commit timestamp
266 assert!(!ctx.is_committed_before(6, None));
267 }
268}