1use std::collections::BTreeSet;
9
10use crate::{
11 NodeId, NodeRecord, NodeRef, RelRef, RelationshipId, RelationshipRecord, SnapshotError,
12 SnapshotMeta, SnapshotPayload,
13};
14
15use super::distinct_stats::DistinctBuilder;
16use super::index_catalog::StoredIndexEntity;
17use super::{ConstraintDefinition, InMemoryGraph, IndexDefinition, VectorIndexSnapshot};
18
19pub(super) const PAYLOAD_FORMAT_VERSION: u32 = 1;
24
25impl InMemoryGraph {
26 pub fn snapshot_payload(&self) -> SnapshotPayload {
30 let mut vector_indexes = self
31 .vector_indexes_read(StoredIndexEntity::Node)
32 .to_snapshots(StoredIndexEntity::Node);
33 vector_indexes.extend(
34 self.vector_indexes_read(StoredIndexEntity::Relationship)
35 .to_snapshots(StoredIndexEntity::Relationship),
36 );
37 SnapshotPayload {
38 next_node_id: self.next_node_id,
39 next_rel_id: self.next_rel_id,
40 nodes: self.iter_node_refs().map(|r| r.to_record()).collect(),
41 relationships: self.iter_rel_refs().map(|r| r.to_record()).collect(),
42 indexes: self.index_catalog_read().list(),
43 constraints: self.constraint_catalog_read().list(),
44 vector_indexes,
45 }
46 }
47
48 pub fn load_snapshot_payload(
51 &mut self,
52 payload: SnapshotPayload,
53 ) -> Result<SnapshotMeta, SnapshotError> {
54 let meta = SnapshotMeta {
55 format_version: PAYLOAD_FORMAT_VERSION,
56 node_count: payload.nodes.len(),
57 relationship_count: payload.relationships.len(),
58 wal_lsn: None,
59 };
60
61 validate_payload_ids(&payload)?;
62
63 let mut builder = SnapshotGraphBuilder::new(payload.next_node_id, payload.next_rel_id)?;
64 for node in payload.nodes {
65 builder.push_node(node)?;
66 }
67 for rel in payload.relationships {
68 builder.push_relationship(rel)?;
69 }
70 let mut rebuilt =
71 builder.finish(payload.indexes, payload.constraints, payload.vector_indexes)?;
72
73 rebuilt.recorder = self.recorder.take();
77 *self = rebuilt;
78
79 Ok(meta)
80 }
81
82 pub fn snapshot_nodes(&self) -> impl Iterator<Item = NodeRef<'_>> + '_ {
89 self.iter_node_refs()
90 }
91
92 pub fn snapshot_relationships(&self) -> impl Iterator<Item = RelRef<'_>> + '_ {
94 self.iter_rel_refs()
95 }
96
97 pub fn snapshot_catalogs(&self) -> SnapshotCatalogs {
99 let mut vector_indexes = self
100 .vector_indexes_read(StoredIndexEntity::Node)
101 .to_snapshots(StoredIndexEntity::Node);
102 vector_indexes.extend(
103 self.vector_indexes_read(StoredIndexEntity::Relationship)
104 .to_snapshots(StoredIndexEntity::Relationship),
105 );
106 SnapshotCatalogs {
107 next_node_id: self.next_node_id,
108 next_rel_id: self.next_rel_id,
109 indexes: self.index_catalog_read().list(),
110 constraints: self.constraint_catalog_read().list(),
111 vector_indexes,
112 }
113 }
114}
115
116#[derive(Debug, Clone)]
119pub struct SnapshotCatalogs {
120 pub next_node_id: NodeId,
121 pub next_rel_id: RelationshipId,
122 pub indexes: Vec<IndexDefinition>,
123 pub constraints: Vec<ConstraintDefinition>,
124 pub vector_indexes: Vec<VectorIndexSnapshot>,
125}
126
127pub struct SnapshotGraphBuilder {
135 graph: InMemoryGraph,
136 node_distinct: DistinctBuilder,
137 rel_distinct: DistinctBuilder,
138}
139
140impl SnapshotGraphBuilder {
141 pub fn new(next_node_id: NodeId, next_rel_id: RelationshipId) -> Result<Self, SnapshotError> {
142 validate_next_id("node", next_node_id)?;
143 validate_next_id("relationship", next_rel_id)?;
144 let mut graph = InMemoryGraph::new();
145 graph.next_node_id = next_node_id;
146 graph.next_rel_id = next_rel_id;
147 Ok(Self {
148 graph,
149 node_distinct: DistinctBuilder::default(),
150 rel_distinct: DistinctBuilder::default(),
151 })
152 }
153
154 pub fn push_node(&mut self, node: NodeRecord) -> Result<(), SnapshotError> {
155 let id = node.id;
156 validate_entity_id("node", id, self.graph.next_node_id)?;
157 for label in &node.labels {
160 self.node_distinct.add_all(label, &node.properties);
161 }
162 if self.graph.has_node_at(id) {
163 return Err(SnapshotError::Decode(format!(
164 "duplicate node id {id} in snapshot payload"
165 )));
166 }
167 self.graph
168 .put_node_checked(id, &node)
169 .map_err(SnapshotError::Decode)?;
170 for label in &node.labels {
171 self.graph.insert_node_label_index(id, label);
172 }
173 Ok(())
174 }
175
176 pub fn push_relationship(&mut self, rel: RelationshipRecord) -> Result<(), SnapshotError> {
177 validate_entity_id("relationship", rel.id, self.graph.next_rel_id)?;
178 validate_slot_id("relationship source node", rel.src)?;
179 validate_slot_id("relationship target node", rel.dst)?;
180 if self.graph.has_rel_at(rel.id) {
181 return Err(SnapshotError::Decode(format!(
182 "duplicate relationship id {} in snapshot payload",
183 rel.id
184 )));
185 }
186 if !self.graph.has_node_at(rel.src) {
187 return Err(SnapshotError::Decode(format!(
188 "relationship {} references missing source node {}",
189 rel.id, rel.src
190 )));
191 }
192 if !self.graph.has_node_at(rel.dst) {
193 return Err(SnapshotError::Decode(format!(
194 "relationship {} references missing target node {}",
195 rel.id, rel.dst
196 )));
197 }
198 self.graph
199 .put_rel_checked(rel.id, &rel)
200 .map_err(SnapshotError::Decode)?;
201 self.graph.attach_relationship(&rel);
202 self.rel_distinct.add_all(&rel.rel_type, &rel.properties);
203 Ok(())
204 }
205
206 pub fn finish(
207 self,
208 indexes: Vec<IndexDefinition>,
209 constraints: Vec<ConstraintDefinition>,
210 vector_indexes: Vec<VectorIndexSnapshot>,
211 ) -> Result<InMemoryGraph, SnapshotError> {
212 let Self {
213 graph: mut rebuilt,
214 node_distinct,
215 rel_distinct,
216 } = self;
217 rebuilt.distinct_stats.nodes = node_distinct.finish();
218 rebuilt.distinct_stats.relationships = rel_distinct.finish();
219 let constraint_owned_indexes: BTreeSet<String> = constraints
227 .iter()
228 .filter_map(|def| {
229 def.owned_index
230 .clone()
231 .or_else(|| def.kind.requires_backing_index().then(|| def.name.clone()))
232 })
233 .collect();
234
235 for def in indexes {
242 if constraint_owned_indexes.contains(&def.name) {
243 continue;
244 }
245 rebuilt
248 .register_index(
249 crate::memory::IndexRequest {
250 explicit_name: Some(def.name.clone()),
251 kind: def.kind,
252 entity: def.entity,
253 label: def.label.clone(),
254 additional_labels: def.additional_labels.clone(),
255 properties: def.properties.clone(),
256 options: def.options.clone(),
257 },
258 true,
259 )
260 .map_err(|e| SnapshotError::Decode(format!("index `{}`: {e}", def.name)))?;
261 }
262
263 for snap in vector_indexes {
273 let entity = snap.entity;
274 let mut registry = rebuilt.vector_indexes_write(entity);
275 if !registry.restore_snapshot(snap) {
276 }
280 }
281
282 for def in constraints {
285 rebuilt
286 .register_constraint(
287 crate::memory::ConstraintRequest {
288 name: def.name.clone(),
289 kind: def.kind.clone(),
290 entity: def.entity,
291 label: def.label.clone(),
292 properties: def.properties.clone(),
293 },
294 true,
295 )
296 .map_err(|e| SnapshotError::Decode(format!("constraint `{}`: {e}", def.name)))?;
297 }
298
299 Ok(rebuilt)
300 }
301}
302
303fn validate_payload_ids(payload: &SnapshotPayload) -> Result<(), SnapshotError> {
304 validate_next_id("node", payload.next_node_id)?;
305 validate_next_id("relationship", payload.next_rel_id)?;
306
307 for node in &payload.nodes {
308 validate_entity_id("node", node.id, payload.next_node_id)?;
309 }
310 for rel in &payload.relationships {
311 validate_entity_id("relationship", rel.id, payload.next_rel_id)?;
312 validate_slot_id("relationship source node", rel.src)?;
313 validate_slot_id("relationship target node", rel.dst)?;
314 }
315
316 Ok(())
317}
318
319fn validate_next_id(kind: &str, next_id: u64) -> Result<(), SnapshotError> {
320 validate_slot_id(&format!("next {kind} id"), next_id)?;
321 if next_id == u64::MAX {
322 return Err(SnapshotError::Decode(format!(
323 "next {kind} id {next_id} leaves no allocatable id"
324 )));
325 }
326 Ok(())
327}
328
329fn validate_entity_id(kind: &str, id: u64, next_id: u64) -> Result<(), SnapshotError> {
330 validate_slot_id(kind, id)?;
331 if id >= next_id {
332 return Err(SnapshotError::Decode(format!(
333 "{kind} id {id} is not below next {kind} id {next_id}"
334 )));
335 }
336 Ok(())
337}
338
339fn validate_slot_id(label: &str, id: u64) -> Result<(), SnapshotError> {
340 let idx = usize::try_from(id).map_err(|_| {
341 SnapshotError::Decode(format!(
342 "{label} {id} does not fit in usize on this platform"
343 ))
344 })?;
345 idx.checked_add(1)
346 .ok_or_else(|| SnapshotError::Decode(format!("{label} {id} leaves no valid slab slot")))?;
347 Ok(())
348}