Skip to main content

uqa_storage/catalog/
graph_access.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Indexed graph reads. Entity payloads are fetched separately from bounded
8//! identity scans so traversal never requires a resident graph replica.
9
10use crate::{StorageBackendError, StorageBackendResult};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum GraphEntityKind {
14    Vertex,
15    Edge,
16}
17
18impl GraphEntityKind {
19    pub fn as_str(self) -> &'static str {
20        match self {
21            Self::Vertex => "vertex",
22            Self::Edge => "edge",
23        }
24    }
25}
26
27/// Conjunctive, storage-side filters over graph entity identities. `None`
28/// means no restriction; an empty label is an ordinary exact label value.
29#[derive(Debug, Clone, Copy)]
30pub struct GraphEntityFilter<'a> {
31    pub kind: GraphEntityKind,
32    pub graph: Option<&'a str>,
33    pub label: Option<&'a str>,
34    pub source: Option<u64>,
35    pub target: Option<u64>,
36}
37
38/// Primary identity index before applying the remaining conjunction. Adjacency and label selections take precedence over graph-wide membership scans.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum GraphEntitySelector<'a> {
41    All,
42    Graph(&'a str),
43    Label(&'a str),
44    Source(u64),
45    Target(u64),
46}
47
48impl<'a> GraphEntityFilter<'a> {
49    pub fn new(kind: GraphEntityKind, graph: Option<&'a str>) -> Self {
50        Self {
51            kind,
52            graph,
53            label: None,
54            source: None,
55            target: None,
56        }
57    }
58
59    pub fn validate(self) -> StorageBackendResult<()> {
60        if self.kind == GraphEntityKind::Vertex && (self.source.is_some() || self.target.is_some())
61        {
62            return Err(StorageBackendError::Other(
63                "vertex scans cannot have edge endpoint filters".into(),
64            ));
65        }
66        Ok(())
67    }
68
69    pub fn selector(self) -> StorageBackendResult<GraphEntitySelector<'a>> {
70        self.validate()?;
71        Ok(if let Some(source) = self.source {
72            GraphEntitySelector::Source(source)
73        } else if let Some(target) = self.target {
74            GraphEntitySelector::Target(target)
75        } else if let Some(label) = self.label {
76            GraphEntitySelector::Label(label)
77        } else if let Some(graph) = self.graph {
78            GraphEntitySelector::Graph(graph)
79        } else {
80            GraphEntitySelector::All
81        })
82    }
83}
84
85/// Upper bound on one graph identity page; callers advance with the last id.
86pub const MAX_GRAPH_ID_PAGE: usize = 4096;
87
88pub fn validate_graph_page(limit: usize) -> StorageBackendResult<()> {
89    if !(1..=MAX_GRAPH_ID_PAGE).contains(&limit) {
90        return Err(StorageBackendError::Other(format!(
91            "graph identity page size must be in 1..={MAX_GRAPH_ID_PAGE}"
92        )));
93    }
94    Ok(())
95}