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