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;
35#[cfg(test)]
36use super::property_index::PropertyIndexState;
37use super::property_index::{EntityValues, PropertyIndexKey, PropertyIndexRegistry, UNLABELLED};
38use super::record_slab::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    pub(super) fn edit_rel_property(
371        &mut self,
372        id: RelationshipId,
373        key: &str,
374        edit: encoded::PropEdit<'_>,
375    ) -> Option<Option<PropertyValue>> {
376        let idx = Self::slot_index(id)?;
377        edit_record_property(
378            &mut self.relationships,
379            idx,
380            encoded::RecordKind::Relationship,
381            &mut self.dicts,
382            key,
383            edit,
384        )
385    }
386
387    /// Resize the node-keyed Vecs so `id as usize` is in range. Adjacency
388    /// lists are kept in lockstep with `nodes`, so a freshly-grown slot has
389    /// empty outgoing/incoming Vecs ready to receive edges.
390    fn slot_len_for_id(id: u64, kind: &str) -> Result<usize, String> {
391        if id > MAX_ENTITY_ID {
392            return Err(format!(
393                "{kind} id {id} is above the largest supported id {MAX_ENTITY_ID}"
394            ));
395        }
396        let idx = usize::try_from(id)
397            .map_err(|_| format!("{kind} id {id} does not fit in usize on this platform"))?;
398        idx.checked_add(1)
399            .ok_or_else(|| format!("{kind} id {id} leaves no valid slab slot"))
400    }
401
402    #[inline]
403    fn slot_index(id: u64) -> Option<usize> {
404        usize::try_from(id).ok()
405    }
406
407    fn ensure_node_slot_checked(&mut self, id: NodeId) -> Result<usize, String> {
408        let target = Self::slot_len_for_id(id, "node")?;
409        if self.nodes.len() < target {
410            let additional = target - self.nodes.len();
411            self.nodes.try_reserve(additional).map_err(|e| {
412                format!("node id {id} requires {target} slots, but allocation failed: {e}")
413            })?;
414            self.adjacency.try_reserve_exact(additional).map_err(|e| {
415                format!(
416                    "node id {id} requires {target} adjacency slots, but allocation failed: {e}"
417                )
418            })?;
419            self.nodes.grow_to(target);
420            self.adjacency.grow_shared(target, adjacency::empty_leaf());
421        }
422        Ok(target - 1)
423    }
424
425    fn ensure_rel_slot_checked(&mut self, id: RelationshipId) -> Result<usize, String> {
426        let target = Self::slot_len_for_id(id, "relationship")?;
427        if self.relationships.len() < target {
428            self.relationships
429                .try_reserve(target - self.relationships.len())
430                .map_err(|e| {
431                    format!(
432                        "relationship id {id} requires {target} slots, but allocation failed: {e}"
433                    )
434                })?;
435            self.relationships.grow_to(target);
436        }
437        Ok(target - 1)
438    }
439
440    pub(super) fn put_node_checked(&mut self, id: NodeId, node: &NodeRecord) -> Result<(), String> {
441        let idx = self.ensure_node_slot_checked(id)?;
442        self.put_node_at_slot(idx, node);
443        Ok(())
444    }
445
446    pub(super) fn put_rel_checked(
447        &mut self,
448        id: RelationshipId,
449        rel: &RelationshipRecord,
450    ) -> Result<(), String> {
451        let idx = self.ensure_rel_slot_checked(id)?;
452        self.put_rel_at_slot(idx, rel);
453        Ok(())
454    }
455
456    pub(super) fn put_node_at_slot(&mut self, idx: usize, node: &NodeRecord) {
457        if self.store_node(idx, node) {
458            self.live_node_count += 1;
459        }
460    }
461
462    pub(super) fn put_rel_at_slot(&mut self, idx: usize, rel: &RelationshipRecord) {
463        let mut out = Scratch::take();
464        encoded::encode_rel(rel, &mut self.dicts, &mut out);
465        if self.relationships.set(idx, &out) {
466            self.live_rel_count += 1;
467        }
468    }
469
470    /// Encode `node` into slot `idx`; returns whether the slot was empty.
471    #[inline]
472    fn store_node(&mut self, idx: usize, node: &NodeRecord) -> bool {
473        let mut out = Scratch::take();
474        encoded::encode_node(node, &mut self.dicts, &mut out);
475        self.nodes.set(idx, &out)
476    }
477
478    pub(super) fn take_node(&mut self, id: NodeId) -> Option<NodeRecord> {
479        let idx = Self::slot_index(id)?;
480        let removed = StoredNode::new(id, self.nodes.get(idx)?, &self.dicts).to_record();
481        self.nodes.remove(idx);
482        self.live_node_count -= 1;
483        // Also clear the per-id adjacency entries so the memory is reclaimed
484        // on the typical "delete every node" pattern. We deliberately do not
485        // shrink the slots — leaving the slot lets new ids reuse the same
486        // index without growth churn (and `next_node_id` is monotonic
487        // anyway, so no immediate reuse).
488        self.clear_adjacency(id);
489        Some(removed)
490    }
491
492    pub(super) fn take_rel(&mut self, id: RelationshipId) -> Option<RelationshipRecord> {
493        let idx = Self::slot_index(id)?;
494        let removed = StoredRel::new(id, self.relationships.get(idx)?, &self.dicts).to_record();
495        self.relationships.remove(idx);
496        self.live_rel_count -= 1;
497        Some(removed)
498    }
499
500    #[inline]
501    pub(super) fn outgoing_at(&self, id: NodeId) -> Option<AdjSide<'_>> {
502        Some(self.adjacency.get(Self::slot_index(id)?)?.outgoing())
503    }
504
505    #[inline]
506    pub(super) fn incoming_at(&self, id: NodeId) -> Option<AdjSide<'_>> {
507        Some(self.adjacency.get(Self::slot_index(id)?)?.incoming())
508    }
509
510    /// Visit the entries of one adjacency list that pass `filter`.
511    /// `skip_self_loops` is set for the incoming half of an undirected
512    /// walk, whose outgoing half already reported the node's self-loops.
513    #[inline]
514    fn try_for_each_adjacent_entry<F, E>(
515        node_id: NodeId,
516        filter: &TypeFilter,
517        adj: AdjSide<'_>,
518        skip_self_loops: bool,
519        visit: &mut F,
520    ) -> Result<(), E>
521    where
522        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
523    {
524        for entry in adj.iter() {
525            if skip_self_loops && entry.neighbour == node_id {
526                continue;
527            }
528            if !filter.matches(entry.type_id) {
529                continue;
530            }
531            visit(entry.rel, entry.neighbour)?;
532        }
533        Ok(())
534    }
535
536    #[inline]
537    pub(super) fn try_for_each_adjacent_id_unchecked<F, E>(
538        &self,
539        node_id: NodeId,
540        direction: Direction,
541        types: &[String],
542        mut visit: F,
543    ) -> Result<(), E>
544    where
545        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
546    {
547        let filter = TypeFilter::resolve(&self.dicts.types, types);
548        if matches!(filter, TypeFilter::Nothing) {
549            return Ok(());
550        }
551        if !matches!(direction, Direction::Left) {
552            if let Some(adj) = self.outgoing_at(node_id) {
553                Self::try_for_each_adjacent_entry(node_id, &filter, adj, false, &mut visit)?;
554            }
555        }
556        if !matches!(direction, Direction::Right) {
557            if let Some(adj) = self.incoming_at(node_id) {
558                let skip_self_loops = matches!(direction, Direction::Undirected);
559                Self::try_for_each_adjacent_entry(
560                    node_id,
561                    &filter,
562                    adj,
563                    skip_self_loops,
564                    &mut visit,
565                )?;
566            }
567        }
568
569        Ok(())
570    }
571
572    #[inline]
573    pub(super) fn try_for_each_adjacent_id<F, E>(
574        &self,
575        node_id: NodeId,
576        direction: Direction,
577        types: &[String],
578        visit: F,
579    ) -> Result<(), E>
580    where
581        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
582    {
583        if !self.has_node_at(node_id) {
584            return Ok(());
585        }
586        self.try_for_each_adjacent_id_unchecked(node_id, direction, types, visit)
587    }
588
589    pub(super) fn iter_node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
590        self.nodes.slots_from(0).map(|i| i as NodeId)
591    }
592
593    pub(super) fn iter_node_refs(&self) -> impl Iterator<Item = NodeRef<'_>> + '_ {
594        self.iter_nodes().map(|(_, node)| node)
595    }
596
597    pub(super) fn iter_rel_ids(&self) -> impl Iterator<Item = RelationshipId> + '_ {
598        self.relationships
599            .slots_from(0)
600            .map(|i| i as RelationshipId)
601    }
602
603    pub(super) fn iter_rel_refs(&self) -> impl Iterator<Item = RelRef<'_>> + '_ {
604        self.iter_rels().map(|(_, rel)| rel)
605    }
606
607    pub(super) fn iter_nodes(&self) -> impl Iterator<Item = (NodeId, NodeRef<'_>)> + '_ {
608        let dicts = &self.dicts;
609        self.nodes.iter().map(move |(i, bytes)| {
610            let id = i as NodeId;
611            (id, NodeRef::stored(StoredNode::new(id, bytes, dicts)))
612        })
613    }
614
615    pub(super) fn iter_rels(&self) -> impl Iterator<Item = (RelationshipId, RelRef<'_>)> + '_ {
616        let dicts = &self.dicts;
617        self.relationships.iter().map(move |(i, bytes)| {
618            let id = i as RelationshipId;
619            (id, RelRef::stored(StoredRel::new(id, bytes, dicts)))
620        })
621    }
622
623    /// Add an entry to `node_id`'s list on `side`. Relies on
624    /// the monotonic-id invariant: relationship ids are allocated once and
625    /// never re-used, so the list can never see a duplicate.
626    fn adjacency_push(&mut self, node_id: NodeId, side: Side, entry: AdjEntry) {
627        if let Ok(idx) = self.ensure_node_slot_checked(node_id) {
628            self.adjacency[idx].push(side, entry);
629        }
630    }
631
632    /// Drop both of `node_id`'s lists at once. A detach delete does this
633    /// first, so removing each relationship touches only the far end's list
634    /// instead of scanning and compacting the node's own list once per
635    /// relationship (quadratic in its degree).
636    pub(super) fn clear_adjacency(&mut self, node_id: NodeId) {
637        let Some(idx) = Self::slot_index(node_id) else {
638            return;
639        };
640        // An empty list needs no clearing, and reaching it mutably would
641        // copy its shared leaf for nothing.
642        if self.adjacency.get(idx).is_some_and(|adj| !adj.is_empty()) {
643            if let Some(adj) = self.adjacency.get_mut(idx) {
644                adj.clear();
645            }
646        }
647    }
648
649    /// Remove `rel_id` from `node_id`'s list on `side`. With `pending`, a
650    /// list longer than [`DEFER_REMOVAL_MIN_BYTES`] is left for
651    /// [`Self::flush_adjacency_removals`], which takes all of a list's
652    /// pending entries in one pass: one at a time, each removal scans and
653    /// compacts the list, which is quadratic when a statement deletes
654    /// many relationships of one hub. A short list is edited at once.
655    fn adjacency_remove(
656        &mut self,
657        node_id: NodeId,
658        side: Side,
659        rel_id: RelationshipId,
660        pending: Option<&mut PendingAdjRemovals>,
661    ) {
662        let Some(idx) = Self::slot_index(node_id) else {
663            return;
664        };
665        let len = self
666            .adjacency
667            .get(idx)
668            .map_or(0, |adj| adj.side_bytes(side));
669        // An empty list (a node detach delete already cleared) needs no
670        // edit, and reaching it mutably would copy its shared leaf.
671        if len == 0 {
672            return;
673        }
674        match pending {
675            Some(pending) if len > DEFER_REMOVAL_MIN_BYTES => {
676                pending.push((node_id, side, rel_id));
677            }
678            _ => {
679                if let Some(adj) = self.adjacency.get_mut(idx) {
680                    adj.remove(side, rel_id);
681                }
682            }
683        }
684    }
685
686    /// Apply the removals [`Self::adjacency_remove`] deferred: each list
687    /// once, keeping the order of its other entries. Leaves `pending` empty.
688    pub(super) fn flush_adjacency_removals(&mut self, pending: &mut PendingAdjRemovals) {
689        pending.sort_unstable_by_key(|&(node, side, rel)| (node, side == Side::In, rel));
690        let mut rels = Vec::new();
691        for group in pending.chunk_by(|a, b| a.0 == b.0 && a.1 == b.1) {
692            let (node_id, side, _) = group[0];
693            let Some(idx) = Self::slot_index(node_id) else {
694                continue;
695            };
696            if self
697                .adjacency
698                .get(idx)
699                .is_none_or(|adj| adj.side_bytes(side) == 0)
700            {
701                continue;
702            }
703            rels.clear();
704            rels.extend(group.iter().map(|&(_, _, rel)| rel));
705            if let Some(adj) = self.adjacency.get_mut(idx) {
706                adj.remove_all(side, &rels);
707            }
708        }
709        pending.clear();
710    }
711
712    pub(super) fn normalize_labels(labels: Vec<String>) -> Labels {
713        let mut out = Labels::new();
714        for label in &labels {
715            let label = label.trim();
716            if !label.is_empty() && !out.has(label) {
717                out.push(label);
718            }
719        }
720        out
721    }
722
723    pub(super) fn insert_node_label_index(&mut self, node_id: NodeId, label: &str) {
724        // Hot path: skip the `String` alloc when the label bucket already
725        // exists. The monotonic-id invariant on the create path guarantees
726        // `node_id` is unique, so we push unconditionally; the previous
727        // `contains` guard turned bulk CREATE into O(n²).
728        if let Some(bucket) = self.nodes_by_label.get_mut(label) {
729            bucket.insert(node_id);
730        } else {
731            self.nodes_by_label
732                .insert(Arc::from(label), std::iter::once(node_id).collect());
733        }
734    }
735
736    fn remove_node_label_index(&mut self, node_id: NodeId, label: &str) {
737        if let Some(ids) = self.nodes_by_label.get_mut(label) {
738            ids.remove(node_id);
739            if ids.is_empty() {
740                self.nodes_by_label.remove(label);
741            }
742        }
743    }
744
745    fn insert_relationship_type_index(&mut self, rel_id: RelationshipId, rel_type: &str) {
746        // See `insert_node_label_index` for the same hot-path rationale.
747        if let Some(bucket) = self.relationships_by_type.get_mut(rel_type) {
748            bucket.insert(rel_id);
749        } else {
750            self.relationships_by_type
751                .insert(Arc::from(rel_type), std::iter::once(rel_id).collect());
752        }
753    }
754
755    fn remove_relationship_type_index(&mut self, rel_id: RelationshipId, rel_type: &str) {
756        if let Some(ids) = self.relationships_by_type.get_mut(rel_type) {
757            ids.remove(rel_id);
758            if ids.is_empty() {
759                self.relationships_by_type.remove(rel_type);
760            }
761        }
762    }
763
764    /// Ids of `label` nodes whose `key` equals `value`, from the scoped
765    /// hash index. `None` when that index is not active for `key` or
766    /// `value` has no index image; the caller must then scan.
767    pub(super) fn indexed_node_ids(
768        &self,
769        label: &str,
770        key: &str,
771        value: &PropertyValue,
772    ) -> Option<Vec<NodeId>> {
773        PropertyIndexKey::from_value(value)?;
774        let indexes = self.indexes_read();
775        if !indexes.node_properties.is_active(key) {
776            return None;
777        }
778        Some(
779            indexes
780                .node_properties
781                .scoped_ids_for(label, key, value, &self.node_values())
782                .map(|ids| ids.to_vec())
783                .unwrap_or_default(),
784        )
785    }
786
787    /// Relationship counterpart of [`Self::indexed_node_ids`].
788    pub(super) fn indexed_rel_ids(
789        &self,
790        rel_type: &str,
791        key: &str,
792        value: &PropertyValue,
793    ) -> Option<Vec<RelationshipId>> {
794        PropertyIndexKey::from_value(value)?;
795        let indexes = self.indexes_read();
796        if !indexes.relationship_properties.is_active(key) {
797            return None;
798        }
799        Some(
800            indexes
801                .relationship_properties
802                .scoped_ids_for(rel_type, key, value, &self.relationship_values())
803                .map(|ids| ids.to_vec())
804                .unwrap_or_default(),
805        )
806    }
807
808    pub(super) fn indexes_read(&self) -> std::sync::RwLockReadGuard<'_, PropertyIndexRegistry> {
809        self.indexes
810            .properties
811            .read()
812            .unwrap_or_else(|poisoned| poisoned.into_inner())
813    }
814
815    pub(super) fn indexes_write(&self) -> RwLockWriteGuard<'_, PropertyIndexRegistry> {
816        self.indexes
817            .properties
818            .write()
819            .unwrap_or_else(|poisoned| poisoned.into_inner())
820    }
821
822    /// Reads node property values from their records, for the hash
823    /// property index (see [`RecordValues`]).
824    #[inline]
825    pub(super) fn node_values(&self) -> RecordValues<'_> {
826        RecordValues::nodes(&self.nodes, &self.dicts)
827    }
828
829    /// Relationship counterpart of [`Self::node_values`].
830    #[inline]
831    pub(super) fn relationship_values(&self) -> RecordValues<'_> {
832        RecordValues::relationships(&self.relationships, &self.dicts)
833    }
834
835    /// [`Self::indexes_mut`] with [`Self::node_values`], borrowed together.
836    fn node_index_parts(&mut self) -> (&mut PropertyIndexRegistry, RecordValues<'_>) {
837        let registry = self
838            .indexes
839            .properties
840            .get_mut()
841            .unwrap_or_else(|poisoned| poisoned.into_inner());
842        (registry, RecordValues::nodes(&self.nodes, &self.dicts))
843    }
844
845    /// [`Self::indexes_mut`] with [`Self::relationship_values`].
846    fn relationship_index_parts(&mut self) -> (&mut PropertyIndexRegistry, RecordValues<'_>) {
847        let registry = self
848            .indexes
849            .properties
850            .get_mut()
851            .unwrap_or_else(|poisoned| poisoned.into_inner());
852        (
853            registry,
854            RecordValues::relationships(&self.relationships, &self.dicts),
855        )
856    }
857
858    pub(super) fn indexes_mut(&mut self) -> &mut PropertyIndexRegistry {
859        self.indexes
860            .properties
861            .get_mut()
862            .unwrap_or_else(|poisoned| poisoned.into_inner())
863    }
864
865    #[inline]
866    pub(super) fn active_node_property_index_count(&self) -> usize {
867        self.indexes
868            .active_node_property_indexes
869            .load(Ordering::Relaxed)
870    }
871
872    #[inline]
873    pub(super) fn active_relationship_property_index_count(&self) -> usize {
874        self.indexes
875            .active_relationship_property_indexes
876            .load(Ordering::Relaxed)
877    }
878
879    #[inline]
880    pub(super) fn active_constraint_count(&self) -> usize {
881        self.active_constraints.load(Ordering::Relaxed)
882    }
883
884    #[inline]
885    pub(super) fn has_active_constraints(&self) -> bool {
886        self.active_constraint_count() != 0
887    }
888
889    #[inline]
890    pub(super) fn active_fulltext_index_count(&self) -> usize {
891        self.indexes.active_fulltext_indexes.load(Ordering::Relaxed)
892    }
893
894    #[inline]
895    pub(super) fn has_active_fulltext_indexes(&self) -> bool {
896        self.active_fulltext_index_count() != 0
897    }
898
899    pub(super) fn node_property_index_is_active(&mut self, key: &str) -> bool {
900        self.active_node_property_index_count() != 0
901            && self.indexes_mut().node_properties.is_active(key)
902    }
903
904    pub(super) fn relationship_property_index_is_active(&mut self, key: &str) -> bool {
905        self.active_relationship_property_index_count() != 0
906            && self.indexes_mut().relationship_properties.is_active(key)
907    }
908
909    /// Give `key` its across-labels map, for lookups that name no label.
910    /// Call after [`Self::ensure_node_property_index`].
911    pub(super) fn ensure_any_scope_node_property_index(&self, key: &str) {
912        if self.indexes_read().node_properties.any_scope_is_active(key) {
913            return;
914        }
915        self.indexes_write()
916            .node_properties
917            .activate_any_scope(key, &self.node_values());
918    }
919
920    pub(super) fn ensure_any_scope_relationship_property_index(&self, key: &str) {
921        if self
922            .indexes_read()
923            .relationship_properties
924            .any_scope_is_active(key)
925        {
926            return;
927        }
928        self.indexes_write()
929            .relationship_properties
930            .activate_any_scope(key, &self.relationship_values());
931    }
932
933    pub(super) fn ensure_node_property_index(&self, key: &str) {
934        {
935            let indexes = self.indexes_read();
936            if indexes.node_properties.is_active(key) {
937                return;
938            }
939        }
940
941        let mut indexes = self.indexes_write();
942        if indexes.node_properties.is_active(key) {
943            return;
944        }
945
946        indexes.node_properties.insert_bulk(
947            key,
948            || {
949                self.iter_nodes().filter_map(|(id, node)| {
950                    let value = node.properties().get(key)?;
951                    Some((id, node.labels().strs(), value))
952                })
953            },
954            &self.node_values(),
955        );
956        if indexes.node_properties.activate(key) {
957            self.indexes
958                .active_node_property_indexes
959                .fetch_add(1, Ordering::Relaxed);
960        }
961    }
962
963    pub(super) fn ensure_relationship_property_index(&self, key: &str) {
964        {
965            let indexes = self.indexes_read();
966            if indexes.relationship_properties.is_active(key) {
967                return;
968            }
969        }
970
971        let mut indexes = self.indexes_write();
972        if indexes.relationship_properties.is_active(key) {
973            return;
974        }
975
976        indexes.relationship_properties.insert_bulk(
977            key,
978            || {
979                self.iter_rels().filter_map(|(id, rel)| {
980                    let value = rel.properties().get(key)?;
981                    Some((id, [rel.rel_type()], value))
982                })
983            },
984            &self.relationship_values(),
985        );
986        if indexes.relationship_properties.activate(key) {
987            self.indexes
988                .active_relationship_property_indexes
989                .fetch_add(1, Ordering::Relaxed);
990        }
991    }
992
993    pub(super) fn index_catalog_read(&self) -> IndexRead<'_, IndexCatalog> {
994        read_shared(&self.indexes.catalog)
995    }
996
997    /// Mutable access copies the catalog first if a clone still shares it.
998    pub(super) fn index_catalog_write(&self) -> IndexWrite<'_, IndexCatalog> {
999        write_shared(&self.indexes.catalog)
1000    }
1001
1002    pub(super) fn constraint_catalog_read(&self) -> IndexRead<'_, ConstraintCatalog> {
1003        read_shared(&self.constraint_catalog)
1004    }
1005
1006    /// Mutable access copies the catalog first if a clone still shares it.
1007    pub(super) fn constraint_catalog_write(&self) -> IndexWrite<'_, ConstraintCatalog> {
1008        write_shared(&self.constraint_catalog)
1009    }
1010
1011    /// Register an explicitly-declared index in the catalog and, when
1012    /// applicable, force the underlying property-index buckets to be
1013    /// populated so equality lookups can use them immediately.
1014    ///
1015    /// Named with a `register_` prefix to avoid colliding with the
1016    /// trait method `GraphStorageMut::create_index` — the trait impl
1017    /// in `impls.rs` delegates here.
1018    #[allow(clippy::result_large_err)]
1019    pub(super) fn register_index(
1020        &self,
1021        request: IndexRequest,
1022        if_not_exists: bool,
1023    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1024        self.register_index_with_recording(request, if_not_exists, true)
1025    }
1026
1027    #[allow(clippy::result_large_err)]
1028    fn register_index_with_recording(
1029        &self,
1030        request: IndexRequest,
1031        if_not_exists: bool,
1032        record_event: bool,
1033    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1034        let request_for_event = record_event.then(|| request.clone());
1035        let outcome = {
1036            let mut catalog = self.index_catalog_write();
1037            catalog.try_create(request, if_not_exists)?
1038        };
1039
1040        if let CreateIndexOutcome::Created(def) = &outcome {
1041            self.populate_index_data(def);
1042        }
1043
1044        // Both Created and NoOpExists are committed catalog states; we
1045        // log only Created because NoOpExists implies a redundant DDL
1046        // that adds nothing to durable state.
1047        if matches!(outcome, CreateIndexOutcome::Created(_)) {
1048            if let Some(request_for_event) = request_for_event {
1049                self.emit(|| crate::MutationEvent::CreateIndex {
1050                    request: request_for_event,
1051                    if_not_exists,
1052                });
1053            }
1054        }
1055
1056        Ok(outcome)
1057    }
1058
1059    /// Replay a CreateIndex event against an empty graph. Mirrors the
1060    /// `replay_create_node` shape: callers must invoke before installing
1061    /// a recorder so we don't re-emit during recovery.
1062    #[doc(hidden)]
1063    pub fn replay_create_index(
1064        &mut self,
1065        request: IndexRequest,
1066        if_not_exists: bool,
1067    ) -> Result<(), String> {
1068        if self.recorder.is_some() {
1069            return Err("cannot replay create_index while a mutation recorder is installed".into());
1070        }
1071        self.register_index(request, if_not_exists)
1072            .map(|_| ())
1073            .map_err(|e| e.to_string())
1074    }
1075
1076    /// Replay a DropIndex event.
1077    #[doc(hidden)]
1078    pub fn replay_drop_index(&mut self, name: &str, if_exists: bool) -> Result<(), String> {
1079        if self.recorder.is_some() {
1080            return Err("cannot replay drop_index while a mutation recorder is installed".into());
1081        }
1082        self.drop_named_index(name, if_exists)
1083            .map(|_| ())
1084            .map_err(|e| e.to_string())
1085    }
1086
1087    /// Register a constraint. For uniqueness/key kinds this also
1088    /// registers a backing RANGE index in the index catalog under the
1089    /// same name. Validation of existing data is the caller's
1090    /// responsibility (the enforcement layer runs a pre-create scan
1091    /// before this method commits).
1092    pub(super) fn register_constraint(
1093        &self,
1094        request: ConstraintRequest,
1095        if_not_exists: bool,
1096    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1097        // Constraint-level conflicts (22N65/66/67) take precedence over
1098        // index-catalog conflicts: if the request collides with an
1099        // existing *constraint* shape or name, we never get to the
1100        // backing-index step.
1101        {
1102            let constraint_catalog = self.constraint_catalog_read();
1103            if let Some(existing) = constraint_catalog.find_equivalent(&request) {
1104                let cloned = existing.clone();
1105                drop(constraint_catalog);
1106                if if_not_exists {
1107                    return Ok(CreateConstraintOutcome::NoOpExists(cloned));
1108                }
1109                return Err(CreateConstraintError::EquivalentConstraintExists(
1110                    cloned.name,
1111                ));
1112            }
1113            if let Some(existing) = constraint_catalog.get(&request.name) {
1114                let cloned = existing.clone();
1115                drop(constraint_catalog);
1116                if if_not_exists {
1117                    return Ok(CreateConstraintOutcome::NoOpExists(cloned));
1118                }
1119                return Err(CreateConstraintError::DuplicateName(cloned.name));
1120            }
1121            if let Some(existing) = constraint_catalog.find_same_schema(&request) {
1122                let cloned = existing.clone();
1123                drop(constraint_catalog);
1124                if super::constraint_catalog::kinds_conflict_for_validation(
1125                    &cloned.kind,
1126                    &request.kind,
1127                ) {
1128                    if if_not_exists {
1129                        return Ok(CreateConstraintOutcome::NoOpExists(cloned));
1130                    }
1131                    return Err(CreateConstraintError::ConflictingConstraint(cloned.name));
1132                }
1133            }
1134        }
1135
1136        // Index-catalog conflicts only matter for constraints that need
1137        // a backing range index. The catalog won't yet own one for this
1138        // request — that registration happens below — so any existing
1139        // entry under the same name or schema is from a foreign index.
1140        if request.kind.requires_backing_index() {
1141            let idx_catalog = self.index_catalog_read();
1142            if idx_catalog.get(&request.name).is_some() {
1143                return Err(CreateConstraintError::DuplicateIndexName(
1144                    request.name.clone(),
1145                ));
1146            }
1147            let conflict = idx_catalog.list().into_iter().find(|def| {
1148                def.kind == StoredIndexKind::Range
1149                    && def.entity == request.entity
1150                    && def.label.as_deref() == Some(request.label.as_str())
1151                    && def.properties == request.properties
1152                    && def.name != request.name
1153            });
1154            drop(idx_catalog);
1155            if let Some(def) = conflict {
1156                return Err(CreateConstraintError::BackingIndexConflict(format!(
1157                    "(:{} {{{}}}) already covered by index `{}`",
1158                    request.label,
1159                    request.properties.join(", "),
1160                    def.name,
1161                )));
1162            }
1163        }
1164
1165        let owns_backing = request.kind.requires_backing_index();
1166        let request_for_event = request.clone();
1167        let outcome = {
1168            let mut catalog = self.constraint_catalog_write();
1169            catalog.try_create(request, if_not_exists)?
1170        };
1171
1172        if let CreateConstraintOutcome::Created(def) = &outcome {
1173            // Pre-create data scan: if the live graph already violates
1174            // the constraint, fail and roll back the catalog write.
1175            // Creating constraints when conflicting data exists fails
1176            // before the catalog change is retained (22N77/79/80).
1177            if let Err(violation) = self.validate_existing_data_for_constraint(def) {
1178                let mut catalog = self.constraint_catalog_write();
1179                let _ = catalog.try_drop(&def.name, true);
1180                return Err(CreateConstraintError::DataViolation(violation.to_string()));
1181            }
1182        }
1183
1184        if let CreateConstraintOutcome::Created(def) = &outcome {
1185            if owns_backing {
1186                // Register a backing RANGE index under the same name. This
1187                // is an implementation detail of the constraint, so WAL and
1188                // snapshot replay record only the constraint mutation.
1189                // A hash-backed constraint keeps the hash index only.
1190                let mut options = BTreeMap::new();
1191                if def.backing == ConstraintBacking::Hash {
1192                    options.insert(SORTED_OPTION.to_string(), IndexConfigValue::Bool(false));
1193                }
1194                let idx_request = IndexRequest {
1195                    explicit_name: Some(def.name.clone()),
1196                    kind: StoredIndexKind::Range,
1197                    entity: def.entity,
1198                    label: Some(def.label.clone()),
1199                    additional_labels: Vec::new(),
1200                    properties: def.properties.clone(),
1201                    options,
1202                };
1203                // Errors from the backing-index registration unwind the
1204                // constraint registration to keep the two catalogs in
1205                // step.
1206                if let Err(err) = self.register_index_with_recording(idx_request, true, false) {
1207                    let mut catalog = self.constraint_catalog_write();
1208                    let _ = catalog.try_drop(&def.name, true);
1209                    return Err(CreateConstraintError::BackingIndexConflict(err.to_string()));
1210                }
1211            }
1212            self.emit(|| crate::MutationEvent::CreateConstraint {
1213                request: request_for_event,
1214                if_not_exists,
1215            });
1216            self.active_constraints.fetch_add(1, Ordering::Relaxed);
1217        }
1218
1219        Ok(outcome)
1220    }
1221
1222    /// Replay a CreateConstraint event against a recorder-detached graph.
1223    #[doc(hidden)]
1224    pub fn replay_create_constraint(
1225        &mut self,
1226        request: ConstraintRequest,
1227        if_not_exists: bool,
1228    ) -> Result<(), String> {
1229        if self.recorder.is_some() {
1230            return Err(
1231                "cannot replay create_constraint while a mutation recorder is installed".into(),
1232            );
1233        }
1234        self.register_constraint(request, if_not_exists)
1235            .map(|_| ())
1236            .map_err(|e| e.to_string())
1237    }
1238
1239    /// Replay a DropConstraint event.
1240    #[doc(hidden)]
1241    pub fn replay_drop_constraint(&mut self, name: &str, if_exists: bool) -> Result<(), String> {
1242        if self.recorder.is_some() {
1243            return Err(
1244                "cannot replay drop_constraint while a mutation recorder is installed".into(),
1245            );
1246        }
1247        self.drop_named_constraint(name, if_exists)
1248            .map(|_| ())
1249            .map_err(|e| e.to_string())
1250    }
1251
1252    /// Inverse of [`Self::register_constraint`]. Cascades to the backing
1253    /// range index when one is owned.
1254    pub(super) fn drop_named_constraint(
1255        &self,
1256        name: &str,
1257        if_exists: bool,
1258    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1259        let outcome = {
1260            let mut catalog = self.constraint_catalog_write();
1261            catalog.try_drop(name, if_exists)?
1262        };
1263        if let DropConstraintOutcome::Dropped(def) = &outcome {
1264            if let Some(index_name) = def.owned_index.as_deref() {
1265                // The backing index is owned exclusively by the
1266                // constraint, so dropping it is unconditional.
1267                let _ = self.drop_named_index_inner(index_name, true, false);
1268            }
1269            self.active_constraints.fetch_sub(1, Ordering::Relaxed);
1270            self.emit(|| crate::MutationEvent::DropConstraint {
1271                name: name.to_string(),
1272                if_exists,
1273            });
1274        }
1275        Ok(outcome)
1276    }
1277
1278    /// Inverse of [`Self::register_index`]. Removes the catalog entry
1279    /// and (for RANGE) releases the hash property index of each property
1280    /// no other RANGE index still declares; a later equality lookup
1281    /// activates it again.
1282    pub(super) fn drop_named_index(
1283        &self,
1284        name: &str,
1285        if_exists: bool,
1286    ) -> Result<DropIndexOutcome, DropIndexError> {
1287        self.drop_named_index_inner(name, if_exists, true)
1288    }
1289
1290    fn drop_named_index_inner(
1291        &self,
1292        name: &str,
1293        if_exists: bool,
1294        emit_event: bool,
1295    ) -> Result<DropIndexOutcome, DropIndexError> {
1296        if let Some(owner) = self
1297            .constraint_catalog_read()
1298            .constraint_owning_index(name)
1299            .cloned()
1300        {
1301            return Err(DropIndexError::ConstraintOwned {
1302                index: name.to_string(),
1303                constraint: owner.name,
1304            });
1305        }
1306
1307        let outcome = {
1308            let mut catalog = self.index_catalog_write();
1309            catalog.try_drop(name, if_exists)?
1310        };
1311        if let DropIndexOutcome::Dropped(def) = &outcome {
1312            // Release backing structures keyed off the dropped def.
1313            match def.kind {
1314                StoredIndexKind::Text => {
1315                    if let Some(label) = def.label.as_deref() {
1316                        for prop in &def.properties {
1317                            self.deactivate_text_scope(def.entity, label, prop);
1318                        }
1319                    }
1320                }
1321                StoredIndexKind::Range => {
1322                    if let Some(label) = def.label.as_deref().filter(|_| is_sorted(def)) {
1323                        for prop in &def.properties {
1324                            self.deactivate_sorted_scope(def.entity, label, prop);
1325                        }
1326                    }
1327                    // Writes stop paying for a hash index nothing declares
1328                    // any more; an equality lookup rebuilds it on demand.
1329                    let declared = self.declared_property_index_keys();
1330                    let mut indexes = self.indexes_write();
1331                    for prop in &def.properties {
1332                        if declared.contains(&(def.entity, prop.clone())) {
1333                            continue;
1334                        }
1335                        match def.entity {
1336                            StoredIndexEntity::Node => indexes.node_properties.deactivate(prop),
1337                            StoredIndexEntity::Relationship => {
1338                                indexes.relationship_properties.deactivate(prop)
1339                            }
1340                        };
1341                    }
1342                }
1343                StoredIndexKind::Point => {
1344                    if let Some(label) = def.label.as_deref() {
1345                        for prop in &def.properties {
1346                            self.deactivate_point_scope(def.entity, label, prop);
1347                        }
1348                    }
1349                }
1350                StoredIndexKind::Lookup => {
1351                    // Lookup rides on the eagerly-maintained label/type
1352                    // indexes; nothing to release.
1353                }
1354                StoredIndexKind::Vector => {
1355                    self.deactivate_vector_index(def.entity, &def.name);
1356                }
1357                StoredIndexKind::Fulltext => {
1358                    self.deactivate_fulltext_index(def.entity, &def.name);
1359                }
1360            }
1361            if emit_event {
1362                self.emit(|| crate::MutationEvent::DropIndex {
1363                    name: name.to_string(),
1364                    if_exists,
1365                });
1366            }
1367        }
1368        Ok(outcome)
1369    }
1370
1371    /// `(entity, key)` pairs whose hash property index a declaration keeps
1372    /// active: every property of a RANGE index in the catalog, which
1373    /// includes the backing indexes of uniqueness / key constraints. These
1374    /// are the only hash indexes a restart rebuilds; any other active key
1375    /// was activated implicitly by an equality lookup.
1376    pub(super) fn declared_property_index_keys(
1377        &self,
1378    ) -> std::collections::BTreeSet<(StoredIndexEntity, String)> {
1379        self.index_catalog_read()
1380            .list()
1381            .into_iter()
1382            .filter(|def| def.kind == StoredIndexKind::Range)
1383            .flat_map(|def| {
1384                let entity = def.entity;
1385                def.properties.into_iter().map(move |p| (entity, p))
1386            })
1387            .collect()
1388    }
1389
1390    fn populate_index_data(&self, def: &IndexDefinition) {
1391        // RANGE: piggy-back on the existing lazy property-index buckets.
1392        // TEXT: build a trigram inverted index over the existing entity
1393        //       data for the (label, property) tuple.
1394        // POINT: build a grid-bucket spatial index over the existing
1395        //        entity data.
1396        // LOOKUP: catalog-only; existing label/type indexes already
1397        //         answer the predicates.
1398        match def.kind {
1399            StoredIndexKind::Range => {
1400                for key in &def.properties {
1401                    match def.entity {
1402                        StoredIndexEntity::Node => self.ensure_node_property_index(key),
1403                        StoredIndexEntity::Relationship => {
1404                            self.ensure_relationship_property_index(key)
1405                        }
1406                    }
1407                    if let Some(label) = def.label.as_deref().filter(|_| is_sorted(def)) {
1408                        self.activate_sorted_scope(def.entity, label, key);
1409                    }
1410                }
1411            }
1412            StoredIndexKind::Text => {
1413                let label = match def.label.as_deref() {
1414                    Some(l) => l,
1415                    None => return,
1416                };
1417                for property in &def.properties {
1418                    self.activate_text_scope(def.entity, label, property);
1419                }
1420            }
1421            StoredIndexKind::Point => {
1422                let label = match def.label.as_deref() {
1423                    Some(l) => l,
1424                    None => return,
1425                };
1426                let cell_size = PointRegistry::cell_size_from_options(&def.options);
1427                for property in &def.properties {
1428                    self.activate_point_scope(def.entity, label, property, cell_size);
1429                }
1430            }
1431            StoredIndexKind::Fulltext => {
1432                let labels: Vec<String> = def.all_labels().map(String::from).collect();
1433                if labels.is_empty() {
1434                    return;
1435                }
1436                self.activate_fulltext_index(def.entity, &def.name, &labels, &def.properties);
1437            }
1438            StoredIndexKind::Vector => {
1439                let label = match def.label.as_deref() {
1440                    Some(l) => l,
1441                    None => return,
1442                };
1443                let property = match def.properties.first() {
1444                    Some(p) => p.as_str(),
1445                    None => return,
1446                };
1447                let similarity = VectorSimilarity::from_options(&def.options)
1448                    .unwrap_or(VectorSimilarity::Cosine);
1449                let provider = VectorIndexProvider::from_options(&def.options)
1450                    .unwrap_or(VectorIndexProvider::Flat);
1451                let hnsw = HnswParams::from_options(&def.options);
1452                let lazy = matches!(
1453                    def.options.get("vector.populate.async"),
1454                    Some(super::index_catalog::IndexConfigValue::Bool(true))
1455                );
1456                self.activate_vector_index(
1457                    def.entity, &def.name, label, property, similarity, provider, hnsw, lazy,
1458                );
1459                if lazy {
1460                    self.index_catalog_write()
1461                        .set_state(&def.name, StoredIndexState::Populating);
1462                }
1463            }
1464            // LOOKUP rides on the label/type indexes maintained eagerly.
1465            StoredIndexKind::Lookup => {}
1466        }
1467    }
1468
1469    pub(super) fn text_indexes_read(
1470        &self,
1471        entity: StoredIndexEntity,
1472    ) -> IndexRead<'_, TrigramRegistry> {
1473        self.indexes.text.read(entity)
1474    }
1475
1476    pub(super) fn text_indexes_write(
1477        &self,
1478        entity: StoredIndexEntity,
1479    ) -> IndexWrite<'_, TrigramRegistry> {
1480        self.indexes.text.write(entity)
1481    }
1482
1483    pub(super) fn fulltext_indexes_read(
1484        &self,
1485        entity: StoredIndexEntity,
1486    ) -> IndexRead<'_, FulltextRegistry> {
1487        self.indexes.fulltext.read(entity)
1488    }
1489
1490    #[allow(dead_code)]
1491    pub(super) fn fulltext_indexes_write(
1492        &self,
1493        entity: StoredIndexEntity,
1494    ) -> IndexWrite<'_, FulltextRegistry> {
1495        self.indexes.fulltext.write(entity)
1496    }
1497
1498    fn activate_text_scope(&self, entity: StoredIndexEntity, label: &str, property: &str) {
1499        if !self.text_indexes_write(entity).add_scope(label, property) {
1500            return;
1501        }
1502
1503        let backfill: Vec<(u64, String)> = match entity {
1504            StoredIndexEntity::Node => self
1505                .iter_nodes()
1506                .filter(|(_, node)| node.labels().strs().any(|l| l == label))
1507                .filter_map(|(id, node)| match node.properties().get(property) {
1508                    Some(crate::ValueRef::String(value)) => Some((id, value.to_owned())),
1509                    _ => None,
1510                })
1511                .collect(),
1512            StoredIndexEntity::Relationship => self
1513                .iter_rels()
1514                .filter(|(_, rel)| rel.rel_type() == label)
1515                .filter_map(|(id, rel)| match rel.properties().get(property) {
1516                    Some(crate::ValueRef::String(value)) => Some((id, value.to_owned())),
1517                    _ => None,
1518                })
1519                .collect(),
1520        };
1521
1522        let mut registry = self.text_indexes_write(entity);
1523        for (id, value) in backfill {
1524            registry.insert(label, property, id, &value);
1525        }
1526    }
1527
1528    /// Drop a (label, property) text scope, decrementing the refcount.
1529    pub(super) fn deactivate_text_scope(
1530        &self,
1531        entity: StoredIndexEntity,
1532        label: &str,
1533        property: &str,
1534    ) {
1535        self.text_indexes_write(entity)
1536            .remove_scope(label, property);
1537    }
1538
1539    fn activate_fulltext_index(
1540        &self,
1541        entity: StoredIndexEntity,
1542        name: &str,
1543        labels: &[String],
1544        properties: &[String],
1545    ) {
1546        use super::fulltext_index::{term_counts_for_properties, TermCounts};
1547
1548        {
1549            let mut registry = self.fulltext_indexes_write(entity);
1550            registry.register(name.to_string(), labels.to_vec(), properties.to_vec());
1551        }
1552        self.indexes
1553            .active_fulltext_indexes
1554            .fetch_add(1, Ordering::Relaxed);
1555
1556        // Backfill: walk every entity matching any label, tokenise covered
1557        // string properties, install one posting batch per entity.
1558        let backfill: Vec<(u64, TermCounts)> = match entity {
1559            StoredIndexEntity::Node => self
1560                .iter_nodes()
1561                .filter(|(_, node)| {
1562                    labels
1563                        .iter()
1564                        .any(|wanted| node.labels().strs().any(|l| l == wanted))
1565                })
1566                .map(|(id, node)| {
1567                    let counts = term_counts_for_properties(node.properties(), properties);
1568                    (id, counts)
1569                })
1570                .filter(|(_, c)| !c.is_empty())
1571                .collect(),
1572            StoredIndexEntity::Relationship => self
1573                .iter_rels()
1574                .filter(|(_, rel)| labels.iter().any(|wanted| wanted == rel.rel_type()))
1575                .map(|(id, rel)| {
1576                    let counts = term_counts_for_properties(rel.properties(), properties);
1577                    (id, counts)
1578                })
1579                .filter(|(_, c)| !c.is_empty())
1580                .collect(),
1581        };
1582
1583        let mut registry = self.fulltext_indexes_write(entity);
1584        if let Some(index) = registry.get_mut(name) {
1585            for (id, counts) in backfill {
1586                index.reindex_entity(id, counts);
1587            }
1588        }
1589    }
1590
1591    pub(super) fn deactivate_fulltext_index(&self, entity: StoredIndexEntity, name: &str) {
1592        self.fulltext_indexes_write(entity).deregister(name);
1593        self.indexes
1594            .active_fulltext_indexes
1595            .fetch_sub(1, Ordering::Relaxed);
1596    }
1597
1598    pub(super) fn sorted_indexes_read(
1599        &self,
1600        entity: StoredIndexEntity,
1601    ) -> IndexRead<'_, SortedPropertyIndex> {
1602        self.indexes.sorted.read(entity)
1603    }
1604
1605    pub(super) fn sorted_indexes_write(
1606        &self,
1607        entity: StoredIndexEntity,
1608    ) -> IndexWrite<'_, SortedPropertyIndex> {
1609        self.indexes.sorted.write(entity)
1610    }
1611
1612    fn activate_sorted_scope(&self, entity: StoredIndexEntity, label: &str, property: &str) {
1613        if !self.sorted_indexes_write(entity).add_scope(label, property) {
1614            return;
1615        }
1616
1617        // Inserted while scanning: collecting every value first held a
1618        // second copy of the whole column until the index was built.
1619        let mut registry = self.sorted_indexes_write(entity);
1620        match entity {
1621            StoredIndexEntity::Node => {
1622                for (id, node) in self.iter_nodes() {
1623                    if !node.labels().strs().any(|l| l == label) {
1624                        continue;
1625                    }
1626                    match node.properties().get(property) {
1627                        // A list is indexed by its elements too.
1628                        Some(crate::ValueRef::Other(value)) => {
1629                            registry.insert(label, property, id, &value.get())
1630                        }
1631                        Some(value) => {
1632                            if let Some(key) = PropertyIndexKey::from_value_ref(value) {
1633                                registry.insert_key(label, property, id, key);
1634                            }
1635                        }
1636                        None => {}
1637                    }
1638                }
1639            }
1640            StoredIndexEntity::Relationship => {
1641                for (id, rel) in self.iter_rels() {
1642                    if rel.rel_type() != label {
1643                        continue;
1644                    }
1645                    match rel.properties().get(property) {
1646                        // A list is indexed by its elements too.
1647                        Some(crate::ValueRef::Other(value)) => {
1648                            registry.insert(label, property, id, &value.get())
1649                        }
1650                        Some(value) => {
1651                            if let Some(key) = PropertyIndexKey::from_value_ref(value) {
1652                                registry.insert_key(label, property, id, key);
1653                            }
1654                        }
1655                        None => {}
1656                    }
1657                }
1658            }
1659        }
1660    }
1661
1662    pub(super) fn deactivate_sorted_scope(
1663        &self,
1664        entity: StoredIndexEntity,
1665        label: &str,
1666        property: &str,
1667    ) {
1668        self.sorted_indexes_write(entity)
1669            .remove_scope(label, property);
1670    }
1671
1672    pub(super) fn point_indexes_read(
1673        &self,
1674        entity: StoredIndexEntity,
1675    ) -> IndexRead<'_, PointRegistry> {
1676        self.indexes.point.read(entity)
1677    }
1678
1679    pub(super) fn point_indexes_write(
1680        &self,
1681        entity: StoredIndexEntity,
1682    ) -> IndexWrite<'_, PointRegistry> {
1683        self.indexes.point.write(entity)
1684    }
1685
1686    fn activate_point_scope(
1687        &self,
1688        entity: StoredIndexEntity,
1689        label: &str,
1690        property: &str,
1691        cell_size: Option<f64>,
1692    ) {
1693        if !self
1694            .point_indexes_write(entity)
1695            .add_scope(label, property, cell_size)
1696        {
1697            return;
1698        }
1699
1700        let backfill: Vec<(u64, LoraPoint)> = match entity {
1701            StoredIndexEntity::Node => self
1702                .iter_nodes()
1703                .filter(|(_, node)| node.labels().strs().any(|l| l == label))
1704                .filter_map(|(id, node)| {
1705                    match node.properties().get(property).map(|v| v.to_owned()) {
1706                        Some(PropertyValue::Point(point)) => Some((id, point)),
1707                        _ => None,
1708                    }
1709                })
1710                .collect(),
1711            StoredIndexEntity::Relationship => self
1712                .iter_rels()
1713                .filter(|(_, rel)| rel.rel_type() == label)
1714                .filter_map(|(id, rel)| {
1715                    match rel.properties().get(property).map(|v| v.to_owned()) {
1716                        Some(PropertyValue::Point(point)) => Some((id, point)),
1717                        _ => None,
1718                    }
1719                })
1720                .collect(),
1721        };
1722
1723        let mut registry = self.point_indexes_write(entity);
1724        for (id, point) in backfill {
1725            registry.insert(label, property, id, point);
1726        }
1727    }
1728
1729    pub(super) fn deactivate_point_scope(
1730        &self,
1731        entity: StoredIndexEntity,
1732        label: &str,
1733        property: &str,
1734    ) {
1735        self.point_indexes_write(entity)
1736            .remove_scope(label, property);
1737    }
1738
1739    pub(super) fn vector_indexes_read(
1740        &self,
1741        entity: StoredIndexEntity,
1742    ) -> IndexRead<'_, VectorIndexRegistry> {
1743        self.indexes.vector.read(entity)
1744    }
1745
1746    pub(super) fn vector_indexes_write(
1747        &self,
1748        entity: StoredIndexEntity,
1749    ) -> IndexWrite<'_, VectorIndexRegistry> {
1750        self.indexes.vector.write(entity)
1751    }
1752
1753    #[allow(clippy::too_many_arguments)]
1754    fn activate_vector_index(
1755        &self,
1756        entity: StoredIndexEntity,
1757        name: &str,
1758        label: &str,
1759        property: &str,
1760        similarity: VectorSimilarity,
1761        provider: VectorIndexProvider,
1762        hnsw: HnswParams,
1763        lazy: bool,
1764    ) {
1765        {
1766            let mut registry = self.vector_indexes_write(entity);
1767            registry.register(
1768                name.to_string(),
1769                label.to_string(),
1770                property.to_string(),
1771                similarity,
1772                provider,
1773                hnsw,
1774            );
1775        }
1776
1777        if lazy {
1778            // Skip the initial backfill — the catalog state is flipped
1779            // to Populating by the caller and the first query routed
1780            // to this index triggers `lazy_populate_vector_index`.
1781            // Mutations between CREATE and first query still feed the
1782            // registry via the maintenance hook, so the lazy phase
1783            // only handles vectors that existed before CREATE.
1784            return;
1785        }
1786
1787        self.backfill_vector_index(entity, label, property);
1788    }
1789
1790    /// Walk the property store for vectors matching this index's
1791    /// `(label, property)` scope and replay them into the registry.
1792    /// Shared by sync CREATE and lazy-populate flows.
1793    fn backfill_vector_index(&self, entity: StoredIndexEntity, label: &str, property: &str) {
1794        let backfill: Vec<(u64, crate::LoraVector)> = match entity {
1795            StoredIndexEntity::Node => self
1796                .iter_nodes()
1797                .filter(|(_, node)| node.labels().strs().any(|l| l == label))
1798                .filter_map(|(id, node)| {
1799                    match node.properties().get(property).map(|v| v.to_owned()) {
1800                        Some(PropertyValue::Vector(v)) => Some((id, v)),
1801                        _ => None,
1802                    }
1803                })
1804                .collect(),
1805            StoredIndexEntity::Relationship => self
1806                .iter_rels()
1807                .filter(|(_, rel)| rel.rel_type() == label)
1808                .filter_map(|(id, rel)| {
1809                    match rel.properties().get(property).map(|v| v.to_owned()) {
1810                        Some(PropertyValue::Vector(v)) => Some((id, v)),
1811                        _ => None,
1812                    }
1813                })
1814                .collect(),
1815        };
1816
1817        let mut registry = self.vector_indexes_write(entity);
1818        for (id, vector) in backfill {
1819            registry.insert_for(label, property, id, &vector);
1820        }
1821    }
1822
1823    /// Lazy-populate a `Populating` vector index: backfill from the
1824    /// property store, then flip catalog state to `Online`. Called
1825    /// from `GraphStorage::vector_search` on the first request that
1826    /// hits a still-populating index. Idempotent: a second concurrent
1827    /// caller finds the state already `Online` and does no work.
1828    pub(super) fn lazy_populate_vector_index(&self, name: &str) {
1829        let def = match self.index_catalog_read().get(name).cloned() {
1830            Some(d) => d,
1831            None => return,
1832        };
1833        if def.state != StoredIndexState::Populating || def.kind != StoredIndexKind::Vector {
1834            return;
1835        }
1836        let label = match def.label.as_deref() {
1837            Some(l) => l,
1838            None => return,
1839        };
1840        let property = match def.properties.first() {
1841            Some(p) => p.as_str(),
1842            None => return,
1843        };
1844        self.backfill_vector_index(def.entity, label, property);
1845        self.index_catalog_write()
1846            .set_state(name, StoredIndexState::Online);
1847    }
1848
1849    pub(super) fn deactivate_vector_index(&self, entity: StoredIndexEntity, name: &str) {
1850        self.vector_indexes_write(entity).deregister(name);
1851    }
1852
1853    /// Snapshot of cardinality stats. Cheap: derived from already-tracked
1854    /// `nodes_by_label` / `relationships_by_type` lengths and the
1855    /// distinct-value sketches (a cached estimate per (scope, key), and
1856    /// the previous map reused while those don't change). The cost model
1857    /// uses this to populate `estimated_rows` on plan-tree nodes.
1858    pub fn graph_stats(&self) -> GraphStats {
1859        let mut stats = GraphStats {
1860            node_count: self.live_node_count,
1861            relationship_count: self.live_rel_count,
1862            ..Default::default()
1863        };
1864        // Collected from ascending keys, the maps are built in one pass
1865        // instead of searched and grown per insert.
1866        stats.nodes_by_label = self
1867            .nodes_by_label
1868            .iter()
1869            .map(|(label, ids)| (label.to_string(), ids.len()))
1870            .collect();
1871        stats.relationships_by_type = self
1872            .relationships_by_type
1873            .iter()
1874            .map(|(rel_type, ids)| (rel_type.to_string(), ids.len()))
1875            .collect();
1876        // Distinct values per (label / type, property): estimated from
1877        // the distinct-value sketches, which exist for every key and are
1878        // a function of the live data alone. Deliberately *not* the
1879        // exact bucket counts of active hash indexes: which keys are
1880        // active depends on lookup history and on whether the process
1881        // restarted, and plans must not.
1882        stats.node_distinct_values = self.distinct_stats.nodes.distinct_values();
1883        stats.relationship_distinct_values = self.distinct_stats.relationships.distinct_values();
1884
1885        for def in self.index_catalog_read().list() {
1886            if def.state != StoredIndexState::Online {
1887                continue;
1888            }
1889            // A hash-backed constraint's index answers equality only, which
1890            // the planner assumes of every key.
1891            if def.kind == StoredIndexKind::Range && !is_sorted(&def) {
1892                continue;
1893            }
1894            let Some(label) = def.label else {
1895                continue;
1896            };
1897            for property in def.properties {
1898                let scope = (label.clone(), property);
1899                match (def.entity, def.kind) {
1900                    (StoredIndexEntity::Node, StoredIndexKind::Range) => {
1901                        stats.node_range_indexes.insert(scope);
1902                    }
1903                    (StoredIndexEntity::Node, StoredIndexKind::Text) => {
1904                        stats.node_text_indexes.insert(scope);
1905                    }
1906                    (StoredIndexEntity::Node, StoredIndexKind::Point) => {
1907                        stats.node_point_indexes.insert(scope);
1908                    }
1909                    (StoredIndexEntity::Relationship, StoredIndexKind::Range) => {
1910                        stats.relationship_range_indexes.insert(scope);
1911                    }
1912                    (StoredIndexEntity::Relationship, StoredIndexKind::Text) => {
1913                        stats.relationship_text_indexes.insert(scope);
1914                    }
1915                    (StoredIndexEntity::Relationship, StoredIndexKind::Point) => {
1916                        stats.relationship_point_indexes.insert(scope);
1917                    }
1918                    (StoredIndexEntity::Node, StoredIndexKind::Vector) => {
1919                        stats.node_vector_indexes.insert(scope);
1920                    }
1921                    (StoredIndexEntity::Relationship, StoredIndexKind::Vector) => {
1922                        stats.relationship_vector_indexes.insert(scope);
1923                    }
1924                    (_, StoredIndexKind::Lookup | StoredIndexKind::Fulltext) => {}
1925                }
1926            }
1927        }
1928        for (entity, elements) in [
1929            (StoredIndexEntity::Node, &mut stats.node_list_elements),
1930            (
1931                StoredIndexEntity::Relationship,
1932                &mut stats.relationship_list_elements,
1933            ),
1934        ] {
1935            let sorted = self.sorted_indexes_read(entity);
1936            for (label, property, distinct, postings) in sorted.element_stats() {
1937                elements.insert(
1938                    (label.to_string(), property.to_string()),
1939                    super::ListElementStats { distinct, postings },
1940                );
1941            }
1942        }
1943        stats
1944    }
1945
1946    /// [`GraphStats::plan_signature`] of [`Self::graph_stats`], computed
1947    /// without building the stats: one pass over the label / type maps,
1948    /// the distinct-value estimates and the index catalog. Plan caches
1949    /// call it after every write, so it must stay allocation-free.
1950    pub fn plan_signature(&self) -> u64 {
1951        let mut sig = super::PlanSignature::new();
1952        sig.totals(self.live_node_count, self.live_rel_count);
1953        for (label, ids) in &self.nodes_by_label {
1954            sig.label(label, ids.len());
1955        }
1956        for (rel_type, ids) in &self.relationships_by_type {
1957            sig.relationship_type(rel_type, ids.len());
1958        }
1959        for (label, property, distinct) in self.distinct_stats.nodes.estimates() {
1960            sig.node_distinct(label, property, distinct);
1961        }
1962        for (rel_type, property, distinct) in self.distinct_stats.relationships.estimates() {
1963            sig.relationship_distinct(rel_type, property, distinct);
1964        }
1965        for def in self.index_catalog_read().iter() {
1966            if def.state != StoredIndexState::Online
1967                || matches!(
1968                    def.kind,
1969                    StoredIndexKind::Lookup | StoredIndexKind::Fulltext
1970                )
1971            {
1972                continue;
1973            }
1974            let Some(label) = &def.label else {
1975                continue;
1976            };
1977            for property in &def.properties {
1978                sig.index(def.entity, def.kind, label, property);
1979            }
1980        }
1981        for entity in [StoredIndexEntity::Node, StoredIndexEntity::Relationship] {
1982            let sorted = self.sorted_indexes_read(entity);
1983            for (label, property, distinct, postings) in sorted.element_stats() {
1984                sig.list_elements(
1985                    entity,
1986                    label,
1987                    property,
1988                    super::ListElementStats { distinct, postings },
1989                );
1990            }
1991        }
1992        sig.finish()
1993    }
1994
1995    /// Approximate retained-heap breakdown of this graph. See
1996    /// [`super::MemoryReport`] for the methodology and per-component
1997    /// fields. Intended for benches and the `mem_probe*` examples;
1998    /// not on a hot path.
1999    pub fn memory_estimate(&self) -> super::MemoryReport {
2000        super::mem_report::estimate(self)
2001    }
2002
2003    /// Count (or uncount) every property of an entity under each of its
2004    /// scopes in the distinct-value sketches.
2005    pub(super) fn count_distinct_values<'a>(
2006        stats: &mut super::distinct_stats::DistinctStats,
2007        scopes: impl IntoIterator<Item = &'a str>,
2008        properties: &Properties,
2009        add: bool,
2010    ) {
2011        if properties.is_empty() {
2012            return;
2013        }
2014        for scope in scopes {
2015            if add {
2016                stats.add_all(scope, properties);
2017            } else {
2018                for (key, value) in properties {
2019                    stats.remove(scope, key, value);
2020                }
2021            }
2022        }
2023    }
2024
2025    pub(super) fn on_node_created(&mut self, node: &NodeRecord) {
2026        for label in &node.labels {
2027            self.insert_node_label_index(node.id, label);
2028        }
2029        Self::count_distinct_values(
2030            &mut self.distinct_stats.nodes,
2031            node.labels.strs(),
2032            &node.properties,
2033            true,
2034        );
2035        self.index_node_properties_if_active(node.id, node.labels.strs(), &node.properties);
2036        self.maintain_node_secondary_indexes(node, SecondaryIndexMutation::Insert);
2037    }
2038
2039    pub(super) fn on_node_property_set(
2040        &mut self,
2041        node_id: NodeId,
2042        key: &str,
2043        old: Option<&PropertyValue>,
2044        new: &PropertyValue,
2045    ) {
2046        let Some(labels) = self.node_at(node_id).map(|node| node.labels().to_owned()) else {
2047            return;
2048        };
2049        for label in &labels {
2050            self.distinct_stats.nodes.replace(label, key, old, new);
2051        }
2052
2053        if self.node_property_index_is_active(key) {
2054            if let Some(old) = old {
2055                self.unindex_node_property_if_active(node_id, labels.strs(), key, old);
2056            }
2057            self.index_node_property_if_active(node_id, labels.strs(), key, new);
2058        }
2059
2060        self.update_secondary_property(
2061            StoredIndexEntity::Node,
2062            labels.strs(),
2063            node_id,
2064            key,
2065            old,
2066            Some(new),
2067        );
2068    }
2069
2070    pub(super) fn on_node_property_removed(
2071        &mut self,
2072        node_id: NodeId,
2073        key: &str,
2074        old: &PropertyValue,
2075    ) {
2076        let Some(labels) = self.node_at(node_id).map(|node| node.labels().to_owned()) else {
2077            return;
2078        };
2079        for label in &labels {
2080            self.distinct_stats.nodes.remove(label, key, old);
2081        }
2082        if self.node_property_index_is_active(key) {
2083            self.unindex_node_property_if_active(node_id, labels.strs(), key, old);
2084        }
2085        self.update_secondary_property(
2086            StoredIndexEntity::Node,
2087            labels.strs(),
2088            node_id,
2089            key,
2090            Some(old),
2091            None,
2092        );
2093    }
2094
2095    pub(super) fn on_node_label_added(&mut self, node_id: NodeId, label: &str) {
2096        self.insert_node_label_index(node_id, label);
2097
2098        let Some(properties) = self
2099            .node_at(node_id)
2100            .map(|node| node.properties().to_owned())
2101        else {
2102            return;
2103        };
2104        Self::count_distinct_values(&mut self.distinct_stats.nodes, [label], &properties, true);
2105        if self.active_node_property_index_count() != 0 {
2106            self.index_node_scope_properties_if_active(node_id, label, &properties);
2107            // Its first label: it was indexed as a node without any.
2108            if self.node_at(node_id).is_some_and(|n| n.labels().len() == 1) {
2109                self.unindex_node_scope_properties_if_active(node_id, UNLABELLED, &properties);
2110            }
2111        }
2112        for (key, value) in &properties {
2113            self.update_secondary_property(
2114                StoredIndexEntity::Node,
2115                [label],
2116                node_id,
2117                key,
2118                None,
2119                Some(value),
2120            );
2121        }
2122    }
2123
2124    pub(super) fn on_node_label_removed(&mut self, node_id: NodeId, label: &str) {
2125        self.remove_node_label_index(node_id, label);
2126
2127        let Some(properties) = self
2128            .node_at(node_id)
2129            .map(|node| node.properties().to_owned())
2130        else {
2131            return;
2132        };
2133        Self::count_distinct_values(&mut self.distinct_stats.nodes, [label], &properties, false);
2134        if self.active_node_property_index_count() != 0 {
2135            self.unindex_node_scope_properties_if_active(node_id, label, &properties);
2136            // Its last label: index it as a node without any.
2137            if self.node_at(node_id).is_some_and(|n| n.labels().is_empty()) {
2138                self.index_node_scope_properties_if_active(node_id, UNLABELLED, &properties);
2139            }
2140        }
2141        for (key, value) in &properties {
2142            self.update_secondary_property(
2143                StoredIndexEntity::Node,
2144                [label],
2145                node_id,
2146                key,
2147                Some(value),
2148                None,
2149            );
2150        }
2151    }
2152
2153    pub(super) fn on_node_deleted(&mut self, node: &NodeRecord) {
2154        for label in &node.labels {
2155            self.remove_node_label_index(node.id, label);
2156        }
2157        Self::count_distinct_values(
2158            &mut self.distinct_stats.nodes,
2159            node.labels.strs(),
2160            &node.properties,
2161            false,
2162        );
2163        self.unindex_active_node_properties(node.id, node.labels.strs(), &node.properties);
2164        self.maintain_node_secondary_indexes(node, SecondaryIndexMutation::Remove);
2165    }
2166
2167    pub(super) fn on_relationship_created(&mut self, rel: &RelationshipRecord) {
2168        self.attach_relationship(rel);
2169        Self::count_distinct_values(
2170            &mut self.distinct_stats.relationships,
2171            [rel.rel_type.as_str()],
2172            &rel.properties,
2173            true,
2174        );
2175        self.index_relationship_properties_if_active(
2176            rel.id,
2177            [rel.rel_type.as_str()],
2178            &rel.properties,
2179        );
2180        self.maintain_relationship_secondary_indexes(rel, SecondaryIndexMutation::Insert);
2181    }
2182
2183    pub(super) fn on_relationship_property_set(
2184        &mut self,
2185        rel_id: RelationshipId,
2186        key: &str,
2187        old: Option<&PropertyValue>,
2188        new: &PropertyValue,
2189    ) {
2190        let Some(rel_type) = self.rel_at(rel_id).map(|rel| rel.type_name().clone()) else {
2191            return;
2192        };
2193        self.distinct_stats
2194            .relationships
2195            .replace(&rel_type, key, old, new);
2196
2197        if self.relationship_property_index_is_active(key) {
2198            if let Some(old) = old {
2199                self.unindex_relationship_property_if_active(rel_id, [rel_type.as_str()], key, old);
2200            }
2201            self.index_relationship_property_if_active(rel_id, [rel_type.as_str()], key, new);
2202        }
2203
2204        self.update_secondary_property(
2205            StoredIndexEntity::Relationship,
2206            [rel_type.as_str()],
2207            rel_id,
2208            key,
2209            old,
2210            Some(new),
2211        );
2212    }
2213
2214    pub(super) fn on_relationship_property_removed(
2215        &mut self,
2216        rel_id: RelationshipId,
2217        key: &str,
2218        old: &PropertyValue,
2219    ) {
2220        let Some(rel_type) = self.rel_at(rel_id).map(|rel| rel.type_name().clone()) else {
2221            return;
2222        };
2223        self.distinct_stats
2224            .relationships
2225            .remove(&rel_type, key, old);
2226        if self.relationship_property_index_is_active(key) {
2227            self.unindex_relationship_property_if_active(rel_id, [rel_type.as_str()], key, old);
2228        }
2229        self.update_secondary_property(
2230            StoredIndexEntity::Relationship,
2231            [rel_type.as_str()],
2232            rel_id,
2233            key,
2234            Some(old),
2235            None,
2236        );
2237    }
2238
2239    /// Index upkeep for a deleted relationship. `pending` defers its
2240    /// removal from long adjacency lists: see [`Self::adjacency_remove`].
2241    pub(super) fn on_relationship_deleted(
2242        &mut self,
2243        rel: &RelationshipRecord,
2244        pending: Option<&mut PendingAdjRemovals>,
2245    ) {
2246        self.detach_relationship_indexes(rel, pending);
2247        Self::count_distinct_values(
2248            &mut self.distinct_stats.relationships,
2249            [rel.rel_type.as_str()],
2250            &rel.properties,
2251            false,
2252        );
2253        self.unindex_active_relationship_properties(
2254            rel.id,
2255            [rel.rel_type.as_str()],
2256            &rel.properties,
2257        );
2258        self.maintain_relationship_secondary_indexes(rel, SecondaryIndexMutation::Remove);
2259    }
2260
2261    fn index_node_property_if_active<'a>(
2262        &mut self,
2263        node_id: NodeId,
2264        labels: impl IntoIterator<Item = &'a str>,
2265        key: &str,
2266        value: &PropertyValue,
2267    ) {
2268        if self.active_node_property_index_count() == 0 {
2269            return;
2270        }
2271        let (indexes, records) = self.node_index_parts();
2272        if indexes.node_properties.is_active(key) {
2273            indexes
2274                .node_properties
2275                .insert_with_scopes(node_id, labels, key, value, &records);
2276        }
2277    }
2278
2279    fn index_node_properties_if_active<'a>(
2280        &mut self,
2281        node_id: NodeId,
2282        labels: impl IntoIterator<Item = &'a str> + Clone,
2283        properties: &Properties,
2284    ) {
2285        if self.active_node_property_index_count() == 0 {
2286            return;
2287        }
2288        let (indexes, records) = self.node_index_parts();
2289        for (key, value) in properties {
2290            if indexes.node_properties.is_active(key) {
2291                indexes.node_properties.insert_with_scopes(
2292                    node_id,
2293                    labels.clone(),
2294                    key,
2295                    value,
2296                    &records,
2297                );
2298            }
2299        }
2300    }
2301
2302    fn unindex_node_property_if_active<'a>(
2303        &mut self,
2304        node_id: NodeId,
2305        labels: impl IntoIterator<Item = &'a str>,
2306        key: &str,
2307        value: &PropertyValue,
2308    ) {
2309        if self.active_node_property_index_count() == 0 {
2310            return;
2311        }
2312        let indexes = self.indexes_mut();
2313        if indexes.node_properties.is_active(key) {
2314            indexes
2315                .node_properties
2316                .remove_with_scopes(node_id, labels, key, value);
2317        }
2318    }
2319
2320    fn index_node_scope_properties_if_active(
2321        &mut self,
2322        node_id: NodeId,
2323        scope: &str,
2324        properties: &Properties,
2325    ) {
2326        if self.active_node_property_index_count() == 0 {
2327            return;
2328        }
2329        let (indexes, records) = self.node_index_parts();
2330        for (key, value) in properties {
2331            if indexes.node_properties.is_active(key) {
2332                indexes
2333                    .node_properties
2334                    .insert_scoped(node_id, scope, key, value, &records);
2335            }
2336        }
2337    }
2338
2339    fn unindex_node_scope_properties_if_active(
2340        &mut self,
2341        node_id: NodeId,
2342        scope: &str,
2343        properties: &Properties,
2344    ) {
2345        if self.active_node_property_index_count() == 0 {
2346            return;
2347        }
2348        let indexes = self.indexes_mut();
2349        for (key, value) in properties {
2350            if indexes.node_properties.is_active(key) {
2351                indexes
2352                    .node_properties
2353                    .remove_scoped(node_id, scope, key, value);
2354            }
2355        }
2356    }
2357
2358    fn unindex_active_node_properties<'a>(
2359        &mut self,
2360        node_id: NodeId,
2361        labels: impl IntoIterator<Item = &'a str> + Clone,
2362        properties: &Properties,
2363    ) {
2364        if self.active_node_property_index_count() == 0 {
2365            return;
2366        }
2367        let indexes = self.indexes_mut();
2368        for (key, value) in properties {
2369            if indexes.node_properties.is_active(key) {
2370                indexes
2371                    .node_properties
2372                    .remove_with_scopes(node_id, labels.clone(), key, value);
2373            }
2374        }
2375    }
2376
2377    fn index_relationship_property_if_active<'a>(
2378        &mut self,
2379        rel_id: RelationshipId,
2380        scopes: impl IntoIterator<Item = &'a str>,
2381        key: &str,
2382        value: &PropertyValue,
2383    ) {
2384        if self.active_relationship_property_index_count() == 0 {
2385            return;
2386        }
2387        let (indexes, records) = self.relationship_index_parts();
2388        if indexes.relationship_properties.is_active(key) {
2389            indexes
2390                .relationship_properties
2391                .insert_with_scopes(rel_id, scopes, key, value, &records);
2392        }
2393    }
2394
2395    fn index_relationship_properties_if_active<'a>(
2396        &mut self,
2397        rel_id: RelationshipId,
2398        scopes: impl IntoIterator<Item = &'a str> + Clone,
2399        properties: &Properties,
2400    ) {
2401        if self.active_relationship_property_index_count() == 0 {
2402            return;
2403        }
2404        let (indexes, records) = self.relationship_index_parts();
2405        for (key, value) in properties {
2406            if indexes.relationship_properties.is_active(key) {
2407                indexes.relationship_properties.insert_with_scopes(
2408                    rel_id,
2409                    scopes.clone(),
2410                    key,
2411                    value,
2412                    &records,
2413                );
2414            }
2415        }
2416    }
2417
2418    fn unindex_relationship_property_if_active<'a>(
2419        &mut self,
2420        rel_id: RelationshipId,
2421        scopes: impl IntoIterator<Item = &'a str>,
2422        key: &str,
2423        value: &PropertyValue,
2424    ) {
2425        if self.active_relationship_property_index_count() == 0 {
2426            return;
2427        }
2428        let indexes = self.indexes_mut();
2429        if indexes.relationship_properties.is_active(key) {
2430            indexes
2431                .relationship_properties
2432                .remove_with_scopes(rel_id, scopes, key, value);
2433        }
2434    }
2435
2436    fn unindex_active_relationship_properties<'a>(
2437        &mut self,
2438        rel_id: RelationshipId,
2439        scopes: impl IntoIterator<Item = &'a str> + Clone,
2440        properties: &Properties,
2441    ) {
2442        if self.active_relationship_property_index_count() == 0 {
2443            return;
2444        }
2445        let indexes = self.indexes_mut();
2446        for (key, value) in properties {
2447            if indexes.relationship_properties.is_active(key) {
2448                indexes.relationship_properties.remove_with_scopes(
2449                    rel_id,
2450                    scopes.clone(),
2451                    key,
2452                    value,
2453                );
2454            }
2455        }
2456    }
2457
2458    pub(super) fn scan_nodes_by_property(
2459        &self,
2460        label: Option<&str>,
2461        key: &str,
2462        value: &PropertyValue,
2463    ) -> Vec<NodeRecord> {
2464        match label {
2465            Some(label) => self
2466                .nodes_by_label
2467                .get(label)
2468                .into_iter()
2469                .flat_map(|ids| ids.iter())
2470                .filter_map(|id| self.node_at(id))
2471                .filter(|node| node.properties().get(key).is_some_and(|v| v == *value))
2472                .map(|node| node.to_record())
2473                .collect(),
2474            None => self
2475                .iter_node_refs()
2476                .filter(|node| node.properties().get(key).is_some_and(|v| v == *value))
2477                .map(|node| node.to_record())
2478                .collect(),
2479        }
2480    }
2481
2482    pub(super) fn scan_node_ids_by_property(
2483        &self,
2484        label: Option<&str>,
2485        key: &str,
2486        value: &PropertyValue,
2487    ) -> Vec<NodeId> {
2488        match label {
2489            Some(label) => self
2490                .nodes_by_label
2491                .get(label)
2492                .into_iter()
2493                .flat_map(|ids| ids.iter())
2494                .filter_map(|id| {
2495                    (self
2496                        .node_at(id)?
2497                        .properties()
2498                        .get(key)
2499                        .is_some_and(|v| v == *value))
2500                    .then_some(id)
2501                })
2502                .collect(),
2503            None => self
2504                .iter_nodes()
2505                .filter_map(|(id, node)| {
2506                    (node.properties().get(key).is_some_and(|v| v == *value)).then_some(id)
2507                })
2508                .collect(),
2509        }
2510    }
2511
2512    pub(super) fn any_node_by_property(
2513        &self,
2514        label: &str,
2515        key: &str,
2516        value: &PropertyValue,
2517    ) -> bool {
2518        self.nodes_by_label
2519            .get(label)
2520            .into_iter()
2521            .flat_map(|ids| ids.iter())
2522            .filter_map(|id| self.node_at(id))
2523            .any(|node| node.properties().get(key).is_some_and(|v| v == *value))
2524    }
2525
2526    pub(super) fn scan_relationships_by_property(
2527        &self,
2528        rel_type: Option<&str>,
2529        key: &str,
2530        value: &PropertyValue,
2531    ) -> Vec<RelationshipRecord> {
2532        match rel_type {
2533            Some(rel_type) => self
2534                .relationships_by_type
2535                .get(rel_type)
2536                .into_iter()
2537                .flat_map(|ids| ids.iter())
2538                .filter_map(|id| self.rel_at(id))
2539                .filter(|rel| rel.properties().get(key).is_some_and(|v| v == *value))
2540                .map(|rel| rel.to_record())
2541                .collect(),
2542            None => self
2543                .iter_rel_refs()
2544                .filter(|rel| rel.properties().get(key).is_some_and(|v| v == *value))
2545                .map(|rel| rel.to_record())
2546                .collect(),
2547        }
2548    }
2549
2550    pub(super) fn scan_relationship_ids_by_property(
2551        &self,
2552        rel_type: Option<&str>,
2553        key: &str,
2554        value: &PropertyValue,
2555    ) -> Vec<RelationshipId> {
2556        match rel_type {
2557            Some(rel_type) => self
2558                .relationships_by_type
2559                .get(rel_type)
2560                .into_iter()
2561                .flat_map(|ids| ids.iter())
2562                .filter_map(|id| {
2563                    (self
2564                        .rel_at(id)?
2565                        .properties()
2566                        .get(key)
2567                        .is_some_and(|v| v == *value))
2568                    .then_some(id)
2569                })
2570                .collect(),
2571            None => self
2572                .iter_rels()
2573                .filter_map(|(id, rel)| {
2574                    (rel.properties().get(key).is_some_and(|v| v == *value)).then_some(id)
2575                })
2576                .collect(),
2577        }
2578    }
2579
2580    pub(super) fn any_relationship_by_property(
2581        &self,
2582        rel_type: &str,
2583        key: &str,
2584        value: &PropertyValue,
2585    ) -> bool {
2586        self.relationships_by_type
2587            .get(rel_type)
2588            .into_iter()
2589            .flat_map(|ids| ids.iter())
2590            .filter_map(|id| self.rel_at(id))
2591            .any(|rel| rel.properties().get(key).is_some_and(|v| v == *value))
2592    }
2593
2594    pub(super) fn attach_relationship(&mut self, rel: &RelationshipRecord) {
2595        let type_id = self.dicts.types.id_or_insert(&rel.rel_type);
2596        self.adjacency_push(
2597            rel.src,
2598            Side::Out,
2599            AdjEntry {
2600                type_id,
2601                neighbour: rel.dst,
2602                rel: rel.id,
2603            },
2604        );
2605        self.adjacency_push(
2606            rel.dst,
2607            Side::In,
2608            AdjEntry {
2609                type_id,
2610                neighbour: rel.src,
2611                rel: rel.id,
2612            },
2613        );
2614        self.insert_relationship_type_index(rel.id, &rel.rel_type);
2615    }
2616
2617    fn detach_relationship_indexes(
2618        &mut self,
2619        rel: &RelationshipRecord,
2620        mut pending: Option<&mut PendingAdjRemovals>,
2621    ) {
2622        self.adjacency_remove(rel.src, Side::Out, rel.id, pending.as_deref_mut());
2623        self.adjacency_remove(rel.dst, Side::In, rel.id, pending);
2624
2625        self.remove_relationship_type_index(rel.id, &rel.rel_type);
2626    }
2627
2628    pub(super) fn relationship_ids_for_direction(
2629        &self,
2630        node_id: NodeId,
2631        direction: Direction,
2632    ) -> Vec<RelationshipId> {
2633        let mut ids = Vec::new();
2634        let _ = self.try_for_each_adjacent_id_unchecked(node_id, direction, &[], |rel_id, _| {
2635            ids.push(rel_id);
2636            Ok::<(), ()>(())
2637        });
2638        ids
2639    }
2640
2641    pub(super) fn has_incident_relationships(&self, node_id: NodeId) -> bool {
2642        self.outgoing_at(node_id).is_some_and(|adj| !adj.is_empty())
2643            || self.incoming_at(node_id).is_some_and(|adj| !adj.is_empty())
2644    }
2645
2646    pub(super) fn incident_relationship_ids(&self, node_id: NodeId) -> Vec<RelationshipId> {
2647        self.relationship_ids_for_direction(node_id, Direction::Undirected)
2648    }
2649
2650    /// Replay a node creation using the id captured in a durable mutation
2651    /// event. This intentionally does not emit a new mutation event: callers
2652    /// must invoke it before installing a recorder on the graph.
2653    #[doc(hidden)]
2654    pub fn replay_create_node(
2655        &mut self,
2656        id: NodeId,
2657        labels: Vec<String>,
2658        properties: Properties,
2659    ) -> Result<NodeRecord, String> {
2660        if self.recorder.is_some() {
2661            return Err(
2662                "cannot replay node creation while a mutation recorder is installed".into(),
2663            );
2664        }
2665        if self.has_node_at(id) {
2666            return Err(format!("node id {id} already exists"));
2667        }
2668        let idx = self.ensure_node_slot_checked(id)?;
2669        self.bump_next_node_id_past(id)?;
2670
2671        let labels = Self::normalize_labels(labels);
2672        let node = NodeRecord {
2673            id,
2674            labels,
2675            properties,
2676        };
2677
2678        self.put_node_at_slot(idx, &node);
2679        // Same index maintenance as a live create: only hash indexes that
2680        // are already active (declared by a replayed CREATE INDEX /
2681        // CREATE CONSTRAINT, which backfills from the data replayed so
2682        // far) are kept current. Lookup-activated (implicit) indexes are
2683        // rebuilt lazily on first use, exactly as in a fresh process.
2684        self.on_node_created(&node);
2685
2686        Ok(node)
2687    }
2688
2689    /// Replay a relationship creation using the id captured in a durable
2690    /// mutation event. This intentionally does not emit a new mutation event:
2691    /// callers must invoke it before installing a recorder on the graph.
2692    #[doc(hidden)]
2693    pub fn replay_create_relationship(
2694        &mut self,
2695        id: RelationshipId,
2696        src: NodeId,
2697        dst: NodeId,
2698        rel_type: &str,
2699        properties: Properties,
2700    ) -> Result<RelationshipRecord, String> {
2701        if self.recorder.is_some() {
2702            return Err(
2703                "cannot replay relationship creation while a mutation recorder is installed".into(),
2704            );
2705        }
2706        if self.has_rel_at(id) {
2707            return Err(format!("relationship id {id} already exists"));
2708        }
2709        if !self.has_node_at(src) {
2710            return Err(format!(
2711                "relationship {id} references missing source node {src}"
2712            ));
2713        }
2714        if !self.has_node_at(dst) {
2715            return Err(format!(
2716                "relationship {id} references missing target node {dst}"
2717            ));
2718        }
2719
2720        let trimmed = rel_type.trim();
2721        if trimmed.is_empty() {
2722            return Err(format!("relationship {id} has an empty type"));
2723        }
2724        let idx = self.ensure_rel_slot_checked(id)?;
2725        self.bump_next_rel_id_past(id)?;
2726
2727        let rel = RelationshipRecord {
2728            id,
2729            src,
2730            dst,
2731            rel_type: trimmed.into(),
2732            properties,
2733        };
2734
2735        self.put_rel_at_slot(idx, &rel);
2736        // See `replay_create_node`: active (declared) indexes only.
2737        self.on_relationship_created(&rel);
2738
2739        Ok(rel)
2740    }
2741
2742    #[cfg(test)]
2743    pub(super) fn assert_property_indexes_match_scan(&self) {
2744        let indexes = self.indexes_read();
2745        assert_eq!(
2746            indexes.node_properties.active_keys.len(),
2747            self.active_node_property_index_count(),
2748            "node property index counter diverged from active key set"
2749        );
2750        assert_eq!(
2751            indexes.relationship_properties.active_keys.len(),
2752            self.active_relationship_property_index_count(),
2753            "relationship property index counter diverged from active key set"
2754        );
2755
2756        let mut expected_nodes = PropertyIndexState {
2757            active_keys: indexes.node_properties.active_keys.clone(),
2758            any_scope_keys: indexes.node_properties.any_scope_keys.clone(),
2759            ..PropertyIndexState::default()
2760        };
2761        for (id, node) in self.iter_nodes() {
2762            for (key, value) in node.properties() {
2763                if expected_nodes.is_active(key) {
2764                    expected_nodes.insert_with_scopes(
2765                        id,
2766                        node.labels().strs(),
2767                        key,
2768                        &value.to_owned(),
2769                        &self.node_values(),
2770                    );
2771                }
2772            }
2773        }
2774        assert_eq!(
2775            indexes.node_properties.scoped_values, expected_nodes.scoped_values,
2776            "node property scoped index values diverged from scan"
2777        );
2778        assert_eq!(
2779            indexes.node_properties.any_scope, expected_nodes.any_scope,
2780            "node property across-scopes index values diverged from scan"
2781        );
2782
2783        let mut expected_relationships = PropertyIndexState {
2784            active_keys: indexes.relationship_properties.active_keys.clone(),
2785            any_scope_keys: indexes.relationship_properties.any_scope_keys.clone(),
2786            ..PropertyIndexState::default()
2787        };
2788        for (id, rel) in self.iter_rels() {
2789            for (key, value) in rel.properties() {
2790                if expected_relationships.is_active(key) {
2791                    expected_relationships.insert_with_scopes(
2792                        id,
2793                        [rel.rel_type()],
2794                        key,
2795                        &value.to_owned(),
2796                        &self.relationship_values(),
2797                    );
2798                }
2799            }
2800        }
2801        assert_eq!(
2802            indexes.relationship_properties.scoped_values, expected_relationships.scoped_values,
2803            "relationship property scoped index values diverged from scan"
2804        );
2805        assert_eq!(
2806            indexes.relationship_properties.any_scope, expected_relationships.any_scope,
2807            "relationship property across-scopes index values diverged from scan"
2808        );
2809    }
2810}
2811
2812/// Property values read from a slab's records, for the hash property
2813/// index's digest entries (see `PropertyValueBuckets`).
2814pub(super) struct RecordValues<'g> {
2815    slab: &'g RecordSlab,
2816    dicts: &'g Dicts,
2817    nodes: bool,
2818}
2819
2820impl<'g> RecordValues<'g> {
2821    pub(super) fn nodes(slab: &'g RecordSlab, dicts: &'g Dicts) -> Self {
2822        Self {
2823            slab,
2824            dicts,
2825            nodes: true,
2826        }
2827    }
2828
2829    pub(super) fn relationships(slab: &'g RecordSlab, dicts: &'g Dicts) -> Self {
2830        Self {
2831            slab,
2832            dicts,
2833            nodes: false,
2834        }
2835    }
2836}
2837
2838impl EntityValues for RecordValues<'_> {
2839    #[inline]
2840    fn get(&self, id: u64, key: &str) -> Option<crate::ValueRef<'_>> {
2841        let bytes = self.slab.get(usize::try_from(id).ok()?)?;
2842        if self.nodes {
2843            StoredNode::new(id, bytes, self.dicts).properties().get(key)
2844        } else {
2845            StoredRel::new(id, bytes, self.dicts).properties().get(key)
2846        }
2847    }
2848}
2849
2850/// Set or remove one property of the record in `slab` slot `idx`,
2851/// rewriting only that entry. The previous value; `None` when the slot is
2852/// empty or there was nothing to remove.
2853#[inline]
2854fn edit_record_property(
2855    slab: &mut RecordSlab,
2856    idx: usize,
2857    kind: encoded::RecordKind,
2858    dicts: &mut Dicts,
2859    key: &str,
2860    edit: encoded::PropEdit<'_>,
2861) -> Option<Option<PropertyValue>> {
2862    let bytes = slab.get(idx)?;
2863    let mut out = Scratch::take();
2864    let old = encoded::edit_property(bytes, kind, dicts, key, edit, &mut out)?;
2865    slab.set(idx, &out);
2866    Some(old)
2867}
2868
2869/// Option of a constraint's backing RANGE index: `false` when it keeps the
2870/// hash index only (see [`ConstraintBacking::Hash`]).
2871pub(super) const SORTED_OPTION: &str = "sorted";
2872
2873/// Whether a RANGE index keeps a sorted copy of its values.
2874pub(super) fn is_sorted(def: &IndexDefinition) -> bool {
2875    !matches!(
2876        def.options.get(SORTED_OPTION),
2877        Some(IndexConfigValue::Bool(false))
2878    )
2879}