Skip to main content

uqa_graph/
persistent_store.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Direct, indexed graph execution over durable records. Handles contain no
8//! vertex, edge, membership, or adjacency replicas.
9
10mod catalog;
11mod overlay;
12pub mod storage;
13mod trait_impl;
14
15use std::collections::{BTreeMap, BTreeSet};
16use std::sync::Arc;
17
18use uqa_core::{Edge, Vertex};
19use uqa_storage::{CatalogFacade, GraphEntityFilter, GraphEntityKind, PersistentStorageBackend};
20
21use crate::{
22    graphid_label_id, make_graphid, Direction, GraphLabelInfo, GraphLabelRegistry, GraphStore,
23    GraphStoreError, GraphStoreResult, LabelKind,
24};
25use storage::GraphStorage;
26
27const ID_PAGE_SIZE: usize = 256;
28
29/// A session-bound durable graph handle. Cloning duplicates only the handle;
30/// atomic rollback is provided by `GraphStore::transaction`, not by Clone.
31pub struct PersistentGraphStore {
32    storage: Arc<dyn GraphStorage>,
33    resource: Option<Arc<dyn Send + Sync>>,
34}
35
36impl Clone for PersistentGraphStore {
37    fn clone(&self) -> Self {
38        Self {
39            storage: Arc::clone(&self.storage),
40            resource: self.resource.clone(),
41        }
42    }
43}
44
45impl std::fmt::Debug for PersistentGraphStore {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("PersistentGraphStore")
48            .finish_non_exhaustive()
49    }
50}
51
52impl PersistentGraphStore {
53    /// The catalog and backend must belong to the same storage session.
54    #[must_use]
55    pub fn from_catalog(
56        catalog: Arc<dyn CatalogFacade>,
57        backend: Arc<dyn PersistentStorageBackend>,
58    ) -> Self {
59        Self::from_storage(Arc::new(catalog::CatalogGraphStorage { catalog, backend }))
60    }
61
62    pub fn from_storage(storage: Arc<dyn GraphStorage>) -> Self {
63        Self {
64            storage,
65            resource: None,
66        }
67    }
68
69    /// Keep a physical snapshot's resource owner alive with every cloned
70    /// handle (for example an encrypted temporary snapshot directory).
71    pub fn retain_resource(mut self, resource: Arc<dyn Send + Sync>) -> Self {
72        self.resource = Some(resource);
73        self
74    }
75
76    /// Read from a pinned durable snapshot, with this transaction's writes
77    /// overlaid by identity. No snapshot entities are copied into memory.
78    pub fn with_read_snapshot(&self, snapshot: &Self) -> Self {
79        Self::from_storage(Arc::new(overlay::OverlayGraphStorage::new(
80            self.clone(),
81            snapshot.clone(),
82        )))
83    }
84
85    /// A mutation candidate copies only its changed-id sets. Previously
86    /// published candidates are immutable rollback/savepoint checkpoints.
87    pub fn fork_for_mutation(&self) -> Self {
88        Self {
89            storage: self
90                .storage
91                .fork_overlay()
92                .unwrap_or_else(|| Arc::clone(&self.storage)),
93            resource: self.resource.clone(),
94        }
95    }
96
97    /// Reuse a transaction's original pinned reader when it has no graph
98    /// changes. A cursor can retain this owner independently of COMMIT.
99    pub fn unmodified_read_snapshot(&self) -> Option<Self> {
100        self.storage.unmodified_read_snapshot()
101    }
102
103    pub(crate) fn transaction_mapped<T, E>(
104        &mut self,
105        operation: impl FnOnce(&mut Self) -> Result<T, E>,
106        map_error: impl Fn(GraphStoreError) -> E,
107    ) -> Result<T, E>
108    where
109        E: std::fmt::Display,
110    {
111        let mut checkpoint = self.storage.begin_write().map_err(&map_error)?;
112        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| operation(self)));
113        match result {
114            Ok(Ok(value)) => match checkpoint.commit() {
115                Ok(()) => Ok(value),
116                Err(error) => {
117                    checkpoint.rollback().map_err(|rollback| {
118                        map_error(GraphStoreError::Storage(format!(
119                            "{error}; rollback failed: {rollback}"
120                        )))
121                    })?;
122                    Err(map_error(error))
123                }
124            },
125            Ok(Err(error)) => {
126                checkpoint.rollback().map_err(|rollback| {
127                    map_error(GraphStoreError::Storage(format!(
128                        "{error}; rollback failed: {rollback}"
129                    )))
130                })?;
131                Err(error)
132            }
133            Err(panic) => {
134                drop(checkpoint);
135                std::panic::resume_unwind(panic)
136            }
137        }
138    }
139
140    fn require_graph(&self, graph: &str) -> GraphStoreResult<()> {
141        if self.storage.has_graph(graph)? {
142            Ok(())
143        } else {
144            Err(GraphStoreError::UnknownGraph(graph.to_owned()))
145        }
146    }
147
148    fn for_each_id(
149        &self,
150        filter: GraphEntityFilter<'_>,
151        mut visit: impl FnMut(u64) -> GraphStoreResult<()>,
152    ) -> GraphStoreResult<()> {
153        let mut after = None;
154        loop {
155            let ids = self.storage.ids(filter, after, ID_PAGE_SIZE)?;
156            if ids.is_empty() {
157                return Ok(());
158            }
159            after = ids.last().copied();
160            for id in ids {
161                visit(id)?;
162            }
163        }
164    }
165
166    fn collect_ids(&self, filter: GraphEntityFilter<'_>) -> GraphStoreResult<BTreeSet<u64>> {
167        let mut ids = BTreeSet::new();
168        self.for_each_id(filter, |id| {
169            ids.insert(id);
170            Ok(())
171        })?;
172        Ok(ids)
173    }
174
175    fn require_vertex(&self, id: u64) -> GraphStoreResult<Vertex> {
176        self.storage.vertex(id)?.ok_or_else(|| {
177            GraphStoreError::CorruptGraph(format!(
178                "graph membership references missing vertex {id}"
179            ))
180        })
181    }
182
183    fn require_edge(&self, id: u64, graph: &str) -> GraphStoreResult<Edge> {
184        let edge = self.storage.edge(id)?.ok_or_else(|| {
185            GraphStoreError::CorruptGraph(format!("graph {graph:?} references missing edge {id}"))
186        })?;
187        for endpoint in [edge.source_id, edge.target_id] {
188            if self
189                .storage
190                .has_membership(GraphEntityKind::Vertex, endpoint, graph)?
191            {
192                if self.storage.vertex(endpoint)?.is_some() {
193                    continue;
194                }
195            } else if self
196                .storage
197                .registry(graph)?
198                .dropped_label_ids
199                .contains(&graphid_label_id(endpoint))
200            {
201                continue;
202            }
203            return Err(GraphStoreError::CorruptGraph(format!(
204                "graph {graph:?} edge {id} references missing endpoint {endpoint}"
205            )));
206        }
207        Ok(edge)
208    }
209
210    fn detach_entity(&self, kind: GraphEntityKind, id: u64, graph: &str) -> GraphStoreResult<()> {
211        self.storage.detach(kind, id, graph)?;
212        if self.storage.memberships(kind, id)?.is_empty() {
213            match kind {
214                GraphEntityKind::Vertex => self.storage.delete_vertex(id)?,
215                GraphEntityKind::Edge => self.storage.delete_edge(id)?,
216            }
217        }
218        Ok(())
219    }
220
221    fn reserve_id(&self, kind: GraphEntityKind, id: u64) -> GraphStoreResult<()> {
222        let next = id.checked_add(1).ok_or_else(|| {
223            GraphStoreError::IdExhausted(format!("{} id counter overflow", kind.as_str()))
224        })?;
225        let previous = self.next_counter(kind)?;
226        self.storage.save_counter(kind, next.max(previous))?;
227        Ok(())
228    }
229
230    fn next_counter(&self, kind: GraphEntityKind) -> GraphStoreResult<u64> {
231        if let Some(next) = self.storage.counter(kind)? {
232            return Ok(next);
233        }
234        self.storage
235            .max_id(kind)?
236            .unwrap_or(0)
237            .checked_add(1)
238            .ok_or_else(|| {
239                GraphStoreError::IdExhausted(format!("{} id counter overflow", kind.as_str()))
240            })
241    }
242
243    fn allocate_counter(&mut self, kind: GraphEntityKind) -> GraphStoreResult<u64> {
244        self.transaction(|store| {
245            let id = store.next_counter(kind)?;
246            let next = id.checked_add(1).ok_or_else(|| {
247                GraphStoreError::IdExhausted(format!("{} id counter overflow", kind.as_str()))
248            })?;
249            store.storage.save_counter(kind, next)?;
250            Ok(id)
251        })
252    }
253
254    fn allocate_label_id(
255        &mut self,
256        label: &str,
257        graph: &str,
258        kind: LabelKind,
259    ) -> GraphStoreResult<u64> {
260        self.transaction(|store| {
261            store.require_graph(graph)?;
262            let mut registry = store.storage.registry(graph)?;
263            let label_id = registry.label_id(label, kind)?;
264            let id = make_graphid(label_id, registry.next_sequence(label_id)?)?;
265            store.storage.save_registry(graph, &registry)?;
266            Ok(id)
267        })
268    }
269
270    pub fn label_registry(&self, graph: &str) -> GraphStoreResult<GraphLabelRegistry> {
271        self.require_graph(graph)?;
272        self.storage.registry(graph)
273    }
274
275    pub fn graph_labels(&self, graph: &str) -> GraphStoreResult<Vec<GraphLabelInfo>> {
276        Ok(self.label_registry(graph)?.labels())
277    }
278
279    pub fn graph_label_kind(
280        &self,
281        graph: &str,
282        label: &str,
283    ) -> GraphStoreResult<Option<LabelKind>> {
284        Ok(self.label_registry(graph)?.label_kind(label))
285    }
286
287    pub fn import_label_registry(
288        &mut self,
289        graph: &str,
290        registry: &GraphLabelRegistry,
291    ) -> GraphStoreResult<()> {
292        self.transaction(|store| {
293            let mut combined = store.label_registry(graph)?;
294            combined.merge(registry);
295            store.storage.save_registry(graph, &combined)
296        })
297    }
298
299    /// One-time legacy migration, streaming entity observations without
300    /// retaining their properties or an entity map in the engine.
301    pub fn rebuild_label_registry_from_ids(&mut self, graph: &str) -> GraphStoreResult<()> {
302        self.transaction(|store| {
303            let mut registry = store.label_registry(graph)?;
304            for kind in [GraphEntityKind::Vertex, GraphEntityKind::Edge] {
305                store.for_each_id(GraphEntityFilter::new(kind, Some(graph)), |id| {
306                    match kind {
307                        GraphEntityKind::Vertex => registry.observe(
308                            &store.require_vertex(id)?.label,
309                            id,
310                            LabelKind::Vertex,
311                        ),
312                        GraphEntityKind::Edge => registry.observe(
313                            &store.require_edge(id, graph)?.label,
314                            id,
315                            LabelKind::Edge,
316                        ),
317                    }
318                    Ok(())
319                })?;
320            }
321            store.storage.save_registry(graph, &registry)
322        })
323    }
324
325    pub fn create_label(
326        &mut self,
327        graph: &str,
328        label: &str,
329        kind: LabelKind,
330    ) -> GraphStoreResult<Option<u32>> {
331        self.transaction(|store| {
332            let mut registry = store.label_registry(graph)?;
333            let result = registry.register_label(label, kind)?;
334            if result.is_some() {
335                store.storage.save_registry(graph, &registry)?;
336            }
337            Ok(result)
338        })
339    }
340
341    pub fn drop_label(
342        &mut self,
343        graph: &str,
344        label: &str,
345    ) -> GraphStoreResult<Option<(u32, LabelKind)>> {
346        self.transaction(|store| {
347            let mut registry = store.label_registry(graph)?;
348            let Some(kind) = registry.label_kind(label) else {
349                return Ok(None);
350            };
351            let default = label == kind.default_label_name();
352            let id = if default {
353                if let Some(dependent) = registry
354                    .labels
355                    .keys()
356                    .find(|name| registry.label_kind(name) == Some(kind))
357                {
358                    return Err(GraphStoreError::InvalidMutation(format!(
359                        "cannot drop default label {label} while label {dependent} depends on it"
360                    )));
361                }
362                kind.default_label_id()
363            } else {
364                *registry.labels.get(label).ok_or_else(|| {
365                    GraphStoreError::CorruptGraph(format!(
366                        "graph {graph:?} label {label:?} has no registry id"
367                    ))
368                })?
369            };
370            let entity_kind = match kind {
371                LabelKind::Vertex => GraphEntityKind::Vertex,
372                LabelKind::Edge => GraphEntityKind::Edge,
373            };
374            let mut filter = GraphEntityFilter::new(entity_kind, Some(graph));
375            if !default {
376                filter.label = Some(label);
377            }
378            // AGE label removal intentionally preserves incident edge rows;
379            // the tombstone distinguishes these from corrupt endpoints.
380            store.for_each_id(filter, |entity_id| {
381                if !default || graphid_label_id(entity_id) == id {
382                    store.detach_entity(entity_kind, entity_id, graph)?;
383                }
384                Ok(())
385            })?;
386            registry.remove_label(label);
387            store.storage.save_registry(graph, &registry)?;
388            Ok(Some((id, kind)))
389        })
390    }
391
392    pub fn rename_graph(&mut self, from: &str, to: &str) -> GraphStoreResult<()> {
393        if from == to {
394            return Ok(());
395        }
396        self.transaction(|store| {
397            store.require_graph(from)?;
398            if store.storage.has_graph(to)? {
399                return Err(GraphStoreError::InvalidMutation(format!(
400                    "graph {to:?} already exists"
401                )));
402            }
403            store.storage.create_graph(to)?;
404            store
405                .storage
406                .save_registry(to, &store.label_registry(from)?)?;
407            for kind in [GraphEntityKind::Vertex, GraphEntityKind::Edge] {
408                store.for_each_id(GraphEntityFilter::new(kind, Some(from)), |id| {
409                    store.storage.attach(kind, id, to)?;
410                    store.storage.detach(kind, id, from)
411                })?;
412            }
413            store.storage.delete_graph(from)
414        })
415    }
416
417    fn algebra(
418        &mut self,
419        first: &str,
420        second: Option<&str>,
421        target: &str,
422        keep: impl Fn(bool) -> bool,
423        include_second: bool,
424    ) -> GraphStoreResult<()> {
425        self.transaction(|store| {
426            store.require_graph(first)?;
427            if let Some(second) = second {
428                store.require_graph(second)?;
429            }
430            store.create_graph(target)?;
431            for kind in [GraphEntityKind::Vertex, GraphEntityKind::Edge] {
432                store.for_each_id(GraphEntityFilter::new(kind, Some(first)), |id| {
433                    let common = match second {
434                        Some(second) => store.storage.has_membership(kind, id, second)?,
435                        None => false,
436                    };
437                    if keep(common) {
438                        store.storage.attach(kind, id, target)?;
439                    }
440                    Ok(())
441                })?;
442                if let Some(second) = second.filter(|_| include_second) {
443                    store.for_each_id(GraphEntityFilter::new(kind, Some(second)), |id| {
444                        store.storage.attach(kind, id, target)
445                    })?;
446                }
447            }
448            let mut registry = store.label_registry(target)?;
449            registry.merge(&store.label_registry(first)?);
450            if let Some(second) = second {
451                registry.merge(&store.label_registry(second)?);
452            }
453            store.storage.save_registry(target, &registry)
454        })
455    }
456}