lora_store/memory/snapshot.rs
1//! Snapshot payload helpers for the in-memory graph.
2//!
3//! `lora-store` no longer ships its own on-disk codec. The byte-level
4//! columnar format lives in `lora-snapshot`; this module just bridges
5//! between [`InMemoryGraph`] and the portable [`SnapshotPayload`]
6//! vocabulary.
7
8use crate::{SnapshotError, SnapshotMeta, SnapshotPayload};
9
10use super::InMemoryGraph;
11
12/// Format-version stamp surfaced through [`SnapshotMeta::format_version`]
13/// for payloads produced via the inherent helpers below. Kept stable
14/// across `lora-snapshot` codec versions because the payload shape
15/// itself has not changed; only the on-disk encoding has.
16pub(super) const PAYLOAD_FORMAT_VERSION: u32 = 1;
17
18impl InMemoryGraph {
19 /// Return the portable graph-state payload. Callers downstream of
20 /// `lora-store` (typically `lora-database`) feed this into
21 /// `lora-snapshot` for byte-level encoding.
22 pub fn snapshot_payload(&self) -> SnapshotPayload {
23 SnapshotPayload {
24 next_node_id: self.next_node_id,
25 next_rel_id: self.next_rel_id,
26 nodes: self.iter_node_records().cloned().collect(),
27 relationships: self.iter_rel_records().cloned().collect(),
28 }
29 }
30
31 /// Replace the graph from a portable graph-state payload, preserving the
32 /// currently installed mutation recorder across the swap.
33 pub fn load_snapshot_payload(
34 &mut self,
35 payload: SnapshotPayload,
36 ) -> Result<SnapshotMeta, SnapshotError> {
37 let meta = SnapshotMeta {
38 format_version: PAYLOAD_FORMAT_VERSION,
39 node_count: payload.nodes.len(),
40 relationship_count: payload.relationships.len(),
41 wal_lsn: None,
42 };
43
44 // Build the restored graph in a fresh local instance and only
45 // commit it into `self` at the very end. If a panic fires mid-
46 // rebuild (e.g. OOM on a HashMap grow) the caller's graph is
47 // untouched — we never observe a half-populated store.
48 let mut rebuilt = Self {
49 next_node_id: payload.next_node_id,
50 next_rel_id: payload.next_rel_id,
51 ..Self::default()
52 };
53
54 for node in payload.nodes {
55 let id = node.id;
56 let labels = node.labels.clone();
57 rebuilt.put_node(id, node);
58 for label in &labels {
59 rebuilt.insert_node_label_index(id, label);
60 }
61 }
62
63 for rel in payload.relationships {
64 rebuilt.attach_relationship(&rel);
65 let id = rel.id;
66 rebuilt.put_rel(id, rel);
67 }
68 rebuilt.rebuild_property_indexes();
69
70 // Preserve the existing recorder across the swap — observers of the
71 // store's identity should not be silently detached by a restore,
72 // same policy as `clear()`.
73 rebuilt.recorder = self.recorder.take();
74 *self = rebuilt;
75
76 Ok(meta)
77 }
78}