Skip to main content

lora_store/memory/
graph.rs

1//! The [`InMemoryGraph`] data structure: slot-indexed node/relationship
2//! storage, adjacency lists, label/type indexes, and the inherent
3//! helpers that the trait impls in `super::impls` delegate to.
4
5use std::collections::BTreeMap;
6use std::sync::atomic::{AtomicUsize, Ordering};
7use std::sync::{Arc, RwLock, RwLockWriteGuard};
8
9use lora_ast::Direction;
10
11use crate::{
12    DeletedRecordSink, Labels, LoraPoint, MutationEvent, MutationRecorder, NodeId, NodeRecord,
13    Properties, PropertyValue, RelationshipId, RelationshipRecord,
14};
15
16use super::adjacency::{self, AdjEntry, AdjSide, Adjacency, Side, TypeFilter};
17use super::chunked_vec::ChunkedVec;
18use super::constraint_catalog::{
19    ConstraintBacking, ConstraintCatalog, ConstraintRequest, CreateConstraintError,
20    CreateConstraintOutcome, DropConstraintError, DropConstraintOutcome,
21};
22use super::distinct_stats::DistinctStatsRegistry;
23use super::entity_index_store::{
24    read_shared, share, write_shared, IndexBundle, IndexRead, IndexWrite,
25};
26use super::fulltext_index::FulltextRegistry;
27use super::hnsw::HnswParams;
28use super::id_bitmap::IdBitmap;
29use super::index_catalog::{
30    CreateIndexError, CreateIndexOutcome, DropIndexError, DropIndexOutcome, IndexCatalog,
31    IndexConfigValue, IndexDefinition, IndexRequest, StoredIndexEntity, StoredIndexKind,
32    StoredIndexState,
33};
34use super::point_index::PointRegistry;
35use super::property_index::{
36    EntityValues, PropertyIndexKey, PropertyIndexRegistry, PropertyIndexState, UNLABELLED,
37};
38use super::record_slab::{BuiltLeaves, RecordSlab};
39use super::secondary_index_maintenance::SecondaryIndexMutation;
40use super::sorted_property_index::SortedPropertyIndex;
41use super::stats::GraphStats;
42use super::text_index::TrigramRegistry;
43use super::vector_index::{VectorIndexProvider, VectorIndexRegistry, VectorSimilarity};
44use crate::dict::Dicts;
45use crate::encoded::{self, Scratch, StoredNode, StoredRel};
46use crate::{NodeRef, RelRef};
47
48/// The largest node or relationship id the store accepts. Slot storage
49/// grows with the highest id ever used (about 16 bytes per 512 ids even
50/// when the ids are deleted), so a corrupt snapshot or WAL claiming an id
51/// near `u64::MAX` would otherwise allocate until the process aborts. At
52/// 2^34 the worst case is about 512 MiB of slot tables, and no real
53/// database allocates 17 billion ids.
54pub(crate) const MAX_ENTITY_ID: u64 = (1 << 34) - 1;
55
56/// Adjacency entries a batch delete still has to remove: node, side,
57/// relationship. See `InMemoryGraph::flush_adjacency_removals`.
58pub(super) type PendingAdjRemovals = Vec<(NodeId, Side, RelationshipId)>;
59
60/// Lists up to this many bytes (about 32 entries) lose an entry at once
61/// even in a batch delete: scanning them costs less than deferring.
62const DEFER_REMOVAL_MIN_BYTES: usize = 256;
63
64#[derive(Default)]
65pub struct InMemoryGraph {
66    pub(super) next_node_id: NodeId,
67    pub(super) next_rel_id: RelationshipId,
68
69    /// Slot-indexed node storage: slot `id` holds the node `id`, encoded
70    /// (see [`crate::encoded`]). Empty slots are tombstones from deletes
71    /// (we don't compact). Because `next_node_id` is monotonic the slot at
72    /// `id` exists exactly when `id < next_node_id`.
73    ///
74    /// [`Self::clone`] (called on every staged write to build a working
75    /// copy, and for every reader snapshot) shares this storage: a
76    /// [`RecordSlab`] clone is one refcount bump, so the whole-graph
77    /// clone is O(#labels + #relationship types), not O(N). A write
78    /// copies only the leaf path it touches: a leaf's offsets, and at
79    /// most one buffer of 64 records. A record is immutable: changing it
80    /// stores a new encoding in its slot (see [`Self::update_node`]).
81    pub(super) nodes: RecordSlab,
82    pub(super) relationships: RecordSlab,
83    /// Numbers the labels, relationship types and property keys the
84    /// records and adjacency entries refer to. Shared with clones until
85    /// one of them sees a new name.
86    pub(super) dicts: Dicts,
87    /// Live (non-tombstoned) counts kept in sync with `put_*`/`take_*` so
88    /// `node_count` / `relationship_count` stay O(1) — without a counter
89    /// they'd have to scan the slab.
90    pub(super) live_node_count: usize,
91    pub(super) live_rel_count: usize,
92
93    /// Adjacency keyed by NodeId. `adjacency[id]` lists the relationships
94    /// that leave `id` as `(type, target, relationship id)` and those that
95    /// enter it with the source as the neighbour, in one allocation. A hop
96    /// reads only the list: it never touches a relationship record. See
97    /// [`super::adjacency`].
98    pub(super) adjacency: ChunkedVec<Adjacency>,
99
100    // secondary indexes
101    /// Label -> the ids of the nodes that carry it, ascending. An
102    /// [`IdBitmap`] holds an id in two bytes or less (an eighth of a byte
103    /// for a label most nodes carry), where a list held eight, and
104    /// removes one by binary search.
105    ///
106    /// Keyed by `Arc<str>`: every staged write clones the graph, and with
107    /// `String` keys that cloned (allocated) every label and type name.
108    pub(super) nodes_by_label: BTreeMap<Arc<str>, IdBitmap>,
109    pub(super) relationships_by_type: BTreeMap<Arc<str>, IdBitmap>,
110
111    /// All index machinery — the declared-index catalog, hash-bucket
112    /// property registry, and the per-entity-kind secondary index
113    /// registries (text, sorted, point, fulltext) plus their active
114    /// counters — collapsed into one bundle. See [`IndexBundle`] for
115    /// the rationale. The bundle is a packaging-only abstraction:
116    /// every field accessed through `self.indexes.<x>` lives at the
117    /// same address it would have as a top-level field.
118    pub(super) indexes: IndexBundle,
119
120    /// Per-(label / type, property key) distinct-value sketches for the
121    /// planner (see [`super::distinct_stats`]). Kept for every key, not
122    /// only indexed ones, and a function of the live data alone, so
123    /// plans don't depend on index activation, lookup history or
124    /// restarts. Cloning is four refcount bumps.
125    pub(super) distinct_stats: DistinctStatsRegistry,
126
127    /// Catalog of explicitly-created constraints (CREATE CONSTRAINT).
128    /// Deliberately not part of [`IndexBundle`] — constraints describe
129    /// data invariants, not indexed access. The fact that uniqueness /
130    /// key constraints back range indexes is handled in the
131    /// constraint code path, not by the bundle's layout.
132    pub(super) constraint_catalog: RwLock<Arc<ConstraintCatalog>>,
133    /// Fast-path counter for mutation-time constraint checks. Most
134    /// workloads have no constraints installed; this lets the executor
135    /// skip taking the catalog lock in that case.
136    pub(super) active_constraints: AtomicUsize,
137
138    /// Optional mutation observer. When `Some`, every committed mutation
139    /// fans out to this recorder *after* the in-memory state has been
140    /// updated. The recorder is not part of the graph's identity, so Clone
141    /// and snapshot restore both reset it to `None`.
142    pub(super) recorder: Option<Arc<dyn MutationRecorder>>,
143
144    /// Optional sink that sees each node / relationship record just before
145    /// a delete drops it. Change feeds use it to report deleted entities
146    /// without copying the graph. Like the recorder, it is not part of the
147    /// graph's identity and is dropped on clone.
148    pub(super) deleted_sink: Option<Arc<dyn DeletedRecordSink>>,
149}
150
151impl std::fmt::Debug for InMemoryGraph {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        f.debug_struct("InMemoryGraph")
154            .field("next_node_id", &self.next_node_id)
155            .field("next_rel_id", &self.next_rel_id)
156            .field(
157                "nodes",
158                &self.iter_nodes().map(|(_, n)| n).collect::<Vec<_>>(),
159            )
160            .field(
161                "relationships",
162                &self.iter_rels().map(|(_, r)| r).collect::<Vec<_>>(),
163            )
164            .field("adjacency", &self.adjacency)
165            .field("nodes_by_label", &self.nodes_by_label)
166            .field("relationships_by_type", &self.relationships_by_type)
167            .field("indexes", &self.indexes)
168            .field(
169                "active_node_property_indexes",
170                &self.active_node_property_index_count(),
171            )
172            .field(
173                "active_relationship_property_indexes",
174                &self.active_relationship_property_index_count(),
175            )
176            .field(
177                "index_catalog_entries",
178                &self
179                    .indexes
180                    .catalog
181                    .read()
182                    .map(|c| c.list().len())
183                    .unwrap_or(0),
184            )
185            .field("active_constraints", &self.active_constraint_count())
186            .field(
187                "active_fulltext_indexes",
188                &self.active_fulltext_index_count(),
189            )
190            .field("recorder", &self.recorder.as_ref().map(|_| "installed"))
191            .finish()
192    }
193}
194
195impl Clone for InMemoryGraph {
196    fn clone(&self) -> Self {
197        // Deliberately drop the recorder on clone: a cloned store is a
198        // separate identity; it should not silently share the observer.
199        Self {
200            next_node_id: self.next_node_id,
201            next_rel_id: self.next_rel_id,
202            nodes: self.nodes.clone(),
203            relationships: self.relationships.clone(),
204            live_node_count: self.live_node_count,
205            live_rel_count: self.live_rel_count,
206            adjacency: self.adjacency.clone(),
207            dicts: self.dicts.clone(),
208            nodes_by_label: self.nodes_by_label.clone(),
209            relationships_by_type: self.relationships_by_type.clone(),
210            // Shares every registry and catalog; each is copied on its
211            // first write (see `IndexBundle`).
212            indexes: self.indexes.clone(),
213            distinct_stats: self.distinct_stats.clone(),
214            constraint_catalog: share(&self.constraint_catalog),
215            active_constraints: AtomicUsize::new(self.active_constraint_count()),
216            recorder: None,
217            deleted_sink: None,
218        }
219    }
220}
221
222impl InMemoryGraph {
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    /// Kept for API compatibility. The chunked storage allocates full
228    /// chunks as the graph grows, so there is no repeated doubling for a
229    /// capacity hint to avoid.
230    pub fn with_capacity_hint(_nodes: usize, _relationships: usize) -> Self {
231        Self::default()
232    }
233
234    pub fn contains_node(&self, node_id: NodeId) -> bool {
235        self.has_node_at(node_id)
236    }
237
238    pub fn contains_relationship(&self, rel_id: RelationshipId) -> bool {
239        self.has_rel_at(rel_id)
240    }
241
242    /// Install (or clear) the mutation recorder. Passing `None` detaches any
243    /// currently-installed recorder. The recorder observes every committed
244    /// mutation *after* it has been applied.
245    pub fn set_mutation_recorder(&mut self, recorder: Option<Arc<dyn MutationRecorder>>) {
246        self.recorder = recorder;
247    }
248
249    /// Install (or clear) the [`DeletedRecordSink`].
250    pub fn set_deleted_record_sink(&mut self, sink: Option<Arc<dyn DeletedRecordSink>>) {
251        self.deleted_sink = sink;
252    }
253
254    /// Handle to the currently-installed recorder, if any.
255    pub fn mutation_recorder(&self) -> Option<&Arc<dyn MutationRecorder>> {
256        self.recorder.as_ref()
257    }
258
259    /// Emit a mutation event only if a recorder is installed. The event is
260    /// built lazily — callers pass a closure, so when no recorder is
261    /// attached we pay only a `None` check and the cost of constructing the
262    /// event (labels/properties clones) is avoided.
263    #[inline]
264    pub(super) fn emit<F: FnOnce() -> MutationEvent>(&self, build: F) {
265        if let Some(rec) = &self.recorder {
266            rec.record(build());
267        }
268    }
269
270    fn bump_next_node_id_past(&mut self, id: NodeId) -> Result<(), String> {
271        let next = id
272            .checked_add(1)
273            .ok_or_else(|| format!("node id {id} leaves no valid next node id"))?;
274        self.next_node_id = self.next_node_id.max(next);
275        Ok(())
276    }
277
278    fn bump_next_rel_id_past(&mut self, id: RelationshipId) -> Result<(), String> {
279        let next = id
280            .checked_add(1)
281            .ok_or_else(|| format!("relationship id {id} leaves no valid next relationship id"))?;
282        self.next_rel_id = self.next_rel_id.max(next);
283        Ok(())
284    }
285
286    pub(super) fn try_reserve_next_node_slot(&mut self) -> Option<(NodeId, usize)> {
287        let id = self.next_node_id;
288        let idx = self.ensure_node_slot_checked(id).ok()?;
289        self.bump_next_node_id_past(id).ok()?;
290        Some((id, idx))
291    }
292
293    pub(super) fn try_reserve_next_rel_slot(&mut self) -> Option<(RelationshipId, usize)> {
294        let id = self.next_rel_id;
295        let idx = self.ensure_rel_slot_checked(id).ok()?;
296        self.bump_next_rel_id_past(id).ok()?;
297        Some((id, idx))
298    }
299
300    // ---------- Slab access helpers ----------
301    //
302    // Slots are indexed by id. Readers get views over the encoded
303    // records; nothing here hands out the bytes.
304
305    #[inline]
306    pub(super) fn node_at(&self, id: NodeId) -> Option<NodeRef<'_>> {
307        let bytes = self.nodes.get(Self::slot_index(id)?)?;
308        Some(NodeRef::stored(StoredNode::new(id, bytes, &self.dicts)))
309    }
310
311    #[inline]
312    pub(super) fn has_node_at(&self, id: NodeId) -> bool {
313        Self::slot_index(id).is_some_and(|idx| self.nodes.contains(idx))
314    }
315
316    #[inline]
317    pub(super) fn rel_at(&self, id: RelationshipId) -> Option<RelRef<'_>> {
318        let bytes = self.relationships.get(Self::slot_index(id)?)?;
319        Some(RelRef::stored(StoredRel::new(id, bytes, &self.dicts)))
320    }
321
322    #[inline]
323    pub(super) fn has_rel_at(&self, id: RelationshipId) -> bool {
324        Self::slot_index(id).is_some_and(|idx| self.relationships.contains(idx))
325    }
326
327    /// `(source, target)` of a relationship, reading only the start of
328    /// its record.
329    #[inline]
330    pub(super) fn rel_endpoints_at(&self, id: RelationshipId) -> Option<(NodeId, NodeId)> {
331        let bytes = self.relationships.get(Self::slot_index(id)?)?;
332        Some(encoded::rel_endpoints(bytes))
333    }
334
335    /// Change a node: decode it, let `change` edit the record, and store
336    /// the new encoding. Records are immutable, so snapshots that share
337    /// the old one keep reading it.
338    pub(super) fn update_node<R>(
339        &mut self,
340        id: NodeId,
341        change: impl FnOnce(&mut NodeRecord) -> R,
342    ) -> Option<R> {
343        let idx = Self::slot_index(id)?;
344        let mut record = self.node_at(id)?.to_record();
345        let result = change(&mut record);
346        self.store_node(idx, &record);
347        Some(result)
348    }
349
350    /// Set or remove one property of a node, rewriting only that entry of
351    /// its record. Returns the property's previous value; `None` when the
352    /// node does not exist or there was nothing to remove.
353    pub(super) fn edit_node_property(
354        &mut self,
355        id: NodeId,
356        key: &str,
357        edit: encoded::PropEdit<'_>,
358    ) -> Option<Option<PropertyValue>> {
359        let idx = Self::slot_index(id)?;
360        edit_record_property(
361            &mut self.nodes,
362            idx,
363            encoded::RecordKind::Node,
364            &mut self.dicts,
365            key,
366            edit,
367        )
368    }
369
370    /// Apply `edits` to node `id`'s record in order and store the result
371    /// once: a staged write stores a record version per edit otherwise,
372    /// each appended to its segment. Per edit, the previous value
373    /// (`Some(None)` when there was none), or `None` for a remove that
374    /// found nothing. `None` when the node does not exist.
375    pub(super) fn edit_node_properties(
376        &mut self,
377        id: NodeId,
378        edits: &[(String, Option<PropertyValue>)],
379    ) -> Option<Vec<Option<Option<PropertyValue>>>> {
380        let idx = Self::slot_index(id)?;
381        let stored = self.nodes.get(idx)?;
382        let mut current = Scratch::take();
383        current.extend_from_slice(stored);
384        let mut next = Vec::with_capacity(current.len() + 64);
385        let mut olds = Vec::with_capacity(edits.len());
386        let mut changed = false;
387        for (key, value) in edits {
388            let edit = match value {
389                Some(value) => encoded::PropEdit::Set(value),
390                None => encoded::PropEdit::Remove,
391            };
392            let old = encoded::edit_property(
393                &current,
394                encoded::RecordKind::Node,
395                &mut self.dicts,
396                key,
397                edit,
398                &mut next,
399            );
400            if old.is_some() {
401                std::mem::swap(&mut *current, &mut next);
402                changed = true;
403            }
404            olds.push(old);
405        }
406        if changed {
407            self.nodes.set(idx, &current);
408        }
409        Some(olds)
410    }
411
412    pub(super) fn edit_rel_property(
413        &mut self,
414        id: RelationshipId,
415        key: &str,
416        edit: encoded::PropEdit<'_>,
417    ) -> Option<Option<PropertyValue>> {
418        let idx = Self::slot_index(id)?;
419        edit_record_property(
420            &mut self.relationships,
421            idx,
422            encoded::RecordKind::Relationship,
423            &mut self.dicts,
424            key,
425            edit,
426        )
427    }
428
429    /// Resize the node-keyed Vecs so `id as usize` is in range. Adjacency
430    /// lists are kept in lockstep with `nodes`, so a freshly-grown slot has
431    /// empty outgoing/incoming Vecs ready to receive edges.
432    fn slot_len_for_id(id: u64, kind: &str) -> Result<usize, String> {
433        if id > MAX_ENTITY_ID {
434            return Err(format!(
435                "{kind} id {id} is above the largest supported id {MAX_ENTITY_ID}"
436            ));
437        }
438        let idx = usize::try_from(id)
439            .map_err(|_| format!("{kind} id {id} does not fit in usize on this platform"))?;
440        idx.checked_add(1)
441            .ok_or_else(|| format!("{kind} id {id} leaves no valid slab slot"))
442    }
443
444    #[inline]
445    fn slot_index(id: u64) -> Option<usize> {
446        usize::try_from(id).ok()
447    }
448
449    fn ensure_node_slot_checked(&mut self, id: NodeId) -> Result<usize, String> {
450        let target = Self::slot_len_for_id(id, "node")?;
451        if self.nodes.len() < target {
452            let additional = target - self.nodes.len();
453            self.nodes.try_reserve(additional).map_err(|e| {
454                format!("node id {id} requires {target} slots, but allocation failed: {e}")
455            })?;
456            self.adjacency.try_reserve_exact(additional).map_err(|e| {
457                format!(
458                    "node id {id} requires {target} adjacency slots, but allocation failed: {e}"
459                )
460            })?;
461            self.nodes.grow_to(target);
462            self.adjacency.grow_shared(target, adjacency::empty_leaf());
463        }
464        Ok(target - 1)
465    }
466
467    fn ensure_rel_slot_checked(&mut self, id: RelationshipId) -> Result<usize, String> {
468        let target = Self::slot_len_for_id(id, "relationship")?;
469        if self.relationships.len() < target {
470            self.relationships
471                .try_reserve(target - self.relationships.len())
472                .map_err(|e| {
473                    format!(
474                        "relationship id {id} requires {target} slots, but allocation failed: {e}"
475                    )
476                })?;
477            self.relationships.grow_to(target);
478        }
479        Ok(target - 1)
480    }
481
482    pub(super) fn put_node_checked(&mut self, id: NodeId, node: &NodeRecord) -> Result<(), String> {
483        let idx = self.ensure_node_slot_checked(id)?;
484        self.put_node_at_slot(idx, node);
485        Ok(())
486    }
487
488    pub(super) fn put_rel_checked(
489        &mut self,
490        id: RelationshipId,
491        rel: &RelationshipRecord,
492    ) -> Result<(), String> {
493        let idx = self.ensure_rel_slot_checked(id)?;
494        self.put_rel_at_slot(idx, rel);
495        Ok(())
496    }
497
498    /// Store already encoded node records (labels and keys numbered by
499    /// this graph's dictionaries), built in leaves. Fails on a taken id.
500    pub(super) fn put_encoded_node_leaves(&mut self, built: BuiltLeaves) -> Result<(), String> {
501        let Some(last) = built.last_slot() else {
502            return Ok(());
503        };
504        self.ensure_node_slot_checked(last as NodeId)?;
505        self.live_node_count += self
506            .nodes
507            .put_leaves(built)
508            .map_err(|slot| format!("duplicate node id {slot} in snapshot payload"))?;
509        Ok(())
510    }
511
512    /// Store already encoded relationship records, built in leaves; `links`
513    /// holds each one's `(id, source, target, type number)`, whose
514    /// endpoints must exist. Fails on a taken id or a missing endpoint.
515    /// Linking them into the adjacency is [`Self::link_relationships`].
516    pub(super) fn put_encoded_relationship_leaves(
517        &mut self,
518        built: BuiltLeaves,
519        links: &[(RelationshipId, NodeId, NodeId, u32)],
520    ) -> Result<(), String> {
521        let Some(last) = built.last_slot() else {
522            return Ok(());
523        };
524        for &(id, src, dst, _) in links {
525            if !self.has_node_at(src) {
526                return Err(format!(
527                    "relationship {id} references missing source node {src}"
528                ));
529            }
530            if !self.has_node_at(dst) {
531                return Err(format!(
532                    "relationship {id} references missing target node {dst}"
533                ));
534            }
535        }
536        self.ensure_rel_slot_checked(last as RelationshipId)?;
537        self.live_rel_count += self
538            .relationships
539            .put_leaves(built)
540            .map_err(|slot| format!("duplicate relationship id {slot} in snapshot payload"))?;
541        Ok(())
542    }
543
544    /// Link each `(id, source, target, type number)` of `links` (in id
545    /// order, endpoints present) into its endpoints' adjacency, as one
546    /// [`Self::adjacency_push`] per end would. On every core when the build
547    /// allows: the ends are sorted by adjacency leaf (stably, so each
548    /// node's list gets them in `links` order) and each leaf is filled on
549    /// its own thread.
550    pub(super) fn link_relationships(&mut self, links: &[(RelationshipId, NodeId, NodeId, u32)]) {
551        #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
552        {
553            use rayon::prelude::*;
554            // Unmapped after the round on macOS (see `crate::scratch`).
555            let mut ends: crate::scratch::ScratchVec<(NodeId, Side, AdjEntry)> =
556                crate::scratch::ScratchVec::with_capacity(2 * links.len());
557            for &(id, src, dst, type_id) in links {
558                let out = AdjEntry {
559                    type_id,
560                    neighbour: dst,
561                    rel: id,
562                };
563                ends.push((src, Side::Out, out));
564                let into = AdjEntry {
565                    type_id,
566                    neighbour: src,
567                    rel: id,
568                };
569                ends.push((dst, Side::In, into));
570            }
571            const LEAF_SHIFT: u32 = super::chunked_vec::CHUNK.trailing_zeros();
572            const SLOT_MASK: usize = super::chunked_vec::CHUNK - 1;
573            let leaf_of = |node: NodeId| (node as usize) >> LEAF_SHIFT;
574            // `links` ascend by id and each pushed its source end first, so
575            // ordering by (leaf, id, side) is the stable order by leaf, and
576            // an unstable sort needs no buffer of the size of `ends`.
577            let ends = ends.as_mut_slice();
578            ends.par_sort_unstable_by_key(|(node, side, entry)| {
579                (leaf_of(*node), entry.rel, *side == Side::In)
580            });
581            let groups: Vec<&[(NodeId, Side, AdjEntry)]> =
582                ends.chunk_by(|a, b| leaf_of(a.0) == leaf_of(b.0)).collect();
583            let touched: Vec<usize> = groups.iter().map(|group| leaf_of(group[0].0)).collect();
584            self.adjacency
585                .leaves_mut(&touched)
586                .into_par_iter()
587                .zip(groups)
588                .for_each(|((_, leaf), group)| {
589                    for &(node, side, entry) in group {
590                        leaf[node as usize & SLOT_MASK].push(side, entry);
591                    }
592                });
593        }
594        #[cfg(not(all(feature = "parallel", not(target_arch = "wasm32"))))]
595        for &(id, src, dst, type_id) in links {
596            let out = AdjEntry {
597                type_id,
598                neighbour: dst,
599                rel: id,
600            };
601            self.adjacency_push(src, Side::Out, out);
602            let into = AdjEntry {
603                type_id,
604                neighbour: src,
605                rel: id,
606            };
607            self.adjacency_push(dst, Side::In, into);
608        }
609    }
610
611    /// List `ids` under the label numbered `label_id`, as
612    /// [`Self::insert_node_label_index`] would one by one.
613    pub(super) fn extend_node_label_index(&mut self, label_id: u32, ids: &[NodeId]) {
614        let label = self.dicts.labels.name(label_id).as_arc();
615        let bucket = match self.nodes_by_label.get_mut(&**label) {
616            Some(bucket) => bucket,
617            None => self.nodes_by_label.entry(Arc::clone(label)).or_default(),
618        };
619        for &id in ids {
620            bucket.insert(id);
621        }
622    }
623
624    /// List `ids` under the relationship type numbered `type_id`.
625    pub(super) fn extend_relationship_type_index(&mut self, type_id: u32, ids: &[RelationshipId]) {
626        let rel_type = self.dicts.types.name(type_id).as_arc();
627        let bucket = match self.relationships_by_type.get_mut(&**rel_type) {
628            Some(bucket) => bucket,
629            None => self
630                .relationships_by_type
631                .entry(Arc::clone(rel_type))
632                .or_default(),
633        };
634        for &id in ids {
635            bucket.insert(id);
636        }
637    }
638
639    pub(super) fn put_node_at_slot(&mut self, idx: usize, node: &NodeRecord) {
640        if self.store_node(idx, node) {
641            self.live_node_count += 1;
642        }
643    }
644
645    pub(super) fn put_rel_at_slot(&mut self, idx: usize, rel: &RelationshipRecord) {
646        let mut out = Scratch::take();
647        encoded::encode_rel(rel, &mut self.dicts, &mut out);
648        if self.relationships.set(idx, &out) {
649            self.live_rel_count += 1;
650        }
651    }
652
653    /// Encode `node` into slot `idx`; returns whether the slot was empty.
654    #[inline]
655    fn store_node(&mut self, idx: usize, node: &NodeRecord) -> bool {
656        let mut out = Scratch::take();
657        encoded::encode_node(node, &mut self.dicts, &mut out);
658        self.nodes.set(idx, &out)
659    }
660
661    pub(super) fn take_node(&mut self, id: NodeId) -> Option<NodeRecord> {
662        let idx = Self::slot_index(id)?;
663        let removed = StoredNode::new(id, self.nodes.get(idx)?, &self.dicts).to_record();
664        self.nodes.remove(idx);
665        self.live_node_count -= 1;
666        // Also clear the per-id adjacency entries so the memory is reclaimed
667        // on the typical "delete every node" pattern. We deliberately do not
668        // shrink the slots — leaving the slot lets new ids reuse the same
669        // index without growth churn (and `next_node_id` is monotonic
670        // anyway, so no immediate reuse).
671        self.clear_adjacency(id);
672        Some(removed)
673    }
674
675    pub(super) fn take_rel(&mut self, id: RelationshipId) -> Option<RelationshipRecord> {
676        let idx = Self::slot_index(id)?;
677        let removed = StoredRel::new(id, self.relationships.get(idx)?, &self.dicts).to_record();
678        self.relationships.remove(idx);
679        self.live_rel_count -= 1;
680        Some(removed)
681    }
682
683    #[inline]
684    pub(super) fn outgoing_at(&self, id: NodeId) -> Option<AdjSide<'_>> {
685        Some(self.adjacency.get(Self::slot_index(id)?)?.outgoing())
686    }
687
688    #[inline]
689    pub(super) fn incoming_at(&self, id: NodeId) -> Option<AdjSide<'_>> {
690        Some(self.adjacency.get(Self::slot_index(id)?)?.incoming())
691    }
692
693    /// Visit the entries of one adjacency list that pass `filter`.
694    /// `skip_self_loops` is set for the incoming half of an undirected
695    /// walk, whose outgoing half already reported the node's self-loops.
696    #[inline]
697    fn try_for_each_adjacent_entry<F, E>(
698        node_id: NodeId,
699        filter: &TypeFilter,
700        adj: AdjSide<'_>,
701        skip_self_loops: bool,
702        visit: &mut F,
703    ) -> Result<(), E>
704    where
705        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
706    {
707        for entry in adj.iter() {
708            if skip_self_loops && entry.neighbour == node_id {
709                continue;
710            }
711            if !filter.matches(entry.type_id) {
712                continue;
713            }
714            visit(entry.rel, entry.neighbour)?;
715        }
716        Ok(())
717    }
718
719    #[inline]
720    pub(super) fn try_for_each_adjacent_id_unchecked<F, E>(
721        &self,
722        node_id: NodeId,
723        direction: Direction,
724        types: &[String],
725        mut visit: F,
726    ) -> Result<(), E>
727    where
728        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
729    {
730        let filter = TypeFilter::resolve(&self.dicts.types, types);
731        if matches!(filter, TypeFilter::Nothing) {
732            return Ok(());
733        }
734        if !matches!(direction, Direction::Left) {
735            if let Some(adj) = self.outgoing_at(node_id) {
736                Self::try_for_each_adjacent_entry(node_id, &filter, adj, false, &mut visit)?;
737            }
738        }
739        if !matches!(direction, Direction::Right) {
740            if let Some(adj) = self.incoming_at(node_id) {
741                let skip_self_loops = matches!(direction, Direction::Undirected);
742                Self::try_for_each_adjacent_entry(
743                    node_id,
744                    &filter,
745                    adj,
746                    skip_self_loops,
747                    &mut visit,
748                )?;
749            }
750        }
751
752        Ok(())
753    }
754
755    #[inline]
756    pub(super) fn try_for_each_adjacent_id<F, E>(
757        &self,
758        node_id: NodeId,
759        direction: Direction,
760        types: &[String],
761        visit: F,
762    ) -> Result<(), E>
763    where
764        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
765    {
766        if !self.has_node_at(node_id) {
767            return Ok(());
768        }
769        self.try_for_each_adjacent_id_unchecked(node_id, direction, types, visit)
770    }
771
772    pub(super) fn iter_node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
773        self.nodes.slots_from(0).map(|i| i as NodeId)
774    }
775
776    pub(super) fn iter_node_refs(&self) -> impl Iterator<Item = NodeRef<'_>> + '_ {
777        self.iter_nodes().map(|(_, node)| node)
778    }
779
780    pub(super) fn iter_rel_ids(&self) -> impl Iterator<Item = RelationshipId> + '_ {
781        self.relationships
782            .slots_from(0)
783            .map(|i| i as RelationshipId)
784    }
785
786    pub(super) fn iter_rel_refs(&self) -> impl Iterator<Item = RelRef<'_>> + '_ {
787        self.iter_rels().map(|(_, rel)| rel)
788    }
789
790    pub(super) fn iter_nodes(&self) -> impl Iterator<Item = (NodeId, NodeRef<'_>)> + '_ {
791        let dicts = &self.dicts;
792        self.nodes.iter().map(move |(i, bytes)| {
793            let id = i as NodeId;
794            (id, NodeRef::stored(StoredNode::new(id, bytes, dicts)))
795        })
796    }
797
798    pub(super) fn iter_rels(&self) -> impl Iterator<Item = (RelationshipId, RelRef<'_>)> + '_ {
799        let dicts = &self.dicts;
800        self.relationships.iter().map(move |(i, bytes)| {
801            let id = i as RelationshipId;
802            (id, RelRef::stored(StoredRel::new(id, bytes, dicts)))
803        })
804    }
805
806    /// Add an entry to `node_id`'s list on `side`. Relies on
807    /// the monotonic-id invariant: relationship ids are allocated once and
808    /// never re-used, so the list can never see a duplicate.
809    fn adjacency_push(&mut self, node_id: NodeId, side: Side, entry: AdjEntry) {
810        if let Ok(idx) = self.ensure_node_slot_checked(node_id) {
811            self.adjacency[idx].push(side, entry);
812        }
813    }
814
815    /// Drop both of `node_id`'s lists at once. A detach delete does this
816    /// first, so removing each relationship touches only the far end's list
817    /// instead of scanning and compacting the node's own list once per
818    /// relationship (quadratic in its degree).
819    pub(super) fn clear_adjacency(&mut self, node_id: NodeId) {
820        let Some(idx) = Self::slot_index(node_id) else {
821            return;
822        };
823        // An empty list needs no clearing, and reaching it mutably would
824        // copy its shared leaf for nothing.
825        if self.adjacency.get(idx).is_some_and(|adj| !adj.is_empty()) {
826            if let Some(adj) = self.adjacency.get_mut(idx) {
827                adj.clear();
828            }
829        }
830    }
831
832    /// Remove `rel_id` from `node_id`'s list on `side`. With `pending`, a
833    /// list longer than [`DEFER_REMOVAL_MIN_BYTES`] is left for
834    /// [`Self::flush_adjacency_removals`], which takes all of a list's
835    /// pending entries in one pass: one at a time, each removal scans and
836    /// compacts the list, which is quadratic when a statement deletes
837    /// many relationships of one hub. A short list is edited at once.
838    fn adjacency_remove(
839        &mut self,
840        node_id: NodeId,
841        side: Side,
842        rel_id: RelationshipId,
843        pending: Option<&mut PendingAdjRemovals>,
844    ) {
845        let Some(idx) = Self::slot_index(node_id) else {
846            return;
847        };
848        let len = self
849            .adjacency
850            .get(idx)
851            .map_or(0, |adj| adj.side_bytes(side));
852        // An empty list (a node detach delete already cleared) needs no
853        // edit, and reaching it mutably would copy its shared leaf.
854        if len == 0 {
855            return;
856        }
857        match pending {
858            Some(pending) if len > DEFER_REMOVAL_MIN_BYTES => {
859                pending.push((node_id, side, rel_id));
860            }
861            _ => {
862                if let Some(adj) = self.adjacency.get_mut(idx) {
863                    adj.remove(side, rel_id);
864                }
865            }
866        }
867    }
868
869    /// Apply the removals [`Self::adjacency_remove`] deferred: each list
870    /// once, keeping the order of its other entries. Leaves `pending` empty.
871    pub(super) fn flush_adjacency_removals(&mut self, pending: &mut PendingAdjRemovals) {
872        pending.sort_unstable_by_key(|&(node, side, rel)| (node, side == Side::In, rel));
873        let mut rels = Vec::new();
874        for group in pending.chunk_by(|a, b| a.0 == b.0 && a.1 == b.1) {
875            let (node_id, side, _) = group[0];
876            let Some(idx) = Self::slot_index(node_id) else {
877                continue;
878            };
879            if self
880                .adjacency
881                .get(idx)
882                .is_none_or(|adj| adj.side_bytes(side) == 0)
883            {
884                continue;
885            }
886            rels.clear();
887            rels.extend(group.iter().map(|&(_, _, rel)| rel));
888            if let Some(adj) = self.adjacency.get_mut(idx) {
889                adj.remove_all(side, &rels);
890            }
891        }
892        pending.clear();
893    }
894
895    pub(super) fn normalize_labels(labels: Vec<String>) -> Labels {
896        let mut out = Labels::new();
897        for label in &labels {
898            let label = label.trim();
899            if !label.is_empty() && !out.has(label) {
900                out.push(label);
901            }
902        }
903        out
904    }
905
906    pub(super) fn insert_node_label_index(&mut self, node_id: NodeId, label: &str) {
907        // Hot path: skip the `String` alloc when the label bucket already
908        // exists. The monotonic-id invariant on the create path guarantees
909        // `node_id` is unique, so we push unconditionally; the previous
910        // `contains` guard turned bulk CREATE into O(n²).
911        if let Some(bucket) = self.nodes_by_label.get_mut(label) {
912            bucket.insert(node_id);
913        } else {
914            self.nodes_by_label
915                .insert(Arc::from(label), std::iter::once(node_id).collect());
916        }
917    }
918
919    fn remove_node_label_index(&mut self, node_id: NodeId, label: &str) {
920        if let Some(ids) = self.nodes_by_label.get_mut(label) {
921            ids.remove(node_id);
922            if ids.is_empty() {
923                self.nodes_by_label.remove(label);
924            }
925        }
926    }
927
928    fn insert_relationship_type_index(&mut self, rel_id: RelationshipId, rel_type: &str) {
929        // See `insert_node_label_index` for the same hot-path rationale.
930        if let Some(bucket) = self.relationships_by_type.get_mut(rel_type) {
931            bucket.insert(rel_id);
932        } else {
933            self.relationships_by_type
934                .insert(Arc::from(rel_type), std::iter::once(rel_id).collect());
935        }
936    }
937
938    fn remove_relationship_type_index(&mut self, rel_id: RelationshipId, rel_type: &str) {
939        if let Some(ids) = self.relationships_by_type.get_mut(rel_type) {
940            ids.remove(rel_id);
941            if ids.is_empty() {
942                self.relationships_by_type.remove(rel_type);
943            }
944        }
945    }
946
947    /// Ids of `label` nodes whose `key` equals `value`, from the scoped
948    /// hash index. `None` when that index is not active for `key` or
949    /// `value` has no index image; the caller must then scan.
950    pub(super) fn indexed_node_ids(
951        &self,
952        label: &str,
953        key: &str,
954        value: &PropertyValue,
955    ) -> Option<Vec<NodeId>> {
956        PropertyIndexKey::from_value(value)?;
957        let indexes = self.indexes_read();
958        if !indexes.node_properties.is_active(key) {
959            return None;
960        }
961        Some(
962            indexes
963                .node_properties
964                .scoped_ids_for(label, key, value, &self.node_values())
965                .map(|ids| ids.to_vec())
966                .unwrap_or_default(),
967        )
968    }
969
970    /// Relationship counterpart of [`Self::indexed_node_ids`].
971    pub(super) fn indexed_rel_ids(
972        &self,
973        rel_type: &str,
974        key: &str,
975        value: &PropertyValue,
976    ) -> Option<Vec<RelationshipId>> {
977        PropertyIndexKey::from_value(value)?;
978        let indexes = self.indexes_read();
979        if !indexes.relationship_properties.is_active(key) {
980            return None;
981        }
982        Some(
983            indexes
984                .relationship_properties
985                .scoped_ids_for(rel_type, key, value, &self.relationship_values())
986                .map(|ids| ids.to_vec())
987                .unwrap_or_default(),
988        )
989    }
990
991    pub(super) fn indexes_read(&self) -> std::sync::RwLockReadGuard<'_, PropertyIndexRegistry> {
992        self.indexes
993            .properties
994            .read()
995            .unwrap_or_else(|poisoned| poisoned.into_inner())
996    }
997
998    pub(super) fn indexes_write(&self) -> RwLockWriteGuard<'_, PropertyIndexRegistry> {
999        self.indexes
1000            .properties
1001            .write()
1002            .unwrap_or_else(|poisoned| poisoned.into_inner())
1003    }
1004
1005    /// Reads node property values from their records, for the hash
1006    /// property index (see [`RecordValues`]).
1007    #[inline]
1008    pub(super) fn node_values(&self) -> RecordValues<'_> {
1009        RecordValues::nodes(&self.nodes, &self.dicts)
1010    }
1011
1012    /// Relationship counterpart of [`Self::node_values`].
1013    #[inline]
1014    pub(super) fn relationship_values(&self) -> RecordValues<'_> {
1015        RecordValues::relationships(&self.relationships, &self.dicts)
1016    }
1017
1018    /// [`Self::indexes_mut`] with [`Self::node_values`], borrowed together.
1019    fn node_index_parts(&mut self) -> (&mut PropertyIndexRegistry, RecordValues<'_>) {
1020        let registry = self
1021            .indexes
1022            .properties
1023            .get_mut()
1024            .unwrap_or_else(|poisoned| poisoned.into_inner());
1025        (registry, RecordValues::nodes(&self.nodes, &self.dicts))
1026    }
1027
1028    /// [`Self::indexes_mut`] with [`Self::relationship_values`].
1029    fn relationship_index_parts(&mut self) -> (&mut PropertyIndexRegistry, RecordValues<'_>) {
1030        let registry = self
1031            .indexes
1032            .properties
1033            .get_mut()
1034            .unwrap_or_else(|poisoned| poisoned.into_inner());
1035        (
1036            registry,
1037            RecordValues::relationships(&self.relationships, &self.dicts),
1038        )
1039    }
1040
1041    pub(super) fn indexes_mut(&mut self) -> &mut PropertyIndexRegistry {
1042        self.indexes
1043            .properties
1044            .get_mut()
1045            .unwrap_or_else(|poisoned| poisoned.into_inner())
1046    }
1047
1048    #[inline]
1049    pub(super) fn active_node_property_index_count(&self) -> usize {
1050        self.indexes
1051            .active_node_property_indexes
1052            .load(Ordering::Relaxed)
1053    }
1054
1055    #[inline]
1056    pub(super) fn active_relationship_property_index_count(&self) -> usize {
1057        self.indexes
1058            .active_relationship_property_indexes
1059            .load(Ordering::Relaxed)
1060    }
1061
1062    #[inline]
1063    pub(super) fn active_constraint_count(&self) -> usize {
1064        self.active_constraints.load(Ordering::Relaxed)
1065    }
1066
1067    #[inline]
1068    pub(super) fn has_active_constraints(&self) -> bool {
1069        self.active_constraint_count() != 0
1070    }
1071
1072    #[inline]
1073    pub(super) fn active_fulltext_index_count(&self) -> usize {
1074        self.indexes.active_fulltext_indexes.load(Ordering::Relaxed)
1075    }
1076
1077    #[inline]
1078    pub(super) fn has_active_fulltext_indexes(&self) -> bool {
1079        self.active_fulltext_index_count() != 0
1080    }
1081
1082    pub(super) fn node_property_index_is_active(&mut self, key: &str) -> bool {
1083        self.active_node_property_index_count() != 0
1084            && self.indexes_mut().node_properties.is_active(key)
1085    }
1086
1087    pub(super) fn relationship_property_index_is_active(&mut self, key: &str) -> bool {
1088        self.active_relationship_property_index_count() != 0
1089            && self.indexes_mut().relationship_properties.is_active(key)
1090    }
1091
1092    /// Give `key` its across-labels map, for lookups that name no label.
1093    /// Call after [`Self::ensure_node_property_index`].
1094    pub(super) fn ensure_any_scope_node_property_index(&self, key: &str) {
1095        if self.indexes_read().node_properties.any_scope_is_active(key) {
1096            return;
1097        }
1098        self.indexes_write()
1099            .node_properties
1100            .activate_any_scope(key, &self.node_values());
1101    }
1102
1103    pub(super) fn ensure_any_scope_relationship_property_index(&self, key: &str) {
1104        if self
1105            .indexes_read()
1106            .relationship_properties
1107            .any_scope_is_active(key)
1108        {
1109            return;
1110        }
1111        self.indexes_write()
1112            .relationship_properties
1113            .activate_any_scope(key, &self.relationship_values());
1114    }
1115
1116    /// For a lookup: activate the node property index on `key` unless
1117    /// another lookup is building it, in which case this one should scan.
1118    /// Returns whether the index is active.
1119    pub(super) fn try_ensure_node_property_index(&self, key: &str) -> bool {
1120        if self.indexes_read().node_properties.is_active(key) {
1121            return true;
1122        }
1123        let Some(_build) = self.indexes.property_builds.claim(true, key) else {
1124            return false;
1125        };
1126        self.ensure_node_property_index(key);
1127        true
1128    }
1129
1130    /// As [`Self::try_ensure_node_property_index`], for relationships.
1131    pub(super) fn try_ensure_relationship_property_index(&self, key: &str) -> bool {
1132        if self.indexes_read().relationship_properties.is_active(key) {
1133            return true;
1134        }
1135        let Some(_build) = self.indexes.property_builds.claim(false, key) else {
1136            return false;
1137        };
1138        self.ensure_relationship_property_index(key);
1139        true
1140    }
1141
1142    /// Built without the property index lock, which is then held only to
1143    /// file the result: other indexes build meanwhile (a restart builds
1144    /// them all at once), and lookups of other keys are not held up.
1145    /// Lookups go through [`Self::try_ensure_node_property_index`], so two
1146    /// of them do not build the same index at once.
1147    pub(super) fn ensure_node_property_index(&self, key: &str) {
1148        if self.indexes_read().node_properties.is_active(key) {
1149            return;
1150        }
1151
1152        let built = if self.live_node_count >= PARALLEL_INDEX_MIN {
1153            self.build_node_property_index_parallel(key)
1154        } else {
1155            let mut built = PropertyIndexState::default();
1156            built.insert_bulk(
1157                key,
1158                || {
1159                    self.iter_nodes().filter_map(|(id, node)| {
1160                        let value = node.properties().get(key)?;
1161                        Some((id, node.labels().strs(), value))
1162                    })
1163                },
1164                &self.node_values(),
1165            );
1166            built
1167        };
1168
1169        let mut indexes = self.indexes_write();
1170        if indexes.node_properties.is_active(key) {
1171            return;
1172        }
1173        indexes.node_properties.absorb(key, built);
1174        if indexes.node_properties.activate(key) {
1175            self.indexes
1176                .active_node_property_indexes
1177                .fetch_add(1, Ordering::Relaxed);
1178        }
1179    }
1180
1181    /// [`PropertyIndexState::build_parallel`] over the nodes.
1182    #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
1183    pub(super) fn build_node_property_index_parallel(&self, key: &str) -> PropertyIndexState {
1184        let Some(key_id) = self.dicts.keys.id_of(key) else {
1185            return PropertyIndexState::default();
1186        };
1187        PropertyIndexState::build_parallel(
1188            key,
1189            self.nodes.len() as u64,
1190            |ids| self.node_slots_in(ids),
1191            |id| {
1192                let node = StoredNode::new(id, self.nodes.get(id as usize)?, &self.dicts);
1193                let value = node.properties().get_id(key_id)?;
1194                Some((node.labels().iter().map(crate::Name::as_str), value))
1195            },
1196            &self.node_values(),
1197        )
1198    }
1199
1200    #[cfg(not(all(feature = "parallel", not(target_arch = "wasm32"))))]
1201    pub(super) fn build_node_property_index_parallel(&self, key: &str) -> PropertyIndexState {
1202        let mut built = PropertyIndexState::default();
1203        built.insert_bulk(
1204            key,
1205            || {
1206                self.iter_nodes().filter_map(|(id, node)| {
1207                    let value = node.properties().get(key)?;
1208                    Some((id, node.labels().strs(), value))
1209                })
1210            },
1211            &self.node_values(),
1212        );
1213        built
1214    }
1215
1216    #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
1217    fn build_relationship_property_index_parallel(&self, key: &str) -> PropertyIndexState {
1218        let Some(key_id) = self.dicts.keys.id_of(key) else {
1219            return PropertyIndexState::default();
1220        };
1221        PropertyIndexState::build_parallel(
1222            key,
1223            self.relationships.len() as u64,
1224            |ids| self.rel_slots_in(ids),
1225            |id| {
1226                let rel = StoredRel::new(id, self.relationships.get(id as usize)?, &self.dicts);
1227                let value = rel.properties().get_id(key_id)?;
1228                Some(([rel.rel_type().as_str()], value))
1229            },
1230            &self.relationship_values(),
1231        )
1232    }
1233
1234    #[cfg(not(all(feature = "parallel", not(target_arch = "wasm32"))))]
1235    fn build_relationship_property_index_parallel(&self, key: &str) -> PropertyIndexState {
1236        let mut built = PropertyIndexState::default();
1237        built.insert_bulk(
1238            key,
1239            || {
1240                self.iter_rels().filter_map(|(id, rel)| {
1241                    let value = rel.properties().get(key)?;
1242                    Some((id, [rel.rel_type()], value))
1243                })
1244            },
1245            &self.relationship_values(),
1246        );
1247        built
1248    }
1249
1250    /// The node slots holding a record, among `ids`.
1251    #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
1252    fn node_slots_in(&self, ids: std::ops::Range<u64>) -> impl Iterator<Item = NodeId> + '_ {
1253        let end = ids.end;
1254        self.nodes
1255            .slots_from(usize::try_from(ids.start).unwrap_or(usize::MAX))
1256            .map(|i| i as NodeId)
1257            .take_while(move |id| *id < end)
1258    }
1259
1260    /// The relationship slots holding a record, among `ids`.
1261    #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
1262    fn rel_slots_in(&self, ids: std::ops::Range<u64>) -> impl Iterator<Item = RelationshipId> + '_ {
1263        let end = ids.end;
1264        self.relationships
1265            .slots_from(usize::try_from(ids.start).unwrap_or(usize::MAX))
1266            .map(|i| i as RelationshipId)
1267            .take_while(move |id| *id < end)
1268    }
1269
1270    /// As [`Self::ensure_node_property_index`].
1271    pub(super) fn ensure_relationship_property_index(&self, key: &str) {
1272        if self.indexes_read().relationship_properties.is_active(key) {
1273            return;
1274        }
1275
1276        let built = if self.live_rel_count >= PARALLEL_INDEX_MIN {
1277            self.build_relationship_property_index_parallel(key)
1278        } else {
1279            let mut built = PropertyIndexState::default();
1280            built.insert_bulk(
1281                key,
1282                || {
1283                    self.iter_rels().filter_map(|(id, rel)| {
1284                        let value = rel.properties().get(key)?;
1285                        Some((id, [rel.rel_type()], value))
1286                    })
1287                },
1288                &self.relationship_values(),
1289            );
1290            built
1291        };
1292
1293        let mut indexes = self.indexes_write();
1294        if indexes.relationship_properties.is_active(key) {
1295            return;
1296        }
1297        indexes.relationship_properties.absorb(key, built);
1298        if indexes.relationship_properties.activate(key) {
1299            self.indexes
1300                .active_relationship_property_indexes
1301                .fetch_add(1, Ordering::Relaxed);
1302        }
1303    }
1304
1305    pub(super) fn index_catalog_read(&self) -> IndexRead<'_, IndexCatalog> {
1306        read_shared(&self.indexes.catalog)
1307    }
1308
1309    /// Mutable access copies the catalog first if a clone still shares it.
1310    pub(super) fn index_catalog_write(&self) -> IndexWrite<'_, IndexCatalog> {
1311        write_shared(&self.indexes.catalog)
1312    }
1313
1314    pub(super) fn constraint_catalog_read(&self) -> IndexRead<'_, ConstraintCatalog> {
1315        read_shared(&self.constraint_catalog)
1316    }
1317
1318    /// Mutable access copies the catalog first if a clone still shares it.
1319    pub(super) fn constraint_catalog_write(&self) -> IndexWrite<'_, ConstraintCatalog> {
1320        write_shared(&self.constraint_catalog)
1321    }
1322
1323    /// Register an explicitly-declared index in the catalog and, when
1324    /// applicable, force the underlying property-index buckets to be
1325    /// populated so equality lookups can use them immediately.
1326    ///
1327    /// Named with a `register_` prefix to avoid colliding with the
1328    /// trait method `GraphStorageMut::create_index` — the trait impl
1329    /// in `impls.rs` delegates here.
1330    #[allow(clippy::result_large_err)]
1331    pub(super) fn register_index(
1332        &self,
1333        request: IndexRequest,
1334        if_not_exists: bool,
1335    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1336        self.register_index_with_recording(request, if_not_exists, true)
1337    }
1338
1339    #[allow(clippy::result_large_err)]
1340    fn register_index_with_recording(
1341        &self,
1342        request: IndexRequest,
1343        if_not_exists: bool,
1344        record_event: bool,
1345    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1346        self.register_index_deferring(request, if_not_exists, record_event, None)
1347    }
1348
1349    /// [`Self::register_index_with_recording`]; with `deferred`, a new
1350    /// index's data is left in it for [`Self::run_deferred`] instead of
1351    /// populated now. Vector indexes populate now: a restore overlays
1352    /// their saved graph on them right after.
1353    #[allow(clippy::result_large_err)]
1354    pub(super) fn register_index_deferring(
1355        &self,
1356        request: IndexRequest,
1357        if_not_exists: bool,
1358        record_event: bool,
1359        deferred: Option<&mut DeferredData>,
1360    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1361        let request_for_event = record_event.then(|| request.clone());
1362        let outcome = {
1363            let mut catalog = self.index_catalog_write();
1364            catalog.try_create(request, if_not_exists)?
1365        };
1366
1367        if let CreateIndexOutcome::Created(def) = &outcome {
1368            match deferred {
1369                Some(deferred) if def.kind != StoredIndexKind::Vector => {
1370                    deferred.populate.push(def.clone());
1371                }
1372                _ => self.populate_index_data(def),
1373            }
1374        }
1375
1376        // Both Created and NoOpExists are committed catalog states; we
1377        // log only Created because NoOpExists implies a redundant DDL
1378        // that adds nothing to durable state.
1379        if matches!(outcome, CreateIndexOutcome::Created(_)) {
1380            if let Some(request_for_event) = request_for_event {
1381                self.emit(|| crate::MutationEvent::CreateIndex {
1382                    request: request_for_event,
1383                    if_not_exists,
1384                });
1385            }
1386        }
1387
1388        Ok(outcome)
1389    }
1390
1391    /// Replay a CreateIndex event against an empty graph. Mirrors the
1392    /// `replay_create_node` shape: callers must invoke before installing
1393    /// a recorder so we don't re-emit during recovery.
1394    #[doc(hidden)]
1395    pub fn replay_create_index(
1396        &mut self,
1397        request: IndexRequest,
1398        if_not_exists: bool,
1399    ) -> Result<(), String> {
1400        if self.recorder.is_some() {
1401            return Err("cannot replay create_index while a mutation recorder is installed".into());
1402        }
1403        self.register_index(request, if_not_exists)
1404            .map(|_| ())
1405            .map_err(|e| e.to_string())
1406    }
1407
1408    /// Replay a DropIndex event.
1409    #[doc(hidden)]
1410    pub fn replay_drop_index(&mut self, name: &str, if_exists: bool) -> Result<(), String> {
1411        if self.recorder.is_some() {
1412            return Err("cannot replay drop_index while a mutation recorder is installed".into());
1413        }
1414        self.drop_named_index(name, if_exists)
1415            .map(|_| ())
1416            .map_err(|e| e.to_string())
1417    }
1418
1419    /// Register a constraint. For uniqueness/key kinds this also
1420    /// registers a backing RANGE index in the index catalog under the
1421    /// same name. Validation of existing data is the caller's
1422    /// responsibility (the enforcement layer runs a pre-create scan
1423    /// before this method commits).
1424    pub(super) fn register_constraint(
1425        &self,
1426        request: ConstraintRequest,
1427        if_not_exists: bool,
1428    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1429        self.register_constraint_deferring(request, if_not_exists, None)
1430    }
1431
1432    /// [`Self::register_constraint`]; with `deferred`, the scan of the
1433    /// existing data and the backing index's data are left in it for
1434    /// [`Self::run_deferred`].
1435    pub(super) fn register_constraint_deferring(
1436        &self,
1437        request: ConstraintRequest,
1438        if_not_exists: bool,
1439        mut deferred: Option<&mut DeferredData>,
1440    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1441        // Constraint-level conflicts (22N65/66/67) take precedence over
1442        // index-catalog conflicts: if the request collides with an
1443        // existing *constraint* shape or name, we never get to the
1444        // backing-index step.
1445        {
1446            let constraint_catalog = self.constraint_catalog_read();
1447            if let Some(existing) = constraint_catalog.find_equivalent(&request) {
1448                let cloned = existing.clone();
1449                drop(constraint_catalog);
1450                if if_not_exists {
1451                    return Ok(CreateConstraintOutcome::NoOpExists(cloned));
1452                }
1453                return Err(CreateConstraintError::EquivalentConstraintExists(
1454                    cloned.name,
1455                ));
1456            }
1457            if let Some(existing) = constraint_catalog.get(&request.name) {
1458                let cloned = existing.clone();
1459                drop(constraint_catalog);
1460                if if_not_exists {
1461                    return Ok(CreateConstraintOutcome::NoOpExists(cloned));
1462                }
1463                return Err(CreateConstraintError::DuplicateName(cloned.name));
1464            }
1465            if let Some(existing) = constraint_catalog.find_same_schema(&request) {
1466                let cloned = existing.clone();
1467                drop(constraint_catalog);
1468                if super::constraint_catalog::kinds_conflict_for_validation(
1469                    &cloned.kind,
1470                    &request.kind,
1471                ) {
1472                    if if_not_exists {
1473                        return Ok(CreateConstraintOutcome::NoOpExists(cloned));
1474                    }
1475                    return Err(CreateConstraintError::ConflictingConstraint(cloned.name));
1476                }
1477            }
1478        }
1479
1480        // Index-catalog conflicts only matter for constraints that need
1481        // a backing range index. The catalog won't yet own one for this
1482        // request — that registration happens below — so any existing
1483        // entry under the same name or schema is from a foreign index.
1484        if request.kind.requires_backing_index() {
1485            let idx_catalog = self.index_catalog_read();
1486            if idx_catalog.get(&request.name).is_some() {
1487                return Err(CreateConstraintError::DuplicateIndexName(
1488                    request.name.clone(),
1489                ));
1490            }
1491            let conflict = idx_catalog.list().into_iter().find(|def| {
1492                def.kind == StoredIndexKind::Range
1493                    && def.entity == request.entity
1494                    && def.label.as_deref() == Some(request.label.as_str())
1495                    && def.properties == request.properties
1496                    && def.name != request.name
1497            });
1498            drop(idx_catalog);
1499            if let Some(def) = conflict {
1500                return Err(CreateConstraintError::BackingIndexConflict(format!(
1501                    "(:{} {{{}}}) already covered by index `{}`",
1502                    request.label,
1503                    request.properties.join(", "),
1504                    def.name,
1505                )));
1506            }
1507        }
1508
1509        let owns_backing = request.kind.requires_backing_index();
1510        let request_for_event = request.clone();
1511        let outcome = {
1512            let mut catalog = self.constraint_catalog_write();
1513            catalog.try_create(request, if_not_exists)?
1514        };
1515
1516        if let CreateConstraintOutcome::Created(def) = &outcome {
1517            // Pre-create data scan: if the live graph already violates
1518            // the constraint, fail and roll back the catalog write.
1519            // Creating constraints when conflicting data exists fails
1520            // before the catalog change is retained (22N77/79/80).
1521            if let Some(deferred) = deferred.as_deref_mut() {
1522                deferred.validate.push(def.clone());
1523            } else if let Err(violation) = self.validate_existing_data_for_constraint(def) {
1524                let mut catalog = self.constraint_catalog_write();
1525                let _ = catalog.try_drop(&def.name, true);
1526                return Err(CreateConstraintError::DataViolation(violation.to_string()));
1527            }
1528        }
1529
1530        if let CreateConstraintOutcome::Created(def) = &outcome {
1531            if owns_backing {
1532                // Register a backing RANGE index under the same name. This
1533                // is an implementation detail of the constraint, so WAL and
1534                // snapshot replay record only the constraint mutation.
1535                // A hash-backed constraint keeps the hash index only.
1536                let mut options = BTreeMap::new();
1537                if def.backing == ConstraintBacking::Hash {
1538                    options.insert(SORTED_OPTION.to_string(), IndexConfigValue::Bool(false));
1539                }
1540                let idx_request = IndexRequest {
1541                    explicit_name: Some(def.name.clone()),
1542                    kind: StoredIndexKind::Range,
1543                    entity: def.entity,
1544                    label: Some(def.label.clone()),
1545                    additional_labels: Vec::new(),
1546                    properties: def.properties.clone(),
1547                    options,
1548                };
1549                // Errors from the backing-index registration unwind the
1550                // constraint registration to keep the two catalogs in
1551                // step.
1552                if let Err(err) = self.register_index_deferring(idx_request, true, false, deferred)
1553                {
1554                    let mut catalog = self.constraint_catalog_write();
1555                    let _ = catalog.try_drop(&def.name, true);
1556                    return Err(CreateConstraintError::BackingIndexConflict(err.to_string()));
1557                }
1558            }
1559            self.emit(|| crate::MutationEvent::CreateConstraint {
1560                request: request_for_event,
1561                if_not_exists,
1562            });
1563            self.active_constraints.fetch_add(1, Ordering::Relaxed);
1564        }
1565
1566        Ok(outcome)
1567    }
1568
1569    /// Do the data work [`Self::register_index_deferring`] and
1570    /// [`Self::register_constraint_deferring`] left in `deferred`: every
1571    /// index population and constraint scan at once, on every core when
1572    /// the build allows. They only read the records, and each index files
1573    /// its result under its own lock. Fails with the first constraint (in
1574    /// registration order) the data violates.
1575    pub(super) fn run_deferred(&self, deferred: DeferredData) -> Result<(), String> {
1576        enum Work {
1577            Populate(IndexDefinition),
1578            /// A RANGE index's two parts, built apart: the hash index of a
1579            /// key, and the sorted index of a label's key.
1580            Hash(StoredIndexEntity, String),
1581            Sorted(StoredIndexEntity, String, String),
1582            Validate(super::ConstraintDefinition),
1583        }
1584        // Scans first: a constraint scan is as long as an index build, and
1585        // the last task to start bounds the wall time.
1586        let mut work: Vec<Work> = deferred.validate.into_iter().map(Work::Validate).collect();
1587        let mut hashed = std::collections::BTreeSet::new();
1588        for def in deferred.populate {
1589            if def.kind != StoredIndexKind::Range {
1590                work.push(Work::Populate(def));
1591                continue;
1592            }
1593            for key in &def.properties {
1594                // Indexes on one key under several labels share its hash index.
1595                if hashed.insert((def.entity == StoredIndexEntity::Node, key.clone())) {
1596                    work.push(Work::Hash(def.entity, key.clone()));
1597                }
1598                if let Some(label) = def.label.as_deref().filter(|_| is_sorted(&def)) {
1599                    work.push(Work::Sorted(def.entity, label.to_owned(), key.clone()));
1600                }
1601            }
1602        }
1603        let run = |work: Work| match work {
1604            Work::Populate(def) => {
1605                self.populate_index_data(&def);
1606                Ok(())
1607            }
1608            Work::Hash(entity, key) => {
1609                match entity {
1610                    StoredIndexEntity::Node => self.ensure_node_property_index(&key),
1611                    StoredIndexEntity::Relationship => {
1612                        self.ensure_relationship_property_index(&key)
1613                    }
1614                }
1615                Ok(())
1616            }
1617            Work::Sorted(entity, label, key) => {
1618                self.activate_sorted_scope(entity, &label, &key);
1619                Ok(())
1620            }
1621            Work::Validate(def) => {
1622                self.validate_existing_data_for_constraint(&def)
1623                    .map_err(|violation| {
1624                        let err = CreateConstraintError::DataViolation(violation.to_string());
1625                        format!("constraint `{}`: {err}", def.name)
1626                    })
1627            }
1628        };
1629        #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
1630        let results: Vec<Result<(), String>> = {
1631            use rayon::prelude::*;
1632            work.into_par_iter().with_max_len(1).map(run).collect()
1633        };
1634        #[cfg(not(all(feature = "parallel", not(target_arch = "wasm32"))))]
1635        let results: Vec<Result<(), String>> = work.into_iter().map(run).collect();
1636        results.into_iter().collect()
1637    }
1638
1639    /// Replay a CreateConstraint event against a recorder-detached graph.
1640    #[doc(hidden)]
1641    pub fn replay_create_constraint(
1642        &mut self,
1643        request: ConstraintRequest,
1644        if_not_exists: bool,
1645    ) -> Result<(), String> {
1646        if self.recorder.is_some() {
1647            return Err(
1648                "cannot replay create_constraint while a mutation recorder is installed".into(),
1649            );
1650        }
1651        self.register_constraint(request, if_not_exists)
1652            .map(|_| ())
1653            .map_err(|e| e.to_string())
1654    }
1655
1656    /// Replay a DropConstraint event.
1657    #[doc(hidden)]
1658    pub fn replay_drop_constraint(&mut self, name: &str, if_exists: bool) -> Result<(), String> {
1659        if self.recorder.is_some() {
1660            return Err(
1661                "cannot replay drop_constraint while a mutation recorder is installed".into(),
1662            );
1663        }
1664        self.drop_named_constraint(name, if_exists)
1665            .map(|_| ())
1666            .map_err(|e| e.to_string())
1667    }
1668
1669    /// Inverse of [`Self::register_constraint`]. Cascades to the backing
1670    /// range index when one is owned.
1671    pub(super) fn drop_named_constraint(
1672        &self,
1673        name: &str,
1674        if_exists: bool,
1675    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1676        let outcome = {
1677            let mut catalog = self.constraint_catalog_write();
1678            catalog.try_drop(name, if_exists)?
1679        };
1680        if let DropConstraintOutcome::Dropped(def) = &outcome {
1681            if let Some(index_name) = def.owned_index.as_deref() {
1682                // The backing index is owned exclusively by the
1683                // constraint, so dropping it is unconditional.
1684                let _ = self.drop_named_index_inner(index_name, true, false);
1685            }
1686            self.active_constraints.fetch_sub(1, Ordering::Relaxed);
1687            self.emit(|| crate::MutationEvent::DropConstraint {
1688                name: name.to_string(),
1689                if_exists,
1690            });
1691        }
1692        Ok(outcome)
1693    }
1694
1695    /// Inverse of [`Self::register_index`]. Removes the catalog entry
1696    /// and (for RANGE) releases the hash property index of each property
1697    /// no other RANGE index still declares; a later equality lookup
1698    /// activates it again.
1699    pub(super) fn drop_named_index(
1700        &self,
1701        name: &str,
1702        if_exists: bool,
1703    ) -> Result<DropIndexOutcome, DropIndexError> {
1704        self.drop_named_index_inner(name, if_exists, true)
1705    }
1706
1707    fn drop_named_index_inner(
1708        &self,
1709        name: &str,
1710        if_exists: bool,
1711        emit_event: bool,
1712    ) -> Result<DropIndexOutcome, DropIndexError> {
1713        if let Some(owner) = self
1714            .constraint_catalog_read()
1715            .constraint_owning_index(name)
1716            .cloned()
1717        {
1718            return Err(DropIndexError::ConstraintOwned {
1719                index: name.to_string(),
1720                constraint: owner.name,
1721            });
1722        }
1723
1724        let outcome = {
1725            let mut catalog = self.index_catalog_write();
1726            catalog.try_drop(name, if_exists)?
1727        };
1728        if let DropIndexOutcome::Dropped(def) = &outcome {
1729            // Release backing structures keyed off the dropped def.
1730            match def.kind {
1731                StoredIndexKind::Text => {
1732                    if let Some(label) = def.label.as_deref() {
1733                        for prop in &def.properties {
1734                            self.deactivate_text_scope(def.entity, label, prop);
1735                        }
1736                    }
1737                }
1738                StoredIndexKind::Range => {
1739                    if let Some(label) = def.label.as_deref().filter(|_| is_sorted(def)) {
1740                        for prop in &def.properties {
1741                            self.deactivate_sorted_scope(def.entity, label, prop);
1742                        }
1743                    }
1744                    // Writes stop paying for a hash index nothing declares
1745                    // any more; an equality lookup rebuilds it on demand.
1746                    let declared = self.declared_property_index_keys();
1747                    let mut indexes = self.indexes_write();
1748                    for prop in &def.properties {
1749                        if declared.contains(&(def.entity, prop.clone())) {
1750                            continue;
1751                        }
1752                        match def.entity {
1753                            StoredIndexEntity::Node => indexes.node_properties.deactivate(prop),
1754                            StoredIndexEntity::Relationship => {
1755                                indexes.relationship_properties.deactivate(prop)
1756                            }
1757                        };
1758                    }
1759                }
1760                StoredIndexKind::Point => {
1761                    if let Some(label) = def.label.as_deref() {
1762                        for prop in &def.properties {
1763                            self.deactivate_point_scope(def.entity, label, prop);
1764                        }
1765                    }
1766                }
1767                StoredIndexKind::Lookup => {
1768                    // Lookup rides on the eagerly-maintained label/type
1769                    // indexes; nothing to release.
1770                }
1771                StoredIndexKind::Vector => {
1772                    self.deactivate_vector_index(def.entity, &def.name);
1773                }
1774                StoredIndexKind::Fulltext => {
1775                    self.deactivate_fulltext_index(def.entity, &def.name);
1776                }
1777            }
1778            if emit_event {
1779                self.emit(|| crate::MutationEvent::DropIndex {
1780                    name: name.to_string(),
1781                    if_exists,
1782                });
1783            }
1784        }
1785        Ok(outcome)
1786    }
1787
1788    /// `(entity, key)` pairs whose hash property index a declaration keeps
1789    /// active: every property of a RANGE index in the catalog, which
1790    /// includes the backing indexes of uniqueness / key constraints. These
1791    /// are the only hash indexes a restart rebuilds; any other active key
1792    /// was activated implicitly by an equality lookup.
1793    pub(super) fn declared_property_index_keys(
1794        &self,
1795    ) -> std::collections::BTreeSet<(StoredIndexEntity, String)> {
1796        self.index_catalog_read()
1797            .list()
1798            .into_iter()
1799            .filter(|def| def.kind == StoredIndexKind::Range)
1800            .flat_map(|def| {
1801                let entity = def.entity;
1802                def.properties.into_iter().map(move |p| (entity, p))
1803            })
1804            .collect()
1805    }
1806
1807    fn populate_index_data(&self, def: &IndexDefinition) {
1808        // RANGE: piggy-back on the existing lazy property-index buckets.
1809        // TEXT: build a trigram inverted index over the existing entity
1810        //       data for the (label, property) tuple.
1811        // POINT: build a grid-bucket spatial index over the existing
1812        //        entity data.
1813        // LOOKUP: catalog-only; existing label/type indexes already
1814        //         answer the predicates.
1815        match def.kind {
1816            StoredIndexKind::Range => {
1817                for key in &def.properties {
1818                    match def.entity {
1819                        StoredIndexEntity::Node => self.ensure_node_property_index(key),
1820                        StoredIndexEntity::Relationship => {
1821                            self.ensure_relationship_property_index(key)
1822                        }
1823                    }
1824                    if let Some(label) = def.label.as_deref().filter(|_| is_sorted(def)) {
1825                        self.activate_sorted_scope(def.entity, label, key);
1826                    }
1827                }
1828            }
1829            StoredIndexKind::Text => {
1830                let label = match def.label.as_deref() {
1831                    Some(l) => l,
1832                    None => return,
1833                };
1834                for property in &def.properties {
1835                    self.activate_text_scope(def.entity, label, property);
1836                }
1837            }
1838            StoredIndexKind::Point => {
1839                let label = match def.label.as_deref() {
1840                    Some(l) => l,
1841                    None => return,
1842                };
1843                let cell_size = PointRegistry::cell_size_from_options(&def.options);
1844                for property in &def.properties {
1845                    self.activate_point_scope(def.entity, label, property, cell_size);
1846                }
1847            }
1848            StoredIndexKind::Fulltext => {
1849                let labels: Vec<String> = def.all_labels().map(String::from).collect();
1850                if labels.is_empty() {
1851                    return;
1852                }
1853                self.activate_fulltext_index(def.entity, &def.name, &labels, &def.properties);
1854            }
1855            StoredIndexKind::Vector => {
1856                let label = match def.label.as_deref() {
1857                    Some(l) => l,
1858                    None => return,
1859                };
1860                let property = match def.properties.first() {
1861                    Some(p) => p.as_str(),
1862                    None => return,
1863                };
1864                let similarity = VectorSimilarity::from_options(&def.options)
1865                    .unwrap_or(VectorSimilarity::Cosine);
1866                let provider = VectorIndexProvider::from_options(&def.options)
1867                    .unwrap_or(VectorIndexProvider::Flat);
1868                let hnsw = HnswParams::from_options(&def.options);
1869                let lazy = matches!(
1870                    def.options.get("vector.populate.async"),
1871                    Some(super::index_catalog::IndexConfigValue::Bool(true))
1872                );
1873                self.activate_vector_index(
1874                    def.entity, &def.name, label, property, similarity, provider, hnsw, lazy,
1875                );
1876                if lazy {
1877                    self.index_catalog_write()
1878                        .set_state(&def.name, StoredIndexState::Populating);
1879                }
1880            }
1881            // LOOKUP rides on the label/type indexes maintained eagerly.
1882            StoredIndexKind::Lookup => {}
1883        }
1884    }
1885
1886    pub(super) fn text_indexes_read(
1887        &self,
1888        entity: StoredIndexEntity,
1889    ) -> IndexRead<'_, TrigramRegistry> {
1890        self.indexes.text.read(entity)
1891    }
1892
1893    pub(super) fn text_indexes_write(
1894        &self,
1895        entity: StoredIndexEntity,
1896    ) -> IndexWrite<'_, TrigramRegistry> {
1897        self.indexes.text.write(entity)
1898    }
1899
1900    pub(super) fn fulltext_indexes_read(
1901        &self,
1902        entity: StoredIndexEntity,
1903    ) -> IndexRead<'_, FulltextRegistry> {
1904        self.indexes.fulltext.read(entity)
1905    }
1906
1907    #[allow(dead_code)]
1908    pub(super) fn fulltext_indexes_write(
1909        &self,
1910        entity: StoredIndexEntity,
1911    ) -> IndexWrite<'_, FulltextRegistry> {
1912        self.indexes.fulltext.write(entity)
1913    }
1914
1915    fn activate_text_scope(&self, entity: StoredIndexEntity, label: &str, property: &str) {
1916        if !self.text_indexes_write(entity).add_scope(label, property) {
1917            return;
1918        }
1919
1920        let backfill: Vec<(u64, String)> = match entity {
1921            StoredIndexEntity::Node => self
1922                .iter_nodes()
1923                .filter(|(_, node)| node.labels().strs().any(|l| l == label))
1924                .filter_map(|(id, node)| match node.properties().get(property) {
1925                    Some(crate::ValueRef::String(value)) => Some((id, value.to_owned())),
1926                    _ => None,
1927                })
1928                .collect(),
1929            StoredIndexEntity::Relationship => self
1930                .iter_rels()
1931                .filter(|(_, rel)| rel.rel_type() == label)
1932                .filter_map(|(id, rel)| match rel.properties().get(property) {
1933                    Some(crate::ValueRef::String(value)) => Some((id, value.to_owned())),
1934                    _ => None,
1935                })
1936                .collect(),
1937        };
1938
1939        let mut registry = self.text_indexes_write(entity);
1940        for (id, value) in backfill {
1941            registry.insert(label, property, id, &value);
1942        }
1943    }
1944
1945    /// Drop a (label, property) text scope, decrementing the refcount.
1946    pub(super) fn deactivate_text_scope(
1947        &self,
1948        entity: StoredIndexEntity,
1949        label: &str,
1950        property: &str,
1951    ) {
1952        self.text_indexes_write(entity)
1953            .remove_scope(label, property);
1954    }
1955
1956    fn activate_fulltext_index(
1957        &self,
1958        entity: StoredIndexEntity,
1959        name: &str,
1960        labels: &[String],
1961        properties: &[String],
1962    ) {
1963        use super::fulltext_index::{term_counts_for_properties, TermCounts};
1964
1965        {
1966            let mut registry = self.fulltext_indexes_write(entity);
1967            registry.register(name.to_string(), labels.to_vec(), properties.to_vec());
1968        }
1969        self.indexes
1970            .active_fulltext_indexes
1971            .fetch_add(1, Ordering::Relaxed);
1972
1973        // Backfill: walk every entity matching any label, tokenise covered
1974        // string properties, install one posting batch per entity.
1975        let backfill: Vec<(u64, TermCounts)> = match entity {
1976            StoredIndexEntity::Node => self
1977                .iter_nodes()
1978                .filter(|(_, node)| {
1979                    labels
1980                        .iter()
1981                        .any(|wanted| node.labels().strs().any(|l| l == wanted))
1982                })
1983                .map(|(id, node)| {
1984                    let counts = term_counts_for_properties(node.properties(), properties);
1985                    (id, counts)
1986                })
1987                .filter(|(_, c)| !c.is_empty())
1988                .collect(),
1989            StoredIndexEntity::Relationship => self
1990                .iter_rels()
1991                .filter(|(_, rel)| labels.iter().any(|wanted| wanted == rel.rel_type()))
1992                .map(|(id, rel)| {
1993                    let counts = term_counts_for_properties(rel.properties(), properties);
1994                    (id, counts)
1995                })
1996                .filter(|(_, c)| !c.is_empty())
1997                .collect(),
1998        };
1999
2000        let mut registry = self.fulltext_indexes_write(entity);
2001        if let Some(index) = registry.get_mut(name) {
2002            for (id, counts) in backfill {
2003                index.reindex_entity(id, counts);
2004            }
2005        }
2006    }
2007
2008    pub(super) fn deactivate_fulltext_index(&self, entity: StoredIndexEntity, name: &str) {
2009        self.fulltext_indexes_write(entity).deregister(name);
2010        self.indexes
2011            .active_fulltext_indexes
2012            .fetch_sub(1, Ordering::Relaxed);
2013    }
2014
2015    pub(super) fn sorted_indexes_read(
2016        &self,
2017        entity: StoredIndexEntity,
2018    ) -> IndexRead<'_, SortedPropertyIndex> {
2019        self.indexes.sorted.read(entity)
2020    }
2021
2022    pub(super) fn sorted_indexes_write(
2023        &self,
2024        entity: StoredIndexEntity,
2025    ) -> IndexWrite<'_, SortedPropertyIndex> {
2026        self.indexes.sorted.write(entity)
2027    }
2028
2029    pub(super) fn activate_sorted_scope(
2030        &self,
2031        entity: StoredIndexEntity,
2032        label: &str,
2033        property: &str,
2034    ) {
2035        let members = match entity {
2036            StoredIndexEntity::Node => self.nodes_by_label.get(label),
2037            StoredIndexEntity::Relationship => self.relationships_by_type.get(label),
2038        }
2039        .map_or(0, |ids| ids.len());
2040        if cfg!(all(feature = "parallel", not(target_arch = "wasm32")))
2041            && members >= PARALLEL_INDEX_MIN
2042        {
2043            return self.activate_sorted_scope_parallel(entity, label, property);
2044        }
2045        if !self.sorted_indexes_write(entity).add_scope(label, property) {
2046            return;
2047        }
2048
2049        // Inserted while scanning: collecting every value first held a
2050        // second copy of the whole column until the index was built.
2051        let mut registry = self.sorted_indexes_write(entity);
2052        match entity {
2053            StoredIndexEntity::Node => {
2054                for (id, node) in self.iter_nodes() {
2055                    if !node.labels().strs().any(|l| l == label) {
2056                        continue;
2057                    }
2058                    match node.properties().get(property) {
2059                        // A list is indexed by its elements too.
2060                        Some(crate::ValueRef::Other(value)) => {
2061                            registry.insert(label, property, id, &value.get())
2062                        }
2063                        Some(crate::ValueRef::String(s)) => {
2064                            registry.insert_str(label, property, id, s)
2065                        }
2066                        Some(value) => {
2067                            if let Some(key) = PropertyIndexKey::from_value_ref(value) {
2068                                registry.insert_key(label, property, id, key);
2069                            }
2070                        }
2071                        None => {}
2072                    }
2073                }
2074            }
2075            StoredIndexEntity::Relationship => {
2076                for (id, rel) in self.iter_rels() {
2077                    if rel.rel_type() != label {
2078                        continue;
2079                    }
2080                    match rel.properties().get(property) {
2081                        // A list is indexed by its elements too.
2082                        Some(crate::ValueRef::Other(value)) => {
2083                            registry.insert(label, property, id, &value.get())
2084                        }
2085                        Some(crate::ValueRef::String(s)) => {
2086                            registry.insert_str(label, property, id, s)
2087                        }
2088                        Some(value) => {
2089                            if let Some(key) = PropertyIndexKey::from_value_ref(value) {
2090                                registry.insert_key(label, property, id, key);
2091                            }
2092                        }
2093                        None => {}
2094                    }
2095                }
2096            }
2097        }
2098    }
2099
2100    /// [`Self::activate_sorted_scope`] for a large label: the entries are
2101    /// gathered over id ranges and sorted on every core, then filed in key
2102    /// order without the index lock, which is held only to install them.
2103    fn activate_sorted_scope_parallel(
2104        &self,
2105        entity: StoredIndexEntity,
2106        label: &str,
2107        property: &str,
2108    ) {
2109        use super::sorted_property_index::{build_scope, element_keys};
2110        use crate::scratch::ScratchVec;
2111        if self.sorted_indexes_read(entity).has_scope(label, property) {
2112            self.sorted_indexes_write(entity).add_scope(label, property);
2113            return;
2114        }
2115        let (ids, slots) = match entity {
2116            StoredIndexEntity::Node => (self.nodes_by_label.get(label), self.nodes.len()),
2117            StoredIndexEntity::Relationship => (
2118                self.relationships_by_type.get(label),
2119                self.relationships.len(),
2120            ),
2121        };
2122        let Some(ids) = ids else {
2123            self.sorted_indexes_write(entity).add_scope(label, property);
2124            return;
2125        };
2126        let Some(key_id) = self.dicts.keys.id_of(property) else {
2127            self.sorted_indexes_write(entity).add_scope(label, property);
2128            return;
2129        };
2130        const RANGE: u64 = 1 << 16;
2131        // Sized up front and dropped once the scope is built: on macOS
2132        // these pages are unmapped then, where malloc kept freed blocks
2133        // of this size resident after the open (G-64).
2134        let gather = |start: u64| {
2135            let in_range = ids
2136                .iter_from(start)
2137                .take_while(|&id| id < start + RANGE)
2138                .count();
2139            let mut values = Vec::new();
2140            let mut strings = ScratchVec::with_capacity(in_range);
2141            let mut elements = Vec::new();
2142            for id in ids.iter_from(start).take_while(|&id| id < start + RANGE) {
2143                let value = match entity {
2144                    StoredIndexEntity::Node => self.nodes.get(id as usize).and_then(|bytes| {
2145                        StoredNode::new(id, bytes, &self.dicts)
2146                            .properties()
2147                            .get_id(key_id)
2148                    }),
2149                    StoredIndexEntity::Relationship => {
2150                        self.relationships.get(id as usize).and_then(|bytes| {
2151                            StoredRel::new(id, bytes, &self.dicts)
2152                                .properties()
2153                                .get_id(key_id)
2154                        })
2155                    }
2156                };
2157                match value {
2158                    // A list is indexed by its elements too.
2159                    Some(crate::ValueRef::Other(value)) => {
2160                        let value = value.get();
2161                        if let Some(key) = PropertyIndexKey::from_value(&value) {
2162                            values.push((key, id));
2163                        }
2164                        elements.extend(element_keys(&value).into_iter().map(|key| (key, id)));
2165                    }
2166                    Some(crate::ValueRef::String(s)) => strings.push((s, id)),
2167                    Some(value) => {
2168                        if let Some(key) = PropertyIndexKey::from_value_ref(value) {
2169                            values.push((key, id));
2170                        }
2171                    }
2172                    None => {}
2173                }
2174            }
2175            (values, strings, elements)
2176        };
2177        let starts: Vec<u64> = (0..(slots as u64).div_ceil(RANGE))
2178            .map(|i| i * RANGE)
2179            .collect();
2180        #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
2181        let gathered: Vec<_> = {
2182            use rayon::prelude::*;
2183            starts.into_par_iter().map(gather).collect()
2184        };
2185        #[cfg(not(all(feature = "parallel", not(target_arch = "wasm32"))))]
2186        let gathered: Vec<_> = starts.into_iter().map(gather).collect();
2187        let mut values = Vec::with_capacity(gathered.iter().map(|(v, _, _)| v.len()).sum());
2188        let mut strings = ScratchVec::with_capacity(gathered.iter().map(|(_, s, _)| s.len()).sum());
2189        let mut elements = Vec::new();
2190        for (v, s, e) in gathered {
2191            values.extend(v);
2192            strings.extend_from(&s);
2193            elements.extend(e);
2194        }
2195        let built = build_scope(values, strings, elements);
2196        self.sorted_indexes_write(entity)
2197            .add_built_scope(label, property, built);
2198    }
2199
2200    pub(super) fn deactivate_sorted_scope(
2201        &self,
2202        entity: StoredIndexEntity,
2203        label: &str,
2204        property: &str,
2205    ) {
2206        self.sorted_indexes_write(entity)
2207            .remove_scope(label, property);
2208    }
2209
2210    pub(super) fn point_indexes_read(
2211        &self,
2212        entity: StoredIndexEntity,
2213    ) -> IndexRead<'_, PointRegistry> {
2214        self.indexes.point.read(entity)
2215    }
2216
2217    pub(super) fn point_indexes_write(
2218        &self,
2219        entity: StoredIndexEntity,
2220    ) -> IndexWrite<'_, PointRegistry> {
2221        self.indexes.point.write(entity)
2222    }
2223
2224    fn activate_point_scope(
2225        &self,
2226        entity: StoredIndexEntity,
2227        label: &str,
2228        property: &str,
2229        cell_size: Option<f64>,
2230    ) {
2231        if !self
2232            .point_indexes_write(entity)
2233            .add_scope(label, property, cell_size)
2234        {
2235            return;
2236        }
2237
2238        let backfill: Vec<(u64, LoraPoint)> = match entity {
2239            StoredIndexEntity::Node => self
2240                .iter_nodes()
2241                .filter(|(_, node)| node.labels().strs().any(|l| l == label))
2242                .filter_map(|(id, node)| {
2243                    match node.properties().get(property).map(|v| v.to_owned()) {
2244                        Some(PropertyValue::Point(point)) => Some((id, point)),
2245                        _ => None,
2246                    }
2247                })
2248                .collect(),
2249            StoredIndexEntity::Relationship => self
2250                .iter_rels()
2251                .filter(|(_, rel)| rel.rel_type() == label)
2252                .filter_map(|(id, rel)| {
2253                    match rel.properties().get(property).map(|v| v.to_owned()) {
2254                        Some(PropertyValue::Point(point)) => Some((id, point)),
2255                        _ => None,
2256                    }
2257                })
2258                .collect(),
2259        };
2260
2261        let mut registry = self.point_indexes_write(entity);
2262        for (id, point) in backfill {
2263            registry.insert(label, property, id, point);
2264        }
2265    }
2266
2267    pub(super) fn deactivate_point_scope(
2268        &self,
2269        entity: StoredIndexEntity,
2270        label: &str,
2271        property: &str,
2272    ) {
2273        self.point_indexes_write(entity)
2274            .remove_scope(label, property);
2275    }
2276
2277    pub(super) fn vector_indexes_read(
2278        &self,
2279        entity: StoredIndexEntity,
2280    ) -> IndexRead<'_, VectorIndexRegistry> {
2281        self.indexes.vector.read(entity)
2282    }
2283
2284    pub(super) fn vector_indexes_write(
2285        &self,
2286        entity: StoredIndexEntity,
2287    ) -> IndexWrite<'_, VectorIndexRegistry> {
2288        self.indexes.vector.write(entity)
2289    }
2290
2291    #[allow(clippy::too_many_arguments)]
2292    fn activate_vector_index(
2293        &self,
2294        entity: StoredIndexEntity,
2295        name: &str,
2296        label: &str,
2297        property: &str,
2298        similarity: VectorSimilarity,
2299        provider: VectorIndexProvider,
2300        hnsw: HnswParams,
2301        lazy: bool,
2302    ) {
2303        {
2304            let mut registry = self.vector_indexes_write(entity);
2305            registry.register(
2306                name.to_string(),
2307                label.to_string(),
2308                property.to_string(),
2309                similarity,
2310                provider,
2311                hnsw,
2312            );
2313        }
2314
2315        if lazy {
2316            // Skip the initial backfill — the catalog state is flipped
2317            // to Populating by the caller and the first query routed
2318            // to this index triggers `lazy_populate_vector_index`.
2319            // Mutations between CREATE and first query still feed the
2320            // registry via the maintenance hook, so the lazy phase
2321            // only handles vectors that existed before CREATE.
2322            return;
2323        }
2324
2325        self.backfill_vector_index(entity, label, property);
2326    }
2327
2328    /// Walk the property store for vectors matching this index's
2329    /// `(label, property)` scope and replay them into the registry.
2330    /// Shared by sync CREATE and lazy-populate flows.
2331    fn backfill_vector_index(&self, entity: StoredIndexEntity, label: &str, property: &str) {
2332        let backfill: Vec<(u64, crate::LoraVector)> = match entity {
2333            StoredIndexEntity::Node => self
2334                .iter_nodes()
2335                .filter(|(_, node)| node.labels().strs().any(|l| l == label))
2336                .filter_map(|(id, node)| {
2337                    match node.properties().get(property).map(|v| v.to_owned()) {
2338                        Some(PropertyValue::Vector(v)) => Some((id, v)),
2339                        _ => None,
2340                    }
2341                })
2342                .collect(),
2343            StoredIndexEntity::Relationship => self
2344                .iter_rels()
2345                .filter(|(_, rel)| rel.rel_type() == label)
2346                .filter_map(|(id, rel)| {
2347                    match rel.properties().get(property).map(|v| v.to_owned()) {
2348                        Some(PropertyValue::Vector(v)) => Some((id, v)),
2349                        _ => None,
2350                    }
2351                })
2352                .collect(),
2353        };
2354
2355        let mut registry = self.vector_indexes_write(entity);
2356        for (id, vector) in backfill {
2357            registry.insert_for(label, property, id, &vector);
2358        }
2359    }
2360
2361    /// Lazy-populate a `Populating` vector index: backfill from the
2362    /// property store, then flip catalog state to `Online`. Called
2363    /// from `GraphStorage::vector_search` on the first request that
2364    /// hits a still-populating index. Idempotent: a second concurrent
2365    /// caller finds the state already `Online` and does no work.
2366    pub(super) fn lazy_populate_vector_index(&self, name: &str) {
2367        let def = match self.index_catalog_read().get(name).cloned() {
2368            Some(d) => d,
2369            None => return,
2370        };
2371        if def.state != StoredIndexState::Populating || def.kind != StoredIndexKind::Vector {
2372            return;
2373        }
2374        let label = match def.label.as_deref() {
2375            Some(l) => l,
2376            None => return,
2377        };
2378        let property = match def.properties.first() {
2379            Some(p) => p.as_str(),
2380            None => return,
2381        };
2382        self.backfill_vector_index(def.entity, label, property);
2383        self.index_catalog_write()
2384            .set_state(name, StoredIndexState::Online);
2385    }
2386
2387    pub(super) fn deactivate_vector_index(&self, entity: StoredIndexEntity, name: &str) {
2388        self.vector_indexes_write(entity).deregister(name);
2389    }
2390
2391    /// Snapshot of cardinality stats. Cheap: derived from already-tracked
2392    /// `nodes_by_label` / `relationships_by_type` lengths and the
2393    /// distinct-value sketches (a cached estimate per (scope, key), and
2394    /// the previous map reused while those don't change). The cost model
2395    /// uses this to populate `estimated_rows` on plan-tree nodes.
2396    pub fn graph_stats(&self) -> GraphStats {
2397        let mut stats = GraphStats {
2398            node_count: self.live_node_count,
2399            relationship_count: self.live_rel_count,
2400            ..Default::default()
2401        };
2402        // Collected from ascending keys, the maps are built in one pass
2403        // instead of searched and grown per insert.
2404        stats.nodes_by_label = self
2405            .nodes_by_label
2406            .iter()
2407            .map(|(label, ids)| (label.to_string(), ids.len()))
2408            .collect();
2409        stats.relationships_by_type = self
2410            .relationships_by_type
2411            .iter()
2412            .map(|(rel_type, ids)| (rel_type.to_string(), ids.len()))
2413            .collect();
2414        // Distinct values per (label / type, property): estimated from
2415        // the distinct-value sketches, which exist for every key and are
2416        // a function of the live data alone. Deliberately *not* the
2417        // exact bucket counts of active hash indexes: which keys are
2418        // active depends on lookup history and on whether the process
2419        // restarted, and plans must not.
2420        stats.node_distinct_values = self.distinct_stats.nodes.distinct_values();
2421        stats.relationship_distinct_values = self.distinct_stats.relationships.distinct_values();
2422
2423        for def in self.index_catalog_read().list() {
2424            if def.state != StoredIndexState::Online {
2425                continue;
2426            }
2427            // A hash-backed constraint's index answers equality only, which
2428            // the planner assumes of every key.
2429            if def.kind == StoredIndexKind::Range && !is_sorted(&def) {
2430                continue;
2431            }
2432            let Some(label) = def.label else {
2433                continue;
2434            };
2435            for property in def.properties {
2436                let scope = (label.clone(), property);
2437                match (def.entity, def.kind) {
2438                    (StoredIndexEntity::Node, StoredIndexKind::Range) => {
2439                        stats.node_range_indexes.insert(scope);
2440                    }
2441                    (StoredIndexEntity::Node, StoredIndexKind::Text) => {
2442                        stats.node_text_indexes.insert(scope);
2443                    }
2444                    (StoredIndexEntity::Node, StoredIndexKind::Point) => {
2445                        stats.node_point_indexes.insert(scope);
2446                    }
2447                    (StoredIndexEntity::Relationship, StoredIndexKind::Range) => {
2448                        stats.relationship_range_indexes.insert(scope);
2449                    }
2450                    (StoredIndexEntity::Relationship, StoredIndexKind::Text) => {
2451                        stats.relationship_text_indexes.insert(scope);
2452                    }
2453                    (StoredIndexEntity::Relationship, StoredIndexKind::Point) => {
2454                        stats.relationship_point_indexes.insert(scope);
2455                    }
2456                    (StoredIndexEntity::Node, StoredIndexKind::Vector) => {
2457                        stats.node_vector_indexes.insert(scope);
2458                    }
2459                    (StoredIndexEntity::Relationship, StoredIndexKind::Vector) => {
2460                        stats.relationship_vector_indexes.insert(scope);
2461                    }
2462                    (_, StoredIndexKind::Lookup | StoredIndexKind::Fulltext) => {}
2463                }
2464            }
2465        }
2466        for (entity, elements) in [
2467            (StoredIndexEntity::Node, &mut stats.node_list_elements),
2468            (
2469                StoredIndexEntity::Relationship,
2470                &mut stats.relationship_list_elements,
2471            ),
2472        ] {
2473            let sorted = self.sorted_indexes_read(entity);
2474            for (label, property, distinct, postings) in sorted.element_stats() {
2475                elements.insert(
2476                    (label.to_string(), property.to_string()),
2477                    super::ListElementStats { distinct, postings },
2478                );
2479            }
2480        }
2481        stats
2482    }
2483
2484    /// [`GraphStats::plan_signature`] of [`Self::graph_stats`], computed
2485    /// without building the stats: one pass over the label / type maps,
2486    /// the distinct-value estimates and the index catalog. Plan caches
2487    /// call it after every write, so it must stay allocation-free.
2488    pub fn plan_signature(&self) -> u64 {
2489        let mut sig = super::PlanSignature::new();
2490        sig.totals(self.live_node_count, self.live_rel_count);
2491        for (label, ids) in &self.nodes_by_label {
2492            sig.label(label, ids.len());
2493        }
2494        for (rel_type, ids) in &self.relationships_by_type {
2495            sig.relationship_type(rel_type, ids.len());
2496        }
2497        for (label, property, distinct) in self.distinct_stats.nodes.estimates() {
2498            sig.node_distinct(label, property, distinct);
2499        }
2500        for (rel_type, property, distinct) in self.distinct_stats.relationships.estimates() {
2501            sig.relationship_distinct(rel_type, property, distinct);
2502        }
2503        for def in self.index_catalog_read().iter() {
2504            if def.state != StoredIndexState::Online
2505                || matches!(
2506                    def.kind,
2507                    StoredIndexKind::Lookup | StoredIndexKind::Fulltext
2508                )
2509            {
2510                continue;
2511            }
2512            let Some(label) = &def.label else {
2513                continue;
2514            };
2515            for property in &def.properties {
2516                sig.index(def.entity, def.kind, label, property);
2517            }
2518        }
2519        for entity in [StoredIndexEntity::Node, StoredIndexEntity::Relationship] {
2520            let sorted = self.sorted_indexes_read(entity);
2521            for (label, property, distinct, postings) in sorted.element_stats() {
2522                sig.list_elements(
2523                    entity,
2524                    label,
2525                    property,
2526                    super::ListElementStats { distinct, postings },
2527                );
2528            }
2529        }
2530        sig.finish()
2531    }
2532
2533    /// Approximate retained-heap breakdown of this graph. See
2534    /// [`super::MemoryReport`] for the methodology and per-component
2535    /// fields. Intended for benches and the `mem_probe*` examples;
2536    /// not on a hot path.
2537    pub fn memory_estimate(&self) -> super::MemoryReport {
2538        super::mem_report::estimate(self)
2539    }
2540
2541    /// Count (or uncount) every property of an entity under each of its
2542    /// scopes in the distinct-value sketches.
2543    pub(super) fn count_distinct_values<'a>(
2544        stats: &mut super::distinct_stats::DistinctStats,
2545        scopes: impl IntoIterator<Item = &'a str>,
2546        properties: &Properties,
2547        add: bool,
2548    ) {
2549        if properties.is_empty() {
2550            return;
2551        }
2552        for scope in scopes {
2553            if add {
2554                stats.add_all(scope, properties);
2555            } else {
2556                for (key, value) in properties {
2557                    stats.remove(scope, key, value);
2558                }
2559            }
2560        }
2561    }
2562
2563    pub(super) fn on_node_created(&mut self, node: &NodeRecord) {
2564        for label in &node.labels {
2565            self.insert_node_label_index(node.id, label);
2566        }
2567        Self::count_distinct_values(
2568            &mut self.distinct_stats.nodes,
2569            node.labels.strs(),
2570            &node.properties,
2571            true,
2572        );
2573        self.index_node_properties_if_active(node.id, node.labels.strs(), &node.properties);
2574        self.maintain_node_secondary_indexes(node, SecondaryIndexMutation::Insert);
2575    }
2576
2577    pub(super) fn on_node_property_set(
2578        &mut self,
2579        node_id: NodeId,
2580        key: &str,
2581        old: Option<&PropertyValue>,
2582        new: &PropertyValue,
2583    ) {
2584        let Some(labels) = self.node_at(node_id).map(|node| node.labels().to_owned()) else {
2585            return;
2586        };
2587        self.on_labelled_node_property_set(&labels, node_id, key, old, new);
2588    }
2589
2590    /// [`Self::on_node_property_set`] with the node's labels in hand.
2591    pub(super) fn on_labelled_node_property_set(
2592        &mut self,
2593        labels: &crate::Labels,
2594        node_id: NodeId,
2595        key: &str,
2596        old: Option<&PropertyValue>,
2597        new: &PropertyValue,
2598    ) {
2599        for label in labels {
2600            self.distinct_stats.nodes.replace(label, key, old, new);
2601        }
2602
2603        if self.node_property_index_is_active(key) {
2604            if let Some(old) = old {
2605                self.unindex_node_property_if_active(node_id, labels.strs(), key, old);
2606            }
2607            self.index_node_property_if_active(node_id, labels.strs(), key, new);
2608        }
2609
2610        self.update_secondary_property(
2611            StoredIndexEntity::Node,
2612            labels.strs(),
2613            node_id,
2614            key,
2615            old,
2616            Some(new),
2617        );
2618    }
2619
2620    pub(super) fn on_node_property_removed(
2621        &mut self,
2622        node_id: NodeId,
2623        key: &str,
2624        old: &PropertyValue,
2625    ) {
2626        let Some(labels) = self.node_at(node_id).map(|node| node.labels().to_owned()) else {
2627            return;
2628        };
2629        self.on_labelled_node_property_removed(&labels, node_id, key, old);
2630    }
2631
2632    /// [`Self::on_node_property_removed`] with the node's labels in hand.
2633    pub(super) fn on_labelled_node_property_removed(
2634        &mut self,
2635        labels: &crate::Labels,
2636        node_id: NodeId,
2637        key: &str,
2638        old: &PropertyValue,
2639    ) {
2640        for label in labels {
2641            self.distinct_stats.nodes.remove(label, key, old);
2642        }
2643        if self.node_property_index_is_active(key) {
2644            self.unindex_node_property_if_active(node_id, labels.strs(), key, old);
2645        }
2646        self.update_secondary_property(
2647            StoredIndexEntity::Node,
2648            labels.strs(),
2649            node_id,
2650            key,
2651            Some(old),
2652            None,
2653        );
2654    }
2655
2656    pub(super) fn on_node_label_added(&mut self, node_id: NodeId, label: &str) {
2657        self.insert_node_label_index(node_id, label);
2658
2659        let Some(properties) = self
2660            .node_at(node_id)
2661            .map(|node| node.properties().to_owned())
2662        else {
2663            return;
2664        };
2665        Self::count_distinct_values(&mut self.distinct_stats.nodes, [label], &properties, true);
2666        if self.active_node_property_index_count() != 0 {
2667            self.index_node_scope_properties_if_active(node_id, label, &properties);
2668            // Its first label: it was indexed as a node without any.
2669            if self.node_at(node_id).is_some_and(|n| n.labels().len() == 1) {
2670                self.unindex_node_scope_properties_if_active(node_id, UNLABELLED, &properties);
2671            }
2672        }
2673        for (key, value) in &properties {
2674            self.update_secondary_property(
2675                StoredIndexEntity::Node,
2676                [label],
2677                node_id,
2678                key,
2679                None,
2680                Some(value),
2681            );
2682        }
2683    }
2684
2685    pub(super) fn on_node_label_removed(&mut self, node_id: NodeId, label: &str) {
2686        self.remove_node_label_index(node_id, label);
2687
2688        let Some(properties) = self
2689            .node_at(node_id)
2690            .map(|node| node.properties().to_owned())
2691        else {
2692            return;
2693        };
2694        Self::count_distinct_values(&mut self.distinct_stats.nodes, [label], &properties, false);
2695        if self.active_node_property_index_count() != 0 {
2696            self.unindex_node_scope_properties_if_active(node_id, label, &properties);
2697            // Its last label: index it as a node without any.
2698            if self.node_at(node_id).is_some_and(|n| n.labels().is_empty()) {
2699                self.index_node_scope_properties_if_active(node_id, UNLABELLED, &properties);
2700            }
2701        }
2702        for (key, value) in &properties {
2703            self.update_secondary_property(
2704                StoredIndexEntity::Node,
2705                [label],
2706                node_id,
2707                key,
2708                Some(value),
2709                None,
2710            );
2711        }
2712    }
2713
2714    pub(super) fn on_node_deleted(&mut self, node: &NodeRecord) {
2715        for label in &node.labels {
2716            self.remove_node_label_index(node.id, label);
2717        }
2718        Self::count_distinct_values(
2719            &mut self.distinct_stats.nodes,
2720            node.labels.strs(),
2721            &node.properties,
2722            false,
2723        );
2724        self.unindex_active_node_properties(node.id, node.labels.strs(), &node.properties);
2725        self.maintain_node_secondary_indexes(node, SecondaryIndexMutation::Remove);
2726    }
2727
2728    pub(super) fn on_relationship_created(&mut self, rel: &RelationshipRecord) {
2729        self.attach_relationship(rel);
2730        Self::count_distinct_values(
2731            &mut self.distinct_stats.relationships,
2732            [rel.rel_type.as_str()],
2733            &rel.properties,
2734            true,
2735        );
2736        self.index_relationship_properties_if_active(
2737            rel.id,
2738            [rel.rel_type.as_str()],
2739            &rel.properties,
2740        );
2741        self.maintain_relationship_secondary_indexes(rel, SecondaryIndexMutation::Insert);
2742    }
2743
2744    pub(super) fn on_relationship_property_set(
2745        &mut self,
2746        rel_id: RelationshipId,
2747        key: &str,
2748        old: Option<&PropertyValue>,
2749        new: &PropertyValue,
2750    ) {
2751        let Some(rel_type) = self.rel_at(rel_id).map(|rel| rel.type_name().clone()) else {
2752            return;
2753        };
2754        self.distinct_stats
2755            .relationships
2756            .replace(&rel_type, key, old, new);
2757
2758        if self.relationship_property_index_is_active(key) {
2759            if let Some(old) = old {
2760                self.unindex_relationship_property_if_active(rel_id, [rel_type.as_str()], key, old);
2761            }
2762            self.index_relationship_property_if_active(rel_id, [rel_type.as_str()], key, new);
2763        }
2764
2765        self.update_secondary_property(
2766            StoredIndexEntity::Relationship,
2767            [rel_type.as_str()],
2768            rel_id,
2769            key,
2770            old,
2771            Some(new),
2772        );
2773    }
2774
2775    pub(super) fn on_relationship_property_removed(
2776        &mut self,
2777        rel_id: RelationshipId,
2778        key: &str,
2779        old: &PropertyValue,
2780    ) {
2781        let Some(rel_type) = self.rel_at(rel_id).map(|rel| rel.type_name().clone()) else {
2782            return;
2783        };
2784        self.distinct_stats
2785            .relationships
2786            .remove(&rel_type, key, old);
2787        if self.relationship_property_index_is_active(key) {
2788            self.unindex_relationship_property_if_active(rel_id, [rel_type.as_str()], key, old);
2789        }
2790        self.update_secondary_property(
2791            StoredIndexEntity::Relationship,
2792            [rel_type.as_str()],
2793            rel_id,
2794            key,
2795            Some(old),
2796            None,
2797        );
2798    }
2799
2800    /// Index upkeep for a deleted relationship. `pending` defers its
2801    /// removal from long adjacency lists: see [`Self::adjacency_remove`].
2802    pub(super) fn on_relationship_deleted(
2803        &mut self,
2804        rel: &RelationshipRecord,
2805        pending: Option<&mut PendingAdjRemovals>,
2806    ) {
2807        self.detach_relationship_indexes(rel, pending);
2808        Self::count_distinct_values(
2809            &mut self.distinct_stats.relationships,
2810            [rel.rel_type.as_str()],
2811            &rel.properties,
2812            false,
2813        );
2814        self.unindex_active_relationship_properties(
2815            rel.id,
2816            [rel.rel_type.as_str()],
2817            &rel.properties,
2818        );
2819        self.maintain_relationship_secondary_indexes(rel, SecondaryIndexMutation::Remove);
2820    }
2821
2822    fn index_node_property_if_active<'a>(
2823        &mut self,
2824        node_id: NodeId,
2825        labels: impl IntoIterator<Item = &'a str>,
2826        key: &str,
2827        value: &PropertyValue,
2828    ) {
2829        if self.active_node_property_index_count() == 0 {
2830            return;
2831        }
2832        let (indexes, records) = self.node_index_parts();
2833        if indexes.node_properties.is_active(key) {
2834            indexes
2835                .node_properties
2836                .insert_with_scopes(node_id, labels, key, value, &records);
2837        }
2838    }
2839
2840    fn index_node_properties_if_active<'a>(
2841        &mut self,
2842        node_id: NodeId,
2843        labels: impl IntoIterator<Item = &'a str> + Clone,
2844        properties: &Properties,
2845    ) {
2846        if self.active_node_property_index_count() == 0 {
2847            return;
2848        }
2849        let (indexes, records) = self.node_index_parts();
2850        for (key, value) in properties {
2851            if indexes.node_properties.is_active(key) {
2852                indexes.node_properties.insert_with_scopes(
2853                    node_id,
2854                    labels.clone(),
2855                    key,
2856                    value,
2857                    &records,
2858                );
2859            }
2860        }
2861    }
2862
2863    fn unindex_node_property_if_active<'a>(
2864        &mut self,
2865        node_id: NodeId,
2866        labels: impl IntoIterator<Item = &'a str>,
2867        key: &str,
2868        value: &PropertyValue,
2869    ) {
2870        if self.active_node_property_index_count() == 0 {
2871            return;
2872        }
2873        let indexes = self.indexes_mut();
2874        if indexes.node_properties.is_active(key) {
2875            indexes
2876                .node_properties
2877                .remove_with_scopes(node_id, labels, key, value);
2878        }
2879    }
2880
2881    fn index_node_scope_properties_if_active(
2882        &mut self,
2883        node_id: NodeId,
2884        scope: &str,
2885        properties: &Properties,
2886    ) {
2887        if self.active_node_property_index_count() == 0 {
2888            return;
2889        }
2890        let (indexes, records) = self.node_index_parts();
2891        for (key, value) in properties {
2892            if indexes.node_properties.is_active(key) {
2893                indexes
2894                    .node_properties
2895                    .insert_scoped(node_id, scope, key, value, &records);
2896            }
2897        }
2898    }
2899
2900    fn unindex_node_scope_properties_if_active(
2901        &mut self,
2902        node_id: NodeId,
2903        scope: &str,
2904        properties: &Properties,
2905    ) {
2906        if self.active_node_property_index_count() == 0 {
2907            return;
2908        }
2909        let indexes = self.indexes_mut();
2910        for (key, value) in properties {
2911            if indexes.node_properties.is_active(key) {
2912                indexes
2913                    .node_properties
2914                    .remove_scoped(node_id, scope, key, value);
2915            }
2916        }
2917    }
2918
2919    fn unindex_active_node_properties<'a>(
2920        &mut self,
2921        node_id: NodeId,
2922        labels: impl IntoIterator<Item = &'a str> + Clone,
2923        properties: &Properties,
2924    ) {
2925        if self.active_node_property_index_count() == 0 {
2926            return;
2927        }
2928        let indexes = self.indexes_mut();
2929        for (key, value) in properties {
2930            if indexes.node_properties.is_active(key) {
2931                indexes
2932                    .node_properties
2933                    .remove_with_scopes(node_id, labels.clone(), key, value);
2934            }
2935        }
2936    }
2937
2938    fn index_relationship_property_if_active<'a>(
2939        &mut self,
2940        rel_id: RelationshipId,
2941        scopes: impl IntoIterator<Item = &'a str>,
2942        key: &str,
2943        value: &PropertyValue,
2944    ) {
2945        if self.active_relationship_property_index_count() == 0 {
2946            return;
2947        }
2948        let (indexes, records) = self.relationship_index_parts();
2949        if indexes.relationship_properties.is_active(key) {
2950            indexes
2951                .relationship_properties
2952                .insert_with_scopes(rel_id, scopes, key, value, &records);
2953        }
2954    }
2955
2956    fn index_relationship_properties_if_active<'a>(
2957        &mut self,
2958        rel_id: RelationshipId,
2959        scopes: impl IntoIterator<Item = &'a str> + Clone,
2960        properties: &Properties,
2961    ) {
2962        if self.active_relationship_property_index_count() == 0 {
2963            return;
2964        }
2965        let (indexes, records) = self.relationship_index_parts();
2966        for (key, value) in properties {
2967            if indexes.relationship_properties.is_active(key) {
2968                indexes.relationship_properties.insert_with_scopes(
2969                    rel_id,
2970                    scopes.clone(),
2971                    key,
2972                    value,
2973                    &records,
2974                );
2975            }
2976        }
2977    }
2978
2979    fn unindex_relationship_property_if_active<'a>(
2980        &mut self,
2981        rel_id: RelationshipId,
2982        scopes: impl IntoIterator<Item = &'a str>,
2983        key: &str,
2984        value: &PropertyValue,
2985    ) {
2986        if self.active_relationship_property_index_count() == 0 {
2987            return;
2988        }
2989        let indexes = self.indexes_mut();
2990        if indexes.relationship_properties.is_active(key) {
2991            indexes
2992                .relationship_properties
2993                .remove_with_scopes(rel_id, scopes, key, value);
2994        }
2995    }
2996
2997    fn unindex_active_relationship_properties<'a>(
2998        &mut self,
2999        rel_id: RelationshipId,
3000        scopes: impl IntoIterator<Item = &'a str> + Clone,
3001        properties: &Properties,
3002    ) {
3003        if self.active_relationship_property_index_count() == 0 {
3004            return;
3005        }
3006        let indexes = self.indexes_mut();
3007        for (key, value) in properties {
3008            if indexes.relationship_properties.is_active(key) {
3009                indexes.relationship_properties.remove_with_scopes(
3010                    rel_id,
3011                    scopes.clone(),
3012                    key,
3013                    value,
3014                );
3015            }
3016        }
3017    }
3018
3019    pub(super) fn scan_nodes_by_property(
3020        &self,
3021        label: Option<&str>,
3022        key: &str,
3023        value: &PropertyValue,
3024    ) -> Vec<NodeRecord> {
3025        match label {
3026            Some(label) => self
3027                .nodes_by_label
3028                .get(label)
3029                .into_iter()
3030                .flat_map(|ids| ids.iter())
3031                .filter_map(|id| self.node_at(id))
3032                .filter(|node| node.properties().get(key).is_some_and(|v| v == *value))
3033                .map(|node| node.to_record())
3034                .collect(),
3035            None => self
3036                .iter_node_refs()
3037                .filter(|node| node.properties().get(key).is_some_and(|v| v == *value))
3038                .map(|node| node.to_record())
3039                .collect(),
3040        }
3041    }
3042
3043    pub(super) fn scan_node_ids_by_property(
3044        &self,
3045        label: Option<&str>,
3046        key: &str,
3047        value: &PropertyValue,
3048    ) -> Vec<NodeId> {
3049        match label {
3050            Some(label) => self
3051                .nodes_by_label
3052                .get(label)
3053                .into_iter()
3054                .flat_map(|ids| ids.iter())
3055                .filter_map(|id| {
3056                    (self
3057                        .node_at(id)?
3058                        .properties()
3059                        .get(key)
3060                        .is_some_and(|v| v == *value))
3061                    .then_some(id)
3062                })
3063                .collect(),
3064            None => self
3065                .iter_nodes()
3066                .filter_map(|(id, node)| {
3067                    (node.properties().get(key).is_some_and(|v| v == *value)).then_some(id)
3068                })
3069                .collect(),
3070        }
3071    }
3072
3073    pub(super) fn any_node_by_property(
3074        &self,
3075        label: &str,
3076        key: &str,
3077        value: &PropertyValue,
3078    ) -> bool {
3079        self.nodes_by_label
3080            .get(label)
3081            .into_iter()
3082            .flat_map(|ids| ids.iter())
3083            .filter_map(|id| self.node_at(id))
3084            .any(|node| node.properties().get(key).is_some_and(|v| v == *value))
3085    }
3086
3087    pub(super) fn scan_relationships_by_property(
3088        &self,
3089        rel_type: Option<&str>,
3090        key: &str,
3091        value: &PropertyValue,
3092    ) -> Vec<RelationshipRecord> {
3093        match rel_type {
3094            Some(rel_type) => self
3095                .relationships_by_type
3096                .get(rel_type)
3097                .into_iter()
3098                .flat_map(|ids| ids.iter())
3099                .filter_map(|id| self.rel_at(id))
3100                .filter(|rel| rel.properties().get(key).is_some_and(|v| v == *value))
3101                .map(|rel| rel.to_record())
3102                .collect(),
3103            None => self
3104                .iter_rel_refs()
3105                .filter(|rel| rel.properties().get(key).is_some_and(|v| v == *value))
3106                .map(|rel| rel.to_record())
3107                .collect(),
3108        }
3109    }
3110
3111    pub(super) fn scan_relationship_ids_by_property(
3112        &self,
3113        rel_type: Option<&str>,
3114        key: &str,
3115        value: &PropertyValue,
3116    ) -> Vec<RelationshipId> {
3117        match rel_type {
3118            Some(rel_type) => self
3119                .relationships_by_type
3120                .get(rel_type)
3121                .into_iter()
3122                .flat_map(|ids| ids.iter())
3123                .filter_map(|id| {
3124                    (self
3125                        .rel_at(id)?
3126                        .properties()
3127                        .get(key)
3128                        .is_some_and(|v| v == *value))
3129                    .then_some(id)
3130                })
3131                .collect(),
3132            None => self
3133                .iter_rels()
3134                .filter_map(|(id, rel)| {
3135                    (rel.properties().get(key).is_some_and(|v| v == *value)).then_some(id)
3136                })
3137                .collect(),
3138        }
3139    }
3140
3141    pub(super) fn any_relationship_by_property(
3142        &self,
3143        rel_type: &str,
3144        key: &str,
3145        value: &PropertyValue,
3146    ) -> bool {
3147        self.relationships_by_type
3148            .get(rel_type)
3149            .into_iter()
3150            .flat_map(|ids| ids.iter())
3151            .filter_map(|id| self.rel_at(id))
3152            .any(|rel| rel.properties().get(key).is_some_and(|v| v == *value))
3153    }
3154
3155    pub(super) fn attach_relationship(&mut self, rel: &RelationshipRecord) {
3156        let type_id = self.dicts.types.id_or_insert(&rel.rel_type);
3157        self.adjacency_push(
3158            rel.src,
3159            Side::Out,
3160            AdjEntry {
3161                type_id,
3162                neighbour: rel.dst,
3163                rel: rel.id,
3164            },
3165        );
3166        self.adjacency_push(
3167            rel.dst,
3168            Side::In,
3169            AdjEntry {
3170                type_id,
3171                neighbour: rel.src,
3172                rel: rel.id,
3173            },
3174        );
3175        self.insert_relationship_type_index(rel.id, &rel.rel_type);
3176    }
3177
3178    fn detach_relationship_indexes(
3179        &mut self,
3180        rel: &RelationshipRecord,
3181        mut pending: Option<&mut PendingAdjRemovals>,
3182    ) {
3183        self.adjacency_remove(rel.src, Side::Out, rel.id, pending.as_deref_mut());
3184        self.adjacency_remove(rel.dst, Side::In, rel.id, pending);
3185
3186        self.remove_relationship_type_index(rel.id, &rel.rel_type);
3187    }
3188
3189    pub(super) fn relationship_ids_for_direction(
3190        &self,
3191        node_id: NodeId,
3192        direction: Direction,
3193    ) -> Vec<RelationshipId> {
3194        let mut ids = Vec::new();
3195        let _ = self.try_for_each_adjacent_id_unchecked(node_id, direction, &[], |rel_id, _| {
3196            ids.push(rel_id);
3197            Ok::<(), ()>(())
3198        });
3199        ids
3200    }
3201
3202    pub(super) fn has_incident_relationships(&self, node_id: NodeId) -> bool {
3203        self.outgoing_at(node_id).is_some_and(|adj| !adj.is_empty())
3204            || self.incoming_at(node_id).is_some_and(|adj| !adj.is_empty())
3205    }
3206
3207    pub(super) fn incident_relationship_ids(&self, node_id: NodeId) -> Vec<RelationshipId> {
3208        self.relationship_ids_for_direction(node_id, Direction::Undirected)
3209    }
3210
3211    /// Replay a node creation using the id captured in a durable mutation
3212    /// event. This intentionally does not emit a new mutation event: callers
3213    /// must invoke it before installing a recorder on the graph.
3214    #[doc(hidden)]
3215    pub fn replay_create_node(
3216        &mut self,
3217        id: NodeId,
3218        labels: Vec<String>,
3219        properties: Properties,
3220    ) -> Result<NodeRecord, String> {
3221        if self.recorder.is_some() {
3222            return Err(
3223                "cannot replay node creation while a mutation recorder is installed".into(),
3224            );
3225        }
3226        if self.has_node_at(id) {
3227            return Err(format!("node id {id} already exists"));
3228        }
3229        let idx = self.ensure_node_slot_checked(id)?;
3230        self.bump_next_node_id_past(id)?;
3231
3232        let labels = Self::normalize_labels(labels);
3233        let node = NodeRecord {
3234            id,
3235            labels,
3236            properties,
3237        };
3238
3239        self.put_node_at_slot(idx, &node);
3240        // Same index maintenance as a live create: only hash indexes that
3241        // are already active (declared by a replayed CREATE INDEX /
3242        // CREATE CONSTRAINT, which backfills from the data replayed so
3243        // far) are kept current. Lookup-activated (implicit) indexes are
3244        // rebuilt lazily on first use, exactly as in a fresh process.
3245        self.on_node_created(&node);
3246
3247        Ok(node)
3248    }
3249
3250    /// Replay a relationship creation using the id captured in a durable
3251    /// mutation event. This intentionally does not emit a new mutation event:
3252    /// callers must invoke it before installing a recorder on the graph.
3253    #[doc(hidden)]
3254    pub fn replay_create_relationship(
3255        &mut self,
3256        id: RelationshipId,
3257        src: NodeId,
3258        dst: NodeId,
3259        rel_type: &str,
3260        properties: Properties,
3261    ) -> Result<RelationshipRecord, String> {
3262        if self.recorder.is_some() {
3263            return Err(
3264                "cannot replay relationship creation while a mutation recorder is installed".into(),
3265            );
3266        }
3267        if self.has_rel_at(id) {
3268            return Err(format!("relationship id {id} already exists"));
3269        }
3270        if !self.has_node_at(src) {
3271            return Err(format!(
3272                "relationship {id} references missing source node {src}"
3273            ));
3274        }
3275        if !self.has_node_at(dst) {
3276            return Err(format!(
3277                "relationship {id} references missing target node {dst}"
3278            ));
3279        }
3280
3281        let trimmed = rel_type.trim();
3282        if trimmed.is_empty() {
3283            return Err(format!("relationship {id} has an empty type"));
3284        }
3285        let idx = self.ensure_rel_slot_checked(id)?;
3286        self.bump_next_rel_id_past(id)?;
3287
3288        let rel = RelationshipRecord {
3289            id,
3290            src,
3291            dst,
3292            rel_type: trimmed.into(),
3293            properties,
3294        };
3295
3296        self.put_rel_at_slot(idx, &rel);
3297        // See `replay_create_node`: active (declared) indexes only.
3298        self.on_relationship_created(&rel);
3299
3300        Ok(rel)
3301    }
3302
3303    #[cfg(test)]
3304    pub(super) fn assert_property_indexes_match_scan(&self) {
3305        let indexes = self.indexes_read();
3306        assert_eq!(
3307            indexes.node_properties.active_keys.len(),
3308            self.active_node_property_index_count(),
3309            "node property index counter diverged from active key set"
3310        );
3311        assert_eq!(
3312            indexes.relationship_properties.active_keys.len(),
3313            self.active_relationship_property_index_count(),
3314            "relationship property index counter diverged from active key set"
3315        );
3316
3317        let mut expected_nodes = PropertyIndexState {
3318            active_keys: indexes.node_properties.active_keys.clone(),
3319            any_scope_keys: indexes.node_properties.any_scope_keys.clone(),
3320            ..PropertyIndexState::default()
3321        };
3322        for (id, node) in self.iter_nodes() {
3323            for (key, value) in node.properties() {
3324                if expected_nodes.is_active(key) {
3325                    expected_nodes.insert_with_scopes(
3326                        id,
3327                        node.labels().strs(),
3328                        key,
3329                        &value.to_owned(),
3330                        &self.node_values(),
3331                    );
3332                }
3333            }
3334        }
3335        assert_eq!(
3336            indexes.node_properties.scoped_values, expected_nodes.scoped_values,
3337            "node property scoped index values diverged from scan"
3338        );
3339        assert_eq!(
3340            indexes.node_properties.any_scope, expected_nodes.any_scope,
3341            "node property across-scopes index values diverged from scan"
3342        );
3343
3344        let mut expected_relationships = PropertyIndexState {
3345            active_keys: indexes.relationship_properties.active_keys.clone(),
3346            any_scope_keys: indexes.relationship_properties.any_scope_keys.clone(),
3347            ..PropertyIndexState::default()
3348        };
3349        for (id, rel) in self.iter_rels() {
3350            for (key, value) in rel.properties() {
3351                if expected_relationships.is_active(key) {
3352                    expected_relationships.insert_with_scopes(
3353                        id,
3354                        [rel.rel_type()],
3355                        key,
3356                        &value.to_owned(),
3357                        &self.relationship_values(),
3358                    );
3359                }
3360            }
3361        }
3362        assert_eq!(
3363            indexes.relationship_properties.scoped_values, expected_relationships.scoped_values,
3364            "relationship property scoped index values diverged from scan"
3365        );
3366        assert_eq!(
3367            indexes.relationship_properties.any_scope, expected_relationships.any_scope,
3368            "relationship property across-scopes index values diverged from scan"
3369        );
3370    }
3371}
3372
3373/// Property values read from a slab's records, for the hash property
3374/// index's digest entries (see `PropertyValueBuckets`).
3375pub(super) struct RecordValues<'g> {
3376    slab: &'g RecordSlab,
3377    dicts: &'g Dicts,
3378    nodes: bool,
3379}
3380
3381impl<'g> RecordValues<'g> {
3382    pub(super) fn nodes(slab: &'g RecordSlab, dicts: &'g Dicts) -> Self {
3383        Self {
3384            slab,
3385            dicts,
3386            nodes: true,
3387        }
3388    }
3389
3390    pub(super) fn relationships(slab: &'g RecordSlab, dicts: &'g Dicts) -> Self {
3391        Self {
3392            slab,
3393            dicts,
3394            nodes: false,
3395        }
3396    }
3397}
3398
3399impl EntityValues for RecordValues<'_> {
3400    #[inline]
3401    fn get(&self, id: u64, key: &str) -> Option<crate::ValueRef<'_>> {
3402        let bytes = self.slab.get(usize::try_from(id).ok()?)?;
3403        if self.nodes {
3404            StoredNode::new(id, bytes, self.dicts).properties().get(key)
3405        } else {
3406            StoredRel::new(id, bytes, self.dicts).properties().get(key)
3407        }
3408    }
3409}
3410
3411/// Set or remove one property of the record in `slab` slot `idx`,
3412/// rewriting only that entry. The previous value; `None` when the slot is
3413/// empty or there was nothing to remove.
3414#[inline]
3415fn edit_record_property(
3416    slab: &mut RecordSlab,
3417    idx: usize,
3418    kind: encoded::RecordKind,
3419    dicts: &mut Dicts,
3420    key: &str,
3421    edit: encoded::PropEdit<'_>,
3422) -> Option<Option<PropertyValue>> {
3423    let bytes = slab.get(idx)?;
3424    let mut out = Scratch::take();
3425    let old = encoded::edit_property(bytes, kind, dicts, key, edit, &mut out)?;
3426    slab.set(idx, &out);
3427    Some(old)
3428}
3429
3430/// Option of a constraint's backing RANGE index: `false` when it keeps the
3431/// hash index only (see [`ConstraintBacking::Hash`]).
3432pub(super) const SORTED_OPTION: &str = "sorted";
3433
3434/// Whether a RANGE index keeps a sorted copy of its values.
3435pub(super) fn is_sorted(def: &IndexDefinition) -> bool {
3436    !matches!(
3437        def.options.get(SORTED_OPTION),
3438        Some(IndexConfigValue::Bool(false))
3439    )
3440}
3441
3442/// Entities from which a property index is built on every core: below
3443/// it, splitting the work costs more than it saves.
3444const PARALLEL_INDEX_MIN: usize = 1 << 17;
3445
3446/// Data work a restore registers its catalogs without, to do it all at
3447/// once in [`InMemoryGraph::run_deferred`].
3448#[derive(Default)]
3449pub(super) struct DeferredData {
3450    /// Indexes to populate from the records.
3451    pub(super) populate: Vec<IndexDefinition>,
3452    /// Constraints to check the records against.
3453    pub(super) validate: Vec<super::ConstraintDefinition>,
3454}