Skip to main content

cypherlite_storage/version/
mod.rs

1// VersionStore: pre-update snapshot storage for node/edge version history
2//
3// W-001: VersionStore module
4// W-002: Pre-update snapshot
5// W-003: Version chain structure
6
7use cypherlite_core::{NodeRecord, RelationshipRecord};
8use std::collections::BTreeMap;
9
10/// A version record capturing the state of a node or relationship at a point in time.
11#[derive(Debug, Clone, PartialEq)]
12pub enum VersionRecord {
13    /// Snapshot of a node record.
14    Node(NodeRecord),
15    /// Snapshot of a relationship record.
16    Relationship(RelationshipRecord),
17}
18
19/// In-memory version store backed by a BTreeMap.
20///
21/// Keys are (entity_id, version_seq) where entity_id is the node/edge ID
22/// and version_seq is a monotonically increasing sequence number per entity.
23pub struct VersionStore {
24    /// Storage: (entity_id, version_seq) -> VersionRecord
25    versions: BTreeMap<(u64, u64), VersionRecord>,
26    /// Next version sequence number per entity.
27    next_seq: BTreeMap<u64, u64>,
28}
29
30impl VersionStore {
31    /// Create a new empty version store.
32    pub fn new() -> Self {
33        Self {
34            versions: BTreeMap::new(),
35            next_seq: BTreeMap::new(),
36        }
37    }
38
39    /// Snapshot a node record before an update.
40    /// Returns the version sequence number assigned.
41    pub fn snapshot_node(&mut self, entity_id: u64, record: NodeRecord) -> u64 {
42        let seq = self.next_seq.entry(entity_id).or_insert(1);
43        let current_seq = *seq;
44        self.versions
45            .insert((entity_id, current_seq), VersionRecord::Node(record));
46        *seq += 1;
47        current_seq
48    }
49
50    /// Snapshot a relationship record before an update.
51    /// Returns the version sequence number assigned.
52    pub fn snapshot_relationship(&mut self, entity_id: u64, record: RelationshipRecord) -> u64 {
53        let seq = self.next_seq.entry(entity_id).or_insert(1);
54        let current_seq = *seq;
55        self.versions.insert(
56            (entity_id, current_seq),
57            VersionRecord::Relationship(record),
58        );
59        *seq += 1;
60        current_seq
61    }
62
63    /// Get a specific version of an entity.
64    pub fn get_version(&self, entity_id: u64, version_seq: u64) -> Option<&VersionRecord> {
65        self.versions.get(&(entity_id, version_seq))
66    }
67
68    /// Get the latest version of an entity (the most recent snapshot).
69    pub fn get_latest_version(&self, entity_id: u64) -> Option<&VersionRecord> {
70        let current_seq = self.next_seq.get(&entity_id)?;
71        if *current_seq <= 1 {
72            return None;
73        }
74        self.versions.get(&(entity_id, *current_seq - 1))
75    }
76
77    /// Get the full version chain for an entity (oldest to newest).
78    pub fn get_version_chain(&self, entity_id: u64) -> Vec<(u64, &VersionRecord)> {
79        self.versions
80            .range((entity_id, 0)..=(entity_id, u64::MAX))
81            .map(|((_, seq), record)| (*seq, record))
82            .collect()
83    }
84
85    /// Get the number of versions stored for an entity.
86    pub fn version_count(&self, entity_id: u64) -> u64 {
87        self.next_seq.get(&entity_id).copied().unwrap_or(1) - 1
88    }
89
90    /// Get the total number of version records stored.
91    pub fn total_versions(&self) -> usize {
92        self.versions.len()
93    }
94
95    /// Insert a version record loaded from persistent storage.
96    ///
97    /// Updates `next_seq` to ensure new sequence numbers don't collide.
98    pub fn insert_loaded_record(
99        &mut self,
100        entity_id: u64,
101        version_seq: u64,
102        record: VersionRecord,
103    ) {
104        self.versions.insert((entity_id, version_seq), record);
105        let seq = self.next_seq.entry(entity_id).or_insert(1);
106        if version_seq >= *seq {
107            *seq = version_seq + 1;
108        }
109    }
110
111    /// Returns an iterator over all (entity_id, version_seq, record) tuples.
112    pub fn all(&self) -> impl Iterator<Item = (&(u64, u64), &VersionRecord)> {
113        self.versions.iter()
114    }
115}
116
117impl Default for VersionStore {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use cypherlite_core::{Direction, EdgeId, NodeId, PropertyValue};
127
128    fn sample_node(id: u64, name: &str) -> NodeRecord {
129        NodeRecord {
130            node_id: NodeId(id),
131            labels: vec![1],
132            properties: vec![(1, PropertyValue::String(name.to_string()))],
133            next_edge_id: None,
134            overflow_page: None,
135        }
136    }
137
138    fn sample_edge(id: u64) -> RelationshipRecord {
139        RelationshipRecord {
140            edge_id: EdgeId(id),
141            start_node: NodeId(1),
142            end_node: NodeId(2),
143            rel_type_id: 1,
144            direction: Direction::Outgoing,
145            next_out_edge: None,
146            next_in_edge: None,
147            properties: vec![(1, PropertyValue::String("v1".to_string()))],
148            #[cfg(feature = "subgraph")]
149            start_is_subgraph: false,
150            #[cfg(feature = "subgraph")]
151            end_is_subgraph: false,
152        }
153    }
154
155    // W-001: VersionStore creation
156    #[test]
157    fn test_version_store_new_is_empty() {
158        let store = VersionStore::new();
159        assert_eq!(store.total_versions(), 0);
160    }
161
162    // W-002: Pre-update snapshot for node
163    #[test]
164    fn test_snapshot_node() {
165        let mut store = VersionStore::new();
166        let node = sample_node(1, "Alice");
167        let seq = store.snapshot_node(1, node.clone());
168        assert_eq!(seq, 1);
169        assert_eq!(store.total_versions(), 1);
170
171        let version = store.get_version(1, 1).expect("version exists");
172        assert_eq!(*version, VersionRecord::Node(node));
173    }
174
175    // W-002: Pre-update snapshot for relationship
176    #[test]
177    fn test_snapshot_relationship() {
178        let mut store = VersionStore::new();
179        let edge = sample_edge(1);
180        let seq = store.snapshot_relationship(1, edge.clone());
181        assert_eq!(seq, 1);
182
183        let version = store.get_version(1, 1).expect("version exists");
184        assert_eq!(*version, VersionRecord::Relationship(edge));
185    }
186
187    // W-002: Multiple snapshots create incrementing sequence numbers
188    #[test]
189    fn test_multiple_snapshots_incrementing_seq() {
190        let mut store = VersionStore::new();
191
192        let seq1 = store.snapshot_node(1, sample_node(1, "v1"));
193        let seq2 = store.snapshot_node(1, sample_node(1, "v2"));
194        let seq3 = store.snapshot_node(1, sample_node(1, "v3"));
195
196        assert_eq!(seq1, 1);
197        assert_eq!(seq2, 2);
198        assert_eq!(seq3, 3);
199        assert_eq!(store.version_count(1), 3);
200    }
201
202    // W-003: Version chain retrieval
203    #[test]
204    fn test_version_chain() {
205        let mut store = VersionStore::new();
206
207        store.snapshot_node(1, sample_node(1, "v1"));
208        store.snapshot_node(1, sample_node(1, "v2"));
209        store.snapshot_node(1, sample_node(1, "v3"));
210
211        let chain = store.get_version_chain(1);
212        assert_eq!(chain.len(), 3);
213        assert_eq!(chain[0].0, 1); // oldest first
214        assert_eq!(chain[2].0, 3); // newest last
215    }
216
217    // W-003: Get latest version
218    #[test]
219    fn test_get_latest_version() {
220        let mut store = VersionStore::new();
221
222        store.snapshot_node(1, sample_node(1, "v1"));
223        store.snapshot_node(1, sample_node(1, "v2"));
224
225        let latest = store.get_latest_version(1).expect("latest exists");
226        match latest {
227            VersionRecord::Node(n) => {
228                assert_eq!(n.properties[0].1, PropertyValue::String("v2".to_string()));
229            }
230            _ => panic!("expected node version"),
231        }
232    }
233
234    // W-003: Get latest version for nonexistent entity
235    #[test]
236    fn test_get_latest_version_nonexistent() {
237        let store = VersionStore::new();
238        assert!(store.get_latest_version(999).is_none());
239    }
240
241    // W-003: Version chain for nonexistent entity is empty
242    #[test]
243    fn test_version_chain_empty() {
244        let store = VersionStore::new();
245        let chain = store.get_version_chain(999);
246        assert!(chain.is_empty());
247    }
248
249    // W-002: Independent version sequences per entity
250    #[test]
251    fn test_independent_sequences_per_entity() {
252        let mut store = VersionStore::new();
253
254        let s1 = store.snapshot_node(1, sample_node(1, "A"));
255        let s2 = store.snapshot_node(2, sample_node(2, "B"));
256        let s3 = store.snapshot_node(1, sample_node(1, "A2"));
257
258        assert_eq!(s1, 1);
259        assert_eq!(s2, 1); // different entity, starts at 1
260        assert_eq!(s3, 2); // same entity as s1, increments
261
262        assert_eq!(store.version_count(1), 2);
263        assert_eq!(store.version_count(2), 1);
264    }
265
266    // W-001: Default trait
267    #[test]
268    fn test_version_store_default() {
269        let store = VersionStore::default();
270        assert_eq!(store.total_versions(), 0);
271    }
272
273    // W-002: Version count for unseen entity
274    #[test]
275    fn test_version_count_unseen_entity() {
276        let store = VersionStore::new();
277        assert_eq!(store.version_count(999), 0);
278    }
279}