Skip to main content

core_query/
view.rs

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