Skip to main content

core_query/
view.rs

1use std::collections::{HashMap, HashSet};
2
3use core_storage::property_index::{PropertyIndex, UnifiedIndex};
4use core_storage::v8::seam::{ColumnsView, EdgePropsView, TopologyView, ValueRef};
5use core_storage::{IdMap, Interner, Value};
6
7use crate::visible::VisibleSet;
8
9/// Read-only twin of `GraphMut`. Holds only borrowed graph state.
10///
11/// **Read node identity through the methods** ([`node_id`](Self::node_id),
12/// [`try_key_of`](Self::try_key_of), [`label_syms`](Self::label_syms),
13/// [`sym`](Self::sym), …), not the `ids` / `syms` / `labels` fields: under a
14/// [`layer`](Self::layer) those fields are the state *below* it — a reader
15/// snapshot's fold, under its delta tail and any transaction's writes — and
16/// only the methods lay the layer over them. `props`, `topo` and `edge_props`
17/// carry their own layer and read correctly either way.
18pub struct GraphView<'a> {
19    pub ids: &'a IdMap,
20    pub syms: &'a Interner,
21    pub labels: &'a [u32],
22    /// Overlay-over-base column store view.  For V5–V7 snapshots and fresh
23    /// databases, `base` is `None` and all column reads go to the overlay.
24    /// For V8 snapshots, `base` holds the archived columns from the mmap.
25    pub props: ColumnsView<'a>,
26    /// Overlay-over-base topology view. For V5–V7 snapshots and fresh
27    /// databases, `base` is `None` and all topology reads go to the owned
28    /// overlay. For V8 snapshots, `base` holds the archived CSR from the
29    /// mmap, and WAL-replayed edges accumulate in the overlay.
30    pub topo: TopologyView<'a>,
31    /// Overlay-over-base edge-property view.  For V8 snapshots the base
32    /// section is consulted zero-copy; the overlay holds only post-snapshot
33    /// changes.  Tombstones in the overlay mask deleted-from-base entries.
34    pub edge_props: EdgePropsView<'a>,
35    /// Optional query-scoped node visibility set. `None` = all nodes visible.
36    /// When `Some(set)`, only dense ids present in `set` are accessible.
37    ///
38    /// [`VisibleSet`] carries its own representation choice, so this field is
39    /// one probe whichever shape the mask took; see that type for the rule.
40    pub mask: Option<&'a VisibleSet>,
41    /// Optional equality index over scalar properties. `Some` on the primary
42    /// locked read path; `None` for MVCC reader snapshots (which fall back to a
43    /// scan). `IndexScan` consults it only when the `(label, field)` is declared.
44    pub prop_index: Option<&'a PropertyIndex>,
45    /// Node inserts, deletes, relabels and renames, and interned symbols,
46    /// laid over `ids`, `labels` and `syms`: a reader snapshot's unfolded
47    /// delta tail, plus an open transaction's own writes. `None` reads the
48    /// fields alone.
49    pub layer: Option<&'a NodeLayer>,
50}
51
52/// Changes to node identity laid over a fold's `IdMap`, labels and
53/// interner: the nodes inserted since (dense ids from `id_floor` up, exactly
54/// as the writer allocated or will allocate them), the nodes deleted,
55/// relabelled or renamed, and the symbols interned (from `sym_floor` up).
56///
57/// It is a layer, not a copy: it holds only what changed, and a [`GraphView`]
58/// reads it before the state below. Building one and reading through it cost
59/// nothing proportional to the store.
60#[derive(Debug, Clone)]
61pub struct NodeLayer {
62    /// `ids.len()` below: the first dense id this layer allocates.
63    id_floor: u32,
64    /// Key of each id from `id_floor`, in allocation order.
65    keys: Vec<String>,
66    /// Label symbol of each id from `id_floor`.
67    labels: Vec<u32>,
68    /// Live keys this layer resolves itself: its own ids, and ids from below
69    /// it renamed.
70    by_key: HashMap<String, u32>,
71    /// Ids deleted, from below or from this layer.
72    deleted: HashSet<u32>,
73    /// Ids from below whose label this layer replaced.
74    relabeled: HashMap<u32, u32>,
75    /// Ids from below whose key this layer replaced.
76    renamed: HashMap<u32, String>,
77    /// `syms.len()` below: the first symbol this layer allocates.
78    sym_floor: u32,
79    /// Text of each symbol from `sym_floor`.
80    syms: Vec<String>,
81    by_sym: HashMap<String, u32>,
82}
83
84impl NodeLayer {
85    /// An empty layer over the state whose ids and symbols are `ids` and
86    /// `syms`.
87    pub fn new(ids: &IdMap, syms: &Interner) -> Self {
88        Self {
89            id_floor: ids.len() as u32,
90            keys: Vec::new(),
91            labels: Vec::new(),
92            by_key: HashMap::new(),
93            deleted: HashSet::new(),
94            relabeled: HashMap::new(),
95            renamed: HashMap::new(),
96            sym_floor: syms.len() as u32,
97            syms: Vec::new(),
98            by_sym: HashMap::new(),
99        }
100    }
101
102    /// The symbol for `name`: the one below if it exists there, else this
103    /// layer's, allocated on first use — the id `Interner::intern` would give.
104    pub fn intern(&mut self, below: &Interner, name: &str) -> u32 {
105        if let Some(sym) = below.get(name) {
106            return sym;
107        }
108        if let Some(&sym) = self.by_sym.get(name) {
109            return sym;
110        }
111        let sym = self.sym_floor + self.syms.len() as u32;
112        self.syms.push(name.to_string());
113        self.by_sym.insert(name.to_string(), sym);
114        sym
115    }
116
117    /// Allocate the next dense id for a node `key` with label `label_sym` —
118    /// the id `IdMap::try_insert` would give. The caller has checked that
119    /// `key` is not live.
120    pub fn insert(&mut self, key: &str, label_sym: u32) -> u32 {
121        let id = self.id_floor + self.keys.len() as u32;
122        self.keys.push(key.to_string());
123        self.labels.push(label_sym);
124        self.by_key.insert(key.to_string(), id);
125        id
126    }
127
128    /// Give the live node `id` the label `label_sym`.
129    pub fn relabel(&mut self, id: u32, label_sym: u32) {
130        match id.checked_sub(self.id_floor) {
131            Some(i) => self.labels[i as usize] = label_sym,
132            None => {
133                self.relabeled.insert(id, label_sym);
134            }
135        }
136    }
137
138    /// Rename the live node `id` from `old` to `new`, keeping its id. The
139    /// caller has checked that `new` is not live.
140    pub fn rename(&mut self, id: u32, old: &str, new: &str) {
141        if self.by_key.get(old) == Some(&id) {
142            self.by_key.remove(old);
143        }
144        match id.checked_sub(self.id_floor) {
145            Some(i) => self.keys[i as usize] = new.to_string(),
146            None => {
147                self.renamed.insert(id, new.to_string());
148            }
149        }
150        self.by_key.insert(new.to_string(), id);
151    }
152
153    /// Retire the live node `key`, whose id is `id`.
154    pub fn delete(&mut self, key: &str, id: u32) {
155        if self.by_key.get(key) == Some(&id) {
156            self.by_key.remove(key);
157        }
158        self.deleted.insert(id);
159    }
160
161    #[inline]
162    fn is_deleted(&self, id: u32) -> bool {
163        !self.deleted.is_empty() && self.deleted.contains(&id)
164    }
165
166    /// The label of an id from below, as this layer leaves it.
167    #[inline]
168    fn below_label(&self, id: u32, below: u32) -> u32 {
169        if self.is_deleted(id) {
170            return u32::MAX;
171        }
172        if self.relabeled.is_empty() {
173            return below;
174        }
175        self.relabeled.get(&id).copied().unwrap_or(below)
176    }
177}
178
179/// Every node slot's label symbol in dense-id order, `u32::MAX` for a deleted
180/// or unlabelled slot. See [`GraphView::label_syms`].
181pub struct LabelSyms<'a> {
182    below: std::slice::Iter<'a, u32>,
183    layer: Option<&'a NodeLayer>,
184    /// The id of the next slot.
185    next: usize,
186}
187
188impl Iterator for LabelSyms<'_> {
189    type Item = u32;
190
191    #[inline]
192    fn next(&mut self) -> Option<u32> {
193        let i = self.next;
194        self.next += 1;
195        if let Some(&sym) = self.below.next() {
196            return Some(match self.layer {
197                None => sym,
198                Some(layer) => layer.below_label(i as u32, sym),
199            });
200        }
201        let layer = self.layer?;
202        let floor = layer.id_floor as usize;
203        if i < floor {
204            // Ids below allocated with no label slot (a gap).
205            return Some(u32::MAX);
206        }
207        let sym = *layer.labels.get(i - floor)?;
208        Some(if layer.is_deleted(i as u32) {
209            u32::MAX
210        } else {
211            sym
212        })
213    }
214
215    fn size_hint(&self) -> (usize, Option<usize>) {
216        let below = self.below.len();
217        match self.layer {
218            None => (below, Some(below)),
219            Some(_) => (below, None),
220        }
221    }
222}
223
224impl<'a> GraphView<'a> {
225    /// Returns `true` if `id` is visible under the current mask.
226    /// Always `true` when no mask is set.
227    #[inline]
228    pub fn visible(&self, id: u32) -> bool {
229        self.mask.is_none_or(|m| m.contains(id))
230    }
231
232    pub fn node_id(&self, key: &str) -> Option<u32> {
233        match self.layer {
234            None => self.ids.get(key),
235            Some(layer) => {
236                if let Some(&id) = layer.by_key.get(key) {
237                    return Some(id);
238                }
239                self.ids
240                    .get(key)
241                    .filter(|id| !layer.is_deleted(*id) && !layer.renamed.contains_key(id))
242            }
243        }
244    }
245
246    pub fn key_of(&self, id: u32) -> &str {
247        self.try_key_of(id).expect("dense ids")
248    }
249
250    /// The key of `id`, or `None` for an id that is retired or was never
251    /// allocated.
252    pub fn try_key_of(&self, id: u32) -> Option<&str> {
253        match self.layer {
254            None => self.ids.key_of(id),
255            Some(layer) => {
256                if layer.is_deleted(id) {
257                    return None;
258                }
259                match id.checked_sub(layer.id_floor) {
260                    Some(i) => layer.keys.get(i as usize).map(String::as_str),
261                    None => match layer.renamed.get(&id) {
262                        Some(key) => Some(key),
263                        None => self.ids.key_of(id),
264                    },
265                }
266            }
267        }
268    }
269
270    /// One past the highest dense id allocated, live or retired.
271    pub fn id_bound(&self) -> u32 {
272        match self.layer {
273            None => self.ids.len() as u32,
274            Some(layer) => layer.id_floor + layer.keys.len() as u32,
275        }
276    }
277
278    /// The symbol interned for `name`, if any.
279    pub fn sym(&self, name: &str) -> Option<u32> {
280        self.syms
281            .get(name)
282            .or_else(|| self.layer.and_then(|l| l.by_sym.get(name).copied()))
283    }
284
285    /// The text of symbol `sym`, if it is interned.
286    pub fn sym_name(&self, sym: u32) -> Option<&str> {
287        match self
288            .layer
289            .and_then(|l| sym.checked_sub(l.sym_floor).map(|i| (l, i)))
290        {
291            Some((layer, i)) => layer.syms.get(i as usize).map(String::as_str),
292            None => self.syms.resolve(sym),
293        }
294    }
295
296    /// The label symbol of every node slot, in dense-id order (`u32::MAX` for
297    /// a deleted or unlabelled slot) — what a label scan walks.
298    #[inline]
299    pub fn label_syms(&self) -> LabelSyms<'_> {
300        LabelSyms {
301            below: self.labels.iter(),
302            layer: self.layer,
303            next: 0,
304        }
305    }
306
307    /// The label symbol of node `id`, `None` for a deleted or unlabelled slot.
308    pub fn label_sym(&self, id: u32) -> Option<u32> {
309        let sym = match self.layer {
310            None => *self.labels.get(id as usize)?,
311            Some(layer) => match id.checked_sub(layer.id_floor) {
312                Some(i) if !layer.is_deleted(id) => *layer.labels.get(i as usize)?,
313                Some(_) => return None,
314                None => layer.below_label(id, *self.labels.get(id as usize)?),
315            },
316        };
317        (sym != u32::MAX).then_some(sym)
318    }
319
320    pub fn label_of(&self, id: u32) -> Option<&str> {
321        self.sym_name(self.label_sym(id)?)
322    }
323
324    /// Indexed lookup of node ids of `label` whose scalar `field` equals
325    /// `value`, filtered by the active mask. Returns `None` when no equality
326    /// index covers `(label, field)` (the caller should fall back to a scan);
327    /// `Some(ids)` — possibly empty — when the index answers the query.
328    pub fn nodes_with_prop(&self, label: &str, field: &str, value: &Value) -> Option<Vec<u32>> {
329        let index = self.prop_index?;
330        // An operand with no index key (a list, a map, a duration) can still
331        // equal a stored value, which the index does not hold: scan for it.
332        if !index.is_enabled(label, field) || !UnifiedIndex::answers_eq(value) {
333            return None;
334        }
335        let ids = index
336            .lookup(label, field, value)
337            .into_iter()
338            .filter(|&id| self.visible(id))
339            .collect();
340        Some(ids)
341    }
342
343    /// The range index a range or ordered scan of `(label, field)` may read:
344    /// `Some` only when a range index is declared on the pair, and the index
345    /// describes exactly the nodes this view reads: it is not a reader's
346    /// layered index (whose range lookups see only the layer's own entries),
347    /// and no node layer (a delta tail, a transaction's writes) lies over the
348    /// view. `None` means scan. The caller applies the mask.
349    pub fn range_index(&self, label: &str, field: &str) -> Option<&UnifiedIndex> {
350        let index = self.prop_index?;
351        let usable = self.layer.is_none()
352            && !index.is_layered()
353            && index.kind_of(label, field) == Some(core_storage::IndexKind::Range);
354        usable.then_some(index)
355    }
356
357    pub fn nodes_with_label(&self, label: &str) -> Vec<u32> {
358        let Some(sym) = self.sym(label) else {
359            return Vec::new();
360        };
361        let matching = |(i, s): (usize, u32)| if s == sym { Some(i as u32) } else { None };
362        match self.layer {
363            None => self
364                .labels
365                .iter()
366                .copied()
367                .enumerate()
368                .filter_map(matching)
369                .collect(),
370            Some(_) => self.label_syms().enumerate().filter_map(matching).collect(),
371        }
372    }
373
374    /// All non-tombstoned node ids regardless of label.
375    ///
376    /// Respects the query-scoped mask: when a mask is active only ids present
377    /// in the mask are returned, consistent with every other node accessor.
378    pub fn nodes_all(&self) -> Vec<u32> {
379        let live = |(i, s): (usize, u32)| {
380            if s != u32::MAX && self.visible(i as u32) {
381                Some(i as u32)
382            } else {
383                None
384            }
385        };
386        match self.layer {
387            None => self
388                .labels
389                .iter()
390                .copied()
391                .enumerate()
392                .filter_map(live)
393                .collect(),
394            Some(_) => self.label_syms().enumerate().filter_map(live).collect(),
395        }
396    }
397
398    /// Look up the property `field` for node `id`.
399    ///
400    /// Returns `ValueRef::Borrowed` for overlay hits (zero allocation) and
401    /// `ValueRef::Owned` for base-section hits (value materialised from
402    /// archived data).  Returns `None` when neither overlay nor base has a
403    /// value for `(id, field)`.
404    pub fn prop(&self, id: u32, field: &str) -> Option<ValueRef<'_>> {
405        self.props.get(id, field)
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::{GraphView, VisibleSet};
412    use core_storage::v8::seam::{ColumnsView, EdgePropsView, TopologyView};
413    use core_storage::{ColumnStore, EdgeProps, IdMap, Interner, Topology, Value};
414
415    struct Fx {
416        ids: IdMap,
417        syms: Interner,
418        labels: Vec<u32>,
419        props: ColumnStore,
420        topo: Topology,
421        eprops: EdgeProps,
422    }
423
424    impl Fx {
425        fn new() -> Self {
426            Fx {
427                ids: IdMap::new(),
428                syms: Interner::new(),
429                labels: vec![],
430                props: ColumnStore::new(),
431                topo: Topology::new(),
432                eprops: EdgeProps::new(),
433            }
434        }
435
436        fn add(&mut self, label: &str, key: &str, props: Vec<(&str, Value)>) -> u32 {
437            let id = self.ids.get_or_insert(key);
438            let sym = self.syms.intern(label);
439            self.labels.resize(id as usize + 1, u32::MAX);
440            self.labels[id as usize] = sym;
441            for (f, v) in props {
442                self.props.set(id, f, v);
443            }
444            id
445        }
446
447        fn view(&self) -> GraphView<'_> {
448            GraphView {
449                ids: &self.ids,
450                syms: &self.syms,
451                labels: &self.labels,
452                props: ColumnsView::owned(&self.props),
453                topo: TopologyView::owned(&self.topo),
454                edge_props: EdgePropsView::owned(&self.eprops),
455                mask: None,
456                prop_index: None,
457                layer: None,
458            }
459        }
460    }
461
462    #[test]
463    fn prop_returns_none_for_missing() {
464        let mut fx = Fx::new();
465        let id = fx.add("N", "alice", vec![("age", Value::Int(36))]);
466        let v = fx.view();
467        assert_eq!(
468            v.prop(id, "age").map(|vr| vr.into_value()),
469            Some(Value::Int(36))
470        );
471        assert!(v.prop(id, "missing").is_none());
472    }
473
474    #[test]
475    fn graph_view_lookups() {
476        let mut fx = Fx::new();
477        let id = fx.add("Person", "ada", vec![("age", Value::Int(36))]);
478        let v = fx.view();
479        assert_eq!(v.node_id("ada"), Some(id));
480        assert_eq!(v.node_id("zzz"), None);
481        assert_eq!(v.key_of(id), "ada");
482        assert_eq!(v.label_of(id), Some("Person"));
483        assert_eq!(v.label_of(99), None);
484        assert_eq!(
485            v.prop(id, "age").map(|vr| vr.into_value()),
486            Some(Value::Int(36))
487        );
488        assert_eq!(v.prop(id, "missing"), None);
489    }
490
491    #[test]
492    fn gap_sentinel_is_not_a_label() {
493        let mut fx = Fx::new();
494        let kept = fx.add("Person", "ada", vec![]);
495        fx.ids.get_or_insert("ghost");
496        fx.labels.resize(2, u32::MAX);
497        let later = fx.add("Person", "bob", vec![]);
498        let v = fx.view();
499        assert_eq!(v.label_of(1), None);
500        assert_eq!(v.nodes_with_label("Person"), vec![kept, later]);
501    }
502
503    #[test]
504    fn nodes_with_label_skips_tombstoned_id() {
505        let mut fx = Fx::new();
506        let ada = fx.add("Person", "ada", vec![]);
507        let bob = fx.add("Person", "bob", vec![]);
508        fx.ids.delete("ada");
509        fx.labels[ada as usize] = u32::MAX;
510        let v = fx.view();
511        assert_eq!(v.node_id("ada"), None);
512        assert_eq!(v.label_of(ada), None);
513        assert_eq!(v.nodes_with_label("Person"), vec![bob]);
514    }
515
516    #[test]
517    fn nodes_with_label_dense_id_order_and_unknown_empty() {
518        let mut fx = Fx::new();
519        let bob = fx.add("Person", "bob", vec![]);
520        let ada = fx.add("Person", "ada", vec![]);
521        let _acme = fx.add("Company", "acme", vec![]);
522        let v = fx.view();
523        assert_eq!(v.nodes_with_label("Person"), vec![bob, ada]);
524        assert_eq!(v.nodes_with_label("Person"), vec![0, 1]);
525        assert!(v.nodes_with_label("Nope").is_empty());
526    }
527
528    #[test]
529    fn nodes_all_respects_mask() {
530        let mut fx = Fx::new();
531        let alice = fx.add("Person", "alice", vec![]);
532        let bob = fx.add("Person", "bob", vec![]);
533        let carol = fx.add("Person", "carol", vec![]);
534
535        // Unmask: all three visible.
536        let v_full = fx.view();
537        let all = v_full.nodes_all();
538        assert!(all.contains(&alice));
539        assert!(all.contains(&bob));
540        assert!(all.contains(&carol));
541
542        // Masked: only alice and carol visible.
543        let visible: VisibleSet = [alice, carol].into_iter().collect();
544        let v_masked = GraphView {
545            ids: &fx.ids,
546            syms: &fx.syms,
547            labels: &fx.labels,
548            props: ColumnsView::owned(&fx.props),
549            topo: TopologyView::owned(&fx.topo),
550            edge_props: EdgePropsView::owned(&fx.eprops),
551            mask: Some(&visible),
552            prop_index: None,
553            layer: None,
554        };
555        let masked = v_masked.nodes_all();
556        assert!(masked.contains(&alice));
557        assert!(!masked.contains(&bob), "masked node must be excluded");
558        assert!(masked.contains(&carol));
559    }
560
561    #[test]
562    fn layer_inserts_deletes_and_resurrects_without_touching_below() {
563        let mut fx = Fx::new();
564        let ada = fx.add("Person", "ada", vec![]);
565        let bob = fx.add("Person", "bob", vec![]);
566        let mut tx = super::NodeLayer::new(&fx.ids, &fx.syms);
567        let company = tx.intern(&fx.syms, "Company");
568        let person = tx.intern(&fx.syms, "Person");
569        let acme = tx.insert("acme", company);
570        tx.delete("ada", ada);
571        let ada2 = tx.insert("ada", person);
572        let base_view = fx.view();
573        let v = GraphView {
574            layer: Some(&tx),
575            ..fx.view()
576        };
577        assert_eq!(acme, 2);
578        assert_eq!(
579            ada2, 3,
580            "a re-created key gets a fresh id, as the commit gives it"
581        );
582        assert_eq!(v.node_id("ada"), Some(ada2));
583        assert_eq!(v.try_key_of(ada), None);
584        assert_eq!(v.key_of(acme), "acme");
585        assert_eq!(v.label_of(acme), Some("Company"));
586        assert_eq!(v.id_bound(), 4);
587        assert_eq!(v.nodes_with_label("Person"), vec![bob, ada2]);
588        assert_eq!(v.nodes_with_label("Company"), vec![acme]);
589        assert_eq!(v.nodes_all(), vec![bob, acme, ada2]);
590        // Below is untouched.
591        assert_eq!(base_view.node_id("ada"), Some(ada));
592        assert_eq!(base_view.sym("Company"), None);
593        assert_eq!(base_view.nodes_all(), vec![ada, bob]);
594    }
595}