Skip to main content

uqa_storage/catalog/graph_observations/
topology.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Logical selector intervals are independent of provider lookup layouts and entity properties.
8
9use std::ops::Bound;
10
11use sha2::{Digest, Sha256};
12
13use super::GraphIdentifierNamespace;
14use crate::mvcc::{SerializableKeySpace, SerializablePredicate};
15use crate::{GraphEntityFilter, GraphEntityKind, KeyValueBatch, StorageBackendResult};
16
17/// The source fields that affect graph identity selection or adjacency. Properties belong to separate entity payload observations.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum GraphEntityTopology<'a> {
20    Vertex {
21        label: &'a str,
22    },
23    Edge {
24        label: &'a str,
25        source: u64,
26        target: u64,
27    },
28}
29
30impl GraphEntityTopology<'_> {
31    pub fn kind(self) -> GraphEntityKind {
32        match self {
33            Self::Vertex { .. } => GraphEntityKind::Vertex,
34            Self::Edge { .. } => GraphEntityKind::Edge,
35        }
36    }
37
38    /// Supply each supported conjunction using the already evaluated source fields. Every key is bounded, and the batch retains the original allowance and participant.
39    pub fn observe_write(
40        self,
41        namespace: GraphIdentifierNamespace,
42        id: u64,
43        graph: Option<&str>,
44        batch: &mut dyn KeyValueBatch,
45    ) -> StorageBackendResult<()> {
46        let (label, endpoints) = match self {
47            Self::Vertex { label } => (label, None),
48            Self::Edge {
49                label,
50                source,
51                target,
52            } => (label, Some((source, target))),
53        };
54        for mask in 0..if endpoints.is_some() { 8 } else { 2 } {
55            let mut filter = GraphEntityFilter::new(self.kind(), graph);
56            filter.label = (mask & 1 != 0).then_some(label);
57            if let Some((source, target)) = endpoints {
58                filter.source = (mask & 2 != 0).then_some(source);
59                filter.target = (mask & 4 != 0).then_some(target);
60            }
61            let key = GraphSelectionKey::new(namespace, filter, None)?;
62            let mut point = key.lower;
63            point[33..].copy_from_slice(&id.to_be_bytes());
64            batch.observe_serializable_write(SerializablePredicate::point(
65                key.object,
66                SerializableKeySpace::Graph,
67                &point,
68            ))?;
69        }
70        Ok(())
71    }
72}
73
74/// A conjunction over graph identities, including empty results and the requested page suffix. Hashing the selector bounds retained key storage independently of label/name length; ordered entity IDs retain their range semantics.
75pub struct GraphSelectionKey {
76    object: [u8; 16],
77    lower: [u8; 41],
78    upper: [u8; 41],
79    after: bool,
80}
81
82impl GraphSelectionKey {
83    pub fn new(
84        namespace: GraphIdentifierNamespace,
85        filter: GraphEntityFilter<'_>,
86        after: Option<u64>,
87    ) -> StorageBackendResult<Self> {
88        filter.validate()?;
89        let mut digest = Sha256::new();
90        digest.update([kind_tag(filter.kind)]);
91        text(&mut digest, filter.graph);
92        text(&mut digest, filter.label);
93        for id in [filter.source, filter.target] {
94            digest.update([u8::from(id.is_some())]);
95            digest.update(id.unwrap_or_default().to_be_bytes());
96        }
97        let mut lower = [0; 41];
98        lower[0] = b's';
99        lower[1..33].copy_from_slice(&digest.finalize());
100        let mut upper = lower;
101        lower[33..].copy_from_slice(&after.unwrap_or_default().to_be_bytes());
102        upper[33..].copy_from_slice(&u64::MAX.to_be_bytes());
103        Ok(Self {
104            object: namespace.serializable_entity_object(),
105            lower,
106            upper,
107            after: after.is_some(),
108        })
109    }
110
111    pub fn predicate(&self) -> SerializablePredicate<'_> {
112        SerializablePredicate::range(
113            self.object,
114            SerializableKeySpace::Graph,
115            if self.after {
116                Bound::Excluded(&self.lower)
117            } else {
118                Bound::Included(&self.lower)
119            },
120            Bound::Included(&self.upper),
121        )
122    }
123}
124
125/// One entity's graph memberships, separated from its payload and from other entities of either kind.
126pub struct GraphMembershipKey {
127    object: [u8; 16],
128    lower: [u8; 42],
129    upper: [u8; 42],
130    point: bool,
131}
132
133impl GraphMembershipKey {
134    pub fn new(
135        namespace: GraphIdentifierNamespace,
136        kind: GraphEntityKind,
137        id: u64,
138        graph: Option<&str>,
139    ) -> Self {
140        let mut lower = [0; 42];
141        lower[0] = b'm';
142        lower[1] = kind_tag(kind);
143        lower[2..10].copy_from_slice(&id.to_be_bytes());
144        let mut upper = lower;
145        if let Some(graph) = graph {
146            lower[10..].copy_from_slice(&Sha256::digest(graph.as_bytes()));
147        } else {
148            upper[10..].fill(u8::MAX);
149        }
150        Self {
151            object: namespace.serializable_entity_object(),
152            lower,
153            upper,
154            point: graph.is_some(),
155        }
156    }
157
158    pub fn predicate(&self) -> SerializablePredicate<'_> {
159        if self.point {
160            SerializablePredicate::point(self.object, SerializableKeySpace::Graph, &self.lower)
161        } else {
162            SerializablePredicate::range(
163                self.object,
164                SerializableKeySpace::Graph,
165                Bound::Included(&self.lower),
166                Bound::Included(&self.upper),
167            )
168        }
169    }
170}
171
172fn kind_tag(kind: GraphEntityKind) -> u8 {
173    match kind {
174        GraphEntityKind::Vertex => b'v',
175        GraphEntityKind::Edge => b'e',
176    }
177}
178
179fn text(digest: &mut Sha256, value: Option<&str>) {
180    digest.update([u8::from(value.is_some())]);
181    let value = value.unwrap_or_default().as_bytes();
182    digest.update(
183        u64::try_from(value.len())
184            .expect("graph selector length")
185            .to_be_bytes(),
186    );
187    digest.update(value);
188}