Skip to main content

core_query/
view.rs

1use core_storage::v8::seam::{ColumnsView, EdgePropsView, TopologyView, ValueRef};
2use core_storage::{IdMap, Interner};
3use std::collections::HashSet;
4
5/// Read-only twin of `GraphMut`. Holds only borrowed graph state.
6pub struct GraphView<'a> {
7    pub ids: &'a IdMap,
8    pub syms: &'a Interner,
9    pub labels: &'a [u32],
10    /// Overlay-over-base column store view.  For V5–V7 snapshots and fresh
11    /// databases, `base` is `None` and all column reads go to the overlay.
12    /// For V8 snapshots, `base` holds the archived columns from the mmap.
13    pub props: ColumnsView<'a>,
14    /// Overlay-over-base topology view. For V5–V7 snapshots and fresh
15    /// databases, `base` is `None` and all topology reads go to the owned
16    /// overlay. For V8 snapshots, `base` holds the archived CSR from the
17    /// mmap, and WAL-replayed edges accumulate in the overlay.
18    pub topo: TopologyView<'a>,
19    /// Overlay-over-base edge-property view.  For V8 snapshots the base
20    /// section is consulted zero-copy; the overlay holds only post-snapshot
21    /// changes.  Tombstones in the overlay mask deleted-from-base entries.
22    pub edge_props: EdgePropsView<'a>,
23    /// Optional query-scoped node visibility set. `None` = all nodes visible.
24    /// When `Some(set)`, only dense ids present in `set` are accessible.
25    pub mask: Option<&'a HashSet<u32>>,
26}
27
28impl<'a> GraphView<'a> {
29    /// Returns `true` if `id` is visible under the current mask.
30    /// Always `true` when no mask is set.
31    #[inline]
32    pub fn visible(&self, id: u32) -> bool {
33        self.mask.is_none_or(|m| m.contains(&id))
34    }
35
36    pub fn node_id(&self, key: &str) -> Option<u32> {
37        self.ids.get(key)
38    }
39
40    pub fn key_of(&self, id: u32) -> &str {
41        self.ids.key_of(id).expect("dense ids")
42    }
43
44    pub fn label_of(&self, id: u32) -> Option<&str> {
45        let sym = *self.labels.get(id as usize)?;
46        if sym == u32::MAX {
47            return None;
48        }
49        self.syms.resolve(sym)
50    }
51
52    pub fn nodes_with_label(&self, label: &str) -> Vec<u32> {
53        let Some(sym) = self.syms.get(label) else {
54            return Vec::new();
55        };
56        self.labels
57            .iter()
58            .enumerate()
59            .filter_map(|(i, &s)| if s == sym { Some(i as u32) } else { None })
60            .collect()
61    }
62
63    /// All non-tombstoned node ids regardless of label.
64    ///
65    /// Respects the query-scoped mask: when a mask is active only ids present
66    /// in the mask are returned, consistent with every other node accessor.
67    pub fn nodes_all(&self) -> Vec<u32> {
68        self.labels
69            .iter()
70            .enumerate()
71            .filter_map(|(i, &s)| {
72                if s != u32::MAX && self.visible(i as u32) {
73                    Some(i as u32)
74                } else {
75                    None
76                }
77            })
78            .collect()
79    }
80
81    /// Look up the property `field` for node `id`.
82    ///
83    /// Returns `ValueRef::Borrowed` for overlay hits (zero allocation) and
84    /// `ValueRef::Owned` for base-section hits (value materialised from
85    /// archived data).  Returns `None` when neither overlay nor base has a
86    /// value for `(id, field)`.
87    pub fn prop(&self, id: u32, field: &str) -> Option<ValueRef<'_>> {
88        self.props.get(id, field)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::GraphView;
95    use core_storage::v8::seam::{ColumnsView, EdgePropsView, TopologyView};
96    use core_storage::{ColumnStore, EdgeProps, IdMap, Interner, Topology, Value};
97
98    struct Fx {
99        ids: IdMap,
100        syms: Interner,
101        labels: Vec<u32>,
102        props: ColumnStore,
103        topo: Topology,
104        eprops: EdgeProps,
105    }
106
107    impl Fx {
108        fn new() -> Self {
109            Fx {
110                ids: IdMap::new(),
111                syms: Interner::new(),
112                labels: vec![],
113                props: ColumnStore::new(),
114                topo: Topology::new(),
115                eprops: EdgeProps::new(),
116            }
117        }
118
119        fn add(&mut self, label: &str, key: &str, props: Vec<(&str, Value)>) -> u32 {
120            let id = self.ids.get_or_insert(key);
121            let sym = self.syms.intern(label);
122            self.labels.resize(id as usize + 1, u32::MAX);
123            self.labels[id as usize] = sym;
124            for (f, v) in props {
125                self.props.set(id, f, v);
126            }
127            id
128        }
129
130        fn view(&self) -> GraphView<'_> {
131            GraphView {
132                ids: &self.ids,
133                syms: &self.syms,
134                labels: &self.labels,
135                props: ColumnsView::owned(&self.props),
136                topo: TopologyView::owned(&self.topo),
137                edge_props: EdgePropsView::owned(&self.eprops),
138                mask: None,
139            }
140        }
141    }
142
143    #[test]
144    fn prop_returns_none_for_missing() {
145        let mut fx = Fx::new();
146        let id = fx.add("N", "alice", vec![("age", Value::Int(36))]);
147        let v = fx.view();
148        assert_eq!(
149            v.prop(id, "age").map(|vr| vr.into_value()),
150            Some(Value::Int(36))
151        );
152        assert!(v.prop(id, "missing").is_none());
153    }
154
155    #[test]
156    fn graph_view_lookups() {
157        let mut fx = Fx::new();
158        let id = fx.add("Person", "ada", vec![("age", Value::Int(36))]);
159        let v = fx.view();
160        assert_eq!(v.node_id("ada"), Some(id));
161        assert_eq!(v.node_id("zzz"), None);
162        assert_eq!(v.key_of(id), "ada");
163        assert_eq!(v.label_of(id), Some("Person"));
164        assert_eq!(v.label_of(99), None);
165        assert_eq!(
166            v.prop(id, "age").map(|vr| vr.into_value()),
167            Some(Value::Int(36))
168        );
169        assert_eq!(v.prop(id, "missing"), None);
170    }
171
172    #[test]
173    fn gap_sentinel_is_not_a_label() {
174        let mut fx = Fx::new();
175        let kept = fx.add("Person", "ada", vec![]);
176        fx.ids.get_or_insert("ghost");
177        fx.labels.resize(2, u32::MAX);
178        let later = fx.add("Person", "bob", vec![]);
179        let v = fx.view();
180        assert_eq!(v.label_of(1), None);
181        assert_eq!(v.nodes_with_label("Person"), vec![kept, later]);
182    }
183
184    #[test]
185    fn nodes_with_label_skips_tombstoned_id() {
186        let mut fx = Fx::new();
187        let ada = fx.add("Person", "ada", vec![]);
188        let bob = fx.add("Person", "bob", vec![]);
189        fx.ids.delete("ada");
190        fx.labels[ada as usize] = u32::MAX;
191        let v = fx.view();
192        assert_eq!(v.node_id("ada"), None);
193        assert_eq!(v.label_of(ada), None);
194        assert_eq!(v.nodes_with_label("Person"), vec![bob]);
195    }
196
197    #[test]
198    fn nodes_with_label_dense_id_order_and_unknown_empty() {
199        let mut fx = Fx::new();
200        let bob = fx.add("Person", "bob", vec![]);
201        let ada = fx.add("Person", "ada", vec![]);
202        let _acme = fx.add("Company", "acme", vec![]);
203        let v = fx.view();
204        assert_eq!(v.nodes_with_label("Person"), vec![bob, ada]);
205        assert_eq!(v.nodes_with_label("Person"), vec![0, 1]);
206        assert!(v.nodes_with_label("Nope").is_empty());
207    }
208
209    #[test]
210    fn nodes_all_respects_mask() {
211        use std::collections::HashSet;
212        let mut fx = Fx::new();
213        let alice = fx.add("Person", "alice", vec![]);
214        let bob = fx.add("Person", "bob", vec![]);
215        let carol = fx.add("Person", "carol", vec![]);
216
217        // Unmask: all three visible.
218        let v_full = fx.view();
219        let all = v_full.nodes_all();
220        assert!(all.contains(&alice));
221        assert!(all.contains(&bob));
222        assert!(all.contains(&carol));
223
224        // Masked: only alice and carol visible.
225        let visible: HashSet<u32> = [alice, carol].into_iter().collect();
226        let v_masked = GraphView {
227            ids: &fx.ids,
228            syms: &fx.syms,
229            labels: &fx.labels,
230            props: ColumnsView::owned(&fx.props),
231            topo: TopologyView::owned(&fx.topo),
232            edge_props: EdgePropsView::owned(&fx.eprops),
233            mask: Some(&visible),
234        };
235        let masked = v_masked.nodes_all();
236        assert!(masked.contains(&alice));
237        assert!(!masked.contains(&bob), "masked node must be excluded");
238        assert!(masked.contains(&carol));
239    }
240}