Skip to main content

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}