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::{AdjEntry, AdjSide, Adjacency, Side, TypeFilter};
17use super::chunked_vec::ChunkedVec;
18use super::constraint_catalog::{
19    ConstraintCatalog, ConstraintRequest, CreateConstraintError, CreateConstraintOutcome,
20    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    IndexDefinition, IndexRequest, StoredIndexEntity, StoredIndexKind, StoredIndexState,
32};
33use super::point_index::PointRegistry;
34#[cfg(test)]
35use super::property_index::PropertyIndexState;
36use super::property_index::{PropertyIndexRegistry, UNLABELLED};
37use super::record_slab::RecordSlab;
38use super::secondary_index_maintenance::SecondaryIndexMutation;
39use super::sorted_property_index::SortedPropertyIndex;
40use super::stats::GraphStats;
41use super::text_index::TrigramRegistry;
42use super::vector_index::{VectorIndexProvider, VectorIndexRegistry, VectorSimilarity};
43use crate::dict::Dicts;
44use crate::encoded::{self, Scratch, StoredNode, StoredRel};
45use crate::{NodeRef, RelRef};
46
47#[derive(Default)]
48pub struct InMemoryGraph {
49    pub(super) next_node_id: NodeId,
50    pub(super) next_rel_id: RelationshipId,
51
52    /// Slot-indexed node storage: slot `id` holds the node `id`, encoded
53    /// (see [`crate::encoded`]). Empty slots are tombstones from deletes
54    /// (we don't compact). Because `next_node_id` is monotonic the slot at
55    /// `id` exists exactly when `id < next_node_id`.
56    ///
57    /// [`Self::clone`] (called on every staged write to build a working
58    /// copy, and for every reader snapshot) shares this storage: a
59    /// [`RecordSlab`] clone is one refcount bump, so the whole-graph
60    /// clone is O(#labels + #relationship types), not O(N). A write
61    /// copies only the leaf path it touches: a leaf's offsets, and at
62    /// most one buffer of 64 records. A record is immutable: changing it
63    /// stores a new encoding in its slot (see [`Self::update_node`]).
64    pub(super) nodes: RecordSlab,
65    pub(super) relationships: RecordSlab,
66    /// Numbers the labels, relationship types and property keys the
67    /// records and adjacency entries refer to. Shared with clones until
68    /// one of them sees a new name.
69    pub(super) dicts: Dicts,
70    /// Live (non-tombstoned) counts kept in sync with `put_*`/`take_*` so
71    /// `node_count` / `relationship_count` stay O(1) — without a counter
72    /// they'd have to scan the slab.
73    pub(super) live_node_count: usize,
74    pub(super) live_rel_count: usize,
75
76    /// Adjacency keyed by NodeId. `adjacency[id]` lists the relationships
77    /// that leave `id` as `(type, target, relationship id)` and those that
78    /// enter it with the source as the neighbour, in one allocation. A hop
79    /// reads only the list: it never touches a relationship record. See
80    /// [`super::adjacency`].
81    pub(super) adjacency: ChunkedVec<Adjacency>,
82
83    // secondary indexes
84    /// Label -> the ids of the nodes that carry it, ascending. An
85    /// [`IdBitmap`] holds an id in two bytes or less (an eighth of a byte
86    /// for a label most nodes carry), where a list held eight, and
87    /// removes one by binary search.
88    pub(super) nodes_by_label: BTreeMap<String, IdBitmap>,
89    pub(super) relationships_by_type: BTreeMap<String, IdBitmap>,
90
91    /// All index machinery — the declared-index catalog, hash-bucket
92    /// property registry, and the per-entity-kind secondary index
93    /// registries (text, sorted, point, fulltext) plus their active
94    /// counters — collapsed into one bundle. See [`IndexBundle`] for
95    /// the rationale. The bundle is a packaging-only abstraction:
96    /// every field accessed through `self.indexes.<x>` lives at the
97    /// same address it would have as a top-level field.
98    pub(super) indexes: IndexBundle,
99
100    /// Per-(label / type, property key) distinct-value sketches for the
101    /// planner (see [`super::distinct_stats`]). Kept for every key, not
102    /// only indexed ones, and a function of the live data alone, so
103    /// plans don't depend on index activation, lookup history or
104    /// restarts. Cloning is four refcount bumps.
105    pub(super) distinct_stats: DistinctStatsRegistry,
106
107    /// Catalog of explicitly-created constraints (CREATE CONSTRAINT).
108    /// Deliberately not part of [`IndexBundle`] — constraints describe
109    /// data invariants, not indexed access. The fact that uniqueness /
110    /// key constraints back range indexes is handled in the
111    /// constraint code path, not by the bundle's layout.
112    pub(super) constraint_catalog: RwLock<Arc<ConstraintCatalog>>,
113    /// Fast-path counter for mutation-time constraint checks. Most
114    /// workloads have no constraints installed; this lets the executor
115    /// skip taking the catalog lock in that case.
116    pub(super) active_constraints: AtomicUsize,
117
118    /// Optional mutation observer. When `Some`, every committed mutation
119    /// fans out to this recorder *after* the in-memory state has been
120    /// updated. The recorder is not part of the graph's identity, so Clone
121    /// and snapshot restore both reset it to `None`.
122    pub(super) recorder: Option<Arc<dyn MutationRecorder>>,
123
124    /// Optional sink that sees each node / relationship record just before
125    /// a delete drops it. Change feeds use it to report deleted entities
126    /// without copying the graph. Like the recorder, it is not part of the
127    /// graph's identity and is dropped on clone.
128    pub(super) deleted_sink: Option<Arc<dyn DeletedRecordSink>>,
129}
130
131impl std::fmt::Debug for InMemoryGraph {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        f.debug_struct("InMemoryGraph")
134            .field("next_node_id", &self.next_node_id)
135            .field("next_rel_id", &self.next_rel_id)
136            .field(
137                "nodes",
138                &self.iter_nodes().map(|(_, n)| n).collect::<Vec<_>>(),
139            )
140            .field(
141                "relationships",
142                &self.iter_rels().map(|(_, r)| r).collect::<Vec<_>>(),
143            )
144            .field("adjacency", &self.adjacency)
145            .field("nodes_by_label", &self.nodes_by_label)
146            .field("relationships_by_type", &self.relationships_by_type)
147            .field("indexes", &self.indexes)
148            .field(
149                "active_node_property_indexes",
150                &self.active_node_property_index_count(),
151            )
152            .field(
153                "active_relationship_property_indexes",
154                &self.active_relationship_property_index_count(),
155            )
156            .field(
157                "index_catalog_entries",
158                &self
159                    .indexes
160                    .catalog
161                    .read()
162                    .map(|c| c.list().len())
163                    .unwrap_or(0),
164            )
165            .field("active_constraints", &self.active_constraint_count())
166            .field(
167                "active_fulltext_indexes",
168                &self.active_fulltext_index_count(),
169            )
170            .field("recorder", &self.recorder.as_ref().map(|_| "installed"))
171            .finish()
172    }
173}
174
175impl Clone for InMemoryGraph {
176    fn clone(&self) -> Self {
177        // Deliberately drop the recorder on clone: a cloned store is a
178        // separate identity; it should not silently share the observer.
179        Self {
180            next_node_id: self.next_node_id,
181            next_rel_id: self.next_rel_id,
182            nodes: self.nodes.clone(),
183            relationships: self.relationships.clone(),
184            live_node_count: self.live_node_count,
185            live_rel_count: self.live_rel_count,
186            adjacency: self.adjacency.clone(),
187            dicts: self.dicts.clone(),
188            nodes_by_label: self.nodes_by_label.clone(),
189            relationships_by_type: self.relationships_by_type.clone(),
190            // Shares every registry and catalog; each is copied on its
191            // first write (see `IndexBundle`).
192            indexes: self.indexes.clone(),
193            distinct_stats: self.distinct_stats.clone(),
194            constraint_catalog: share(&self.constraint_catalog),
195            active_constraints: AtomicUsize::new(self.active_constraint_count()),
196            recorder: None,
197            deleted_sink: None,
198        }
199    }
200}
201
202impl InMemoryGraph {
203    pub fn new() -> Self {
204        Self::default()
205    }
206
207    /// Kept for API compatibility. The chunked storage allocates full
208    /// chunks as the graph grows, so there is no repeated doubling for a
209    /// capacity hint to avoid.
210    pub fn with_capacity_hint(_nodes: usize, _relationships: usize) -> Self {
211        Self::default()
212    }
213
214    pub fn contains_node(&self, node_id: NodeId) -> bool {
215        self.has_node_at(node_id)
216    }
217
218    pub fn contains_relationship(&self, rel_id: RelationshipId) -> bool {
219        self.has_rel_at(rel_id)
220    }
221
222    /// Install (or clear) the mutation recorder. Passing `None` detaches any
223    /// currently-installed recorder. The recorder observes every committed
224    /// mutation *after* it has been applied.
225    pub fn set_mutation_recorder(&mut self, recorder: Option<Arc<dyn MutationRecorder>>) {
226        self.recorder = recorder;
227    }
228
229    /// Install (or clear) the [`DeletedRecordSink`].
230    pub fn set_deleted_record_sink(&mut self, sink: Option<Arc<dyn DeletedRecordSink>>) {
231        self.deleted_sink = sink;
232    }
233
234    /// Handle to the currently-installed recorder, if any.
235    pub fn mutation_recorder(&self) -> Option<&Arc<dyn MutationRecorder>> {
236        self.recorder.as_ref()
237    }
238
239    /// Emit a mutation event only if a recorder is installed. The event is
240    /// built lazily — callers pass a closure, so when no recorder is
241    /// attached we pay only a `None` check and the cost of constructing the
242    /// event (labels/properties clones) is avoided.
243    #[inline]
244    pub(super) fn emit<F: FnOnce() -> MutationEvent>(&self, build: F) {
245        if let Some(rec) = &self.recorder {
246            rec.record(build());
247        }
248    }
249
250    fn bump_next_node_id_past(&mut self, id: NodeId) -> Result<(), String> {
251        let next = id
252            .checked_add(1)
253            .ok_or_else(|| format!("node id {id} leaves no valid next node id"))?;
254        self.next_node_id = self.next_node_id.max(next);
255        Ok(())
256    }
257
258    fn bump_next_rel_id_past(&mut self, id: RelationshipId) -> Result<(), String> {
259        let next = id
260            .checked_add(1)
261            .ok_or_else(|| format!("relationship id {id} leaves no valid next relationship id"))?;
262        self.next_rel_id = self.next_rel_id.max(next);
263        Ok(())
264    }
265
266    pub(super) fn try_reserve_next_node_slot(&mut self) -> Option<(NodeId, usize)> {
267        let id = self.next_node_id;
268        let idx = self.ensure_node_slot_checked(id).ok()?;
269        self.bump_next_node_id_past(id).ok()?;
270        Some((id, idx))
271    }
272
273    pub(super) fn try_reserve_next_rel_slot(&mut self) -> Option<(RelationshipId, usize)> {
274        let id = self.next_rel_id;
275        let idx = self.ensure_rel_slot_checked(id).ok()?;
276        self.bump_next_rel_id_past(id).ok()?;
277        Some((id, idx))
278    }
279
280    // ---------- Slab access helpers ----------
281    //
282    // Slots are indexed by id. Readers get views over the encoded
283    // records; nothing here hands out the bytes.
284
285    #[inline]
286    pub(super) fn node_at(&self, id: NodeId) -> Option<NodeRef<'_>> {
287        let bytes = self.nodes.get(Self::slot_index(id)?)?;
288        Some(NodeRef::stored(StoredNode::new(id, bytes, &self.dicts)))
289    }
290
291    #[inline]
292    pub(super) fn has_node_at(&self, id: NodeId) -> bool {
293        Self::slot_index(id).is_some_and(|idx| self.nodes.contains(idx))
294    }
295
296    #[inline]
297    pub(super) fn rel_at(&self, id: RelationshipId) -> Option<RelRef<'_>> {
298        let bytes = self.relationships.get(Self::slot_index(id)?)?;
299        Some(RelRef::stored(StoredRel::new(id, bytes, &self.dicts)))
300    }
301
302    #[inline]
303    pub(super) fn has_rel_at(&self, id: RelationshipId) -> bool {
304        Self::slot_index(id).is_some_and(|idx| self.relationships.contains(idx))
305    }
306
307    /// `(source, target)` of a relationship, reading only the start of
308    /// its record.
309    #[inline]
310    pub(super) fn rel_endpoints_at(&self, id: RelationshipId) -> Option<(NodeId, NodeId)> {
311        let bytes = self.relationships.get(Self::slot_index(id)?)?;
312        Some(encoded::rel_endpoints(bytes))
313    }
314
315    /// Change a node: decode it, let `change` edit the record, and store
316    /// the new encoding. Records are immutable, so snapshots that share
317    /// the old one keep reading it.
318    pub(super) fn update_node<R>(
319        &mut self,
320        id: NodeId,
321        change: impl FnOnce(&mut NodeRecord) -> R,
322    ) -> Option<R> {
323        let idx = Self::slot_index(id)?;
324        let mut record = self.node_at(id)?.to_record();
325        let result = change(&mut record);
326        self.store_node(idx, &record);
327        Some(result)
328    }
329
330    /// Set or remove one property of a node, rewriting only that entry of
331    /// its record. Returns the property's previous value; `None` when the
332    /// node does not exist or there was nothing to remove.
333    pub(super) fn edit_node_property(
334        &mut self,
335        id: NodeId,
336        key: &str,
337        edit: encoded::PropEdit<'_>,
338    ) -> Option<Option<PropertyValue>> {
339        let idx = Self::slot_index(id)?;
340        edit_record_property(
341            &mut self.nodes,
342            idx,
343            encoded::RecordKind::Node,
344            &mut self.dicts.keys,
345            key,
346            edit,
347        )
348    }
349
350    pub(super) fn edit_rel_property(
351        &mut self,
352        id: RelationshipId,
353        key: &str,
354        edit: encoded::PropEdit<'_>,
355    ) -> Option<Option<PropertyValue>> {
356        let idx = Self::slot_index(id)?;
357        edit_record_property(
358            &mut self.relationships,
359            idx,
360            encoded::RecordKind::Relationship,
361            &mut self.dicts.keys,
362            key,
363            edit,
364        )
365    }
366
367    /// Resize the node-keyed Vecs so `id as usize` is in range. Adjacency
368    /// lists are kept in lockstep with `nodes`, so a freshly-grown slot has
369    /// empty outgoing/incoming Vecs ready to receive edges.
370    fn slot_len_for_id(id: u64, kind: &str) -> Result<usize, String> {
371        let idx = usize::try_from(id)
372            .map_err(|_| format!("{kind} id {id} does not fit in usize on this platform"))?;
373        idx.checked_add(1)
374            .ok_or_else(|| format!("{kind} id {id} leaves no valid slab slot"))
375    }
376
377    #[inline]
378    fn slot_index(id: u64) -> Option<usize> {
379        usize::try_from(id).ok()
380    }
381
382    fn ensure_node_slot_checked(&mut self, id: NodeId) -> Result<usize, String> {
383        let target = Self::slot_len_for_id(id, "node")?;
384        if self.nodes.len() < target {
385            let additional = target - self.nodes.len();
386            self.nodes.try_reserve(additional).map_err(|e| {
387                format!("node id {id} requires {target} slots, but allocation failed: {e}")
388            })?;
389            self.adjacency.try_reserve_exact(additional).map_err(|e| {
390                format!(
391                    "node id {id} requires {target} adjacency slots, but allocation failed: {e}"
392                )
393            })?;
394            self.nodes.grow_to(target);
395            self.adjacency.resize_with(target, Adjacency::default);
396        }
397        Ok(target - 1)
398    }
399
400    fn ensure_rel_slot_checked(&mut self, id: RelationshipId) -> Result<usize, String> {
401        let target = Self::slot_len_for_id(id, "relationship")?;
402        if self.relationships.len() < target {
403            self.relationships
404                .try_reserve(target - self.relationships.len())
405                .map_err(|e| {
406                    format!(
407                        "relationship id {id} requires {target} slots, but allocation failed: {e}"
408                    )
409                })?;
410            self.relationships.grow_to(target);
411        }
412        Ok(target - 1)
413    }
414
415    pub(super) fn put_node_checked(&mut self, id: NodeId, node: &NodeRecord) -> Result<(), String> {
416        let idx = self.ensure_node_slot_checked(id)?;
417        self.put_node_at_slot(idx, node);
418        Ok(())
419    }
420
421    pub(super) fn put_rel_checked(
422        &mut self,
423        id: RelationshipId,
424        rel: &RelationshipRecord,
425    ) -> Result<(), String> {
426        let idx = self.ensure_rel_slot_checked(id)?;
427        self.put_rel_at_slot(idx, rel);
428        Ok(())
429    }
430
431    pub(super) fn put_node_at_slot(&mut self, idx: usize, node: &NodeRecord) {
432        if self.store_node(idx, node) {
433            self.live_node_count += 1;
434        }
435    }
436
437    pub(super) fn put_rel_at_slot(&mut self, idx: usize, rel: &RelationshipRecord) {
438        let mut out = Scratch::take();
439        encoded::encode_rel(rel, &mut self.dicts, &mut out);
440        if self.relationships.set(idx, &out) {
441            self.live_rel_count += 1;
442        }
443    }
444
445    /// Encode `node` into slot `idx`; returns whether the slot was empty.
446    #[inline]
447    fn store_node(&mut self, idx: usize, node: &NodeRecord) -> bool {
448        let mut out = Scratch::take();
449        encoded::encode_node(node, &mut self.dicts, &mut out);
450        self.nodes.set(idx, &out)
451    }
452
453    pub(super) fn take_node(&mut self, id: NodeId) -> Option<NodeRecord> {
454        let idx = Self::slot_index(id)?;
455        let removed = StoredNode::new(id, self.nodes.get(idx)?, &self.dicts).to_record();
456        self.nodes.remove(idx);
457        self.live_node_count -= 1;
458        // Also clear the per-id adjacency entries so the memory is reclaimed
459        // on the typical "delete every node" pattern. We deliberately do not
460        // shrink the slots — leaving the slot lets new ids reuse the same
461        // index without growth churn (and `next_node_id` is monotonic
462        // anyway, so no immediate reuse).
463        if let Some(adj) = self.adjacency.get_mut(idx) {
464            adj.clear();
465        }
466        Some(removed)
467    }
468
469    pub(super) fn take_rel(&mut self, id: RelationshipId) -> Option<RelationshipRecord> {
470        let idx = Self::slot_index(id)?;
471        let removed = StoredRel::new(id, self.relationships.get(idx)?, &self.dicts).to_record();
472        self.relationships.remove(idx);
473        self.live_rel_count -= 1;
474        Some(removed)
475    }
476
477    #[inline]
478    pub(super) fn outgoing_at(&self, id: NodeId) -> Option<AdjSide<'_>> {
479        Some(self.adjacency.get(Self::slot_index(id)?)?.outgoing())
480    }
481
482    #[inline]
483    pub(super) fn incoming_at(&self, id: NodeId) -> Option<AdjSide<'_>> {
484        Some(self.adjacency.get(Self::slot_index(id)?)?.incoming())
485    }
486
487    /// Visit the entries of one adjacency list that pass `filter`.
488    /// `skip_self_loops` is set for the incoming half of an undirected
489    /// walk, whose outgoing half already reported the node's self-loops.
490    #[inline]
491    fn try_for_each_adjacent_entry<F, E>(
492        node_id: NodeId,
493        filter: &TypeFilter,
494        adj: AdjSide<'_>,
495        skip_self_loops: bool,
496        visit: &mut F,
497    ) -> Result<(), E>
498    where
499        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
500    {
501        for entry in adj.iter() {
502            if skip_self_loops && entry.neighbour == node_id {
503                continue;
504            }
505            if !filter.matches(entry.type_id) {
506                continue;
507            }
508            visit(entry.rel, entry.neighbour)?;
509        }
510        Ok(())
511    }
512
513    #[inline]
514    pub(super) fn try_for_each_adjacent_id_unchecked<F, E>(
515        &self,
516        node_id: NodeId,
517        direction: Direction,
518        types: &[String],
519        mut visit: F,
520    ) -> Result<(), E>
521    where
522        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
523    {
524        let filter = TypeFilter::resolve(&self.dicts.types, types);
525        if matches!(filter, TypeFilter::Nothing) {
526            return Ok(());
527        }
528        if !matches!(direction, Direction::Left) {
529            if let Some(adj) = self.outgoing_at(node_id) {
530                Self::try_for_each_adjacent_entry(node_id, &filter, adj, false, &mut visit)?;
531            }
532        }
533        if !matches!(direction, Direction::Right) {
534            if let Some(adj) = self.incoming_at(node_id) {
535                let skip_self_loops = matches!(direction, Direction::Undirected);
536                Self::try_for_each_adjacent_entry(
537                    node_id,
538                    &filter,
539                    adj,
540                    skip_self_loops,
541                    &mut visit,
542                )?;
543            }
544        }
545
546        Ok(())
547    }
548
549    #[inline]
550    pub(super) fn try_for_each_adjacent_id<F, E>(
551        &self,
552        node_id: NodeId,
553        direction: Direction,
554        types: &[String],
555        visit: F,
556    ) -> Result<(), E>
557    where
558        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
559    {
560        if !self.has_node_at(node_id) {
561            return Ok(());
562        }
563        self.try_for_each_adjacent_id_unchecked(node_id, direction, types, visit)
564    }
565
566    pub(super) fn iter_node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
567        self.nodes.slots_from(0).map(|i| i as NodeId)
568    }
569
570    pub(super) fn iter_node_refs(&self) -> impl Iterator<Item = NodeRef<'_>> + '_ {
571        self.iter_nodes().map(|(_, node)| node)
572    }
573
574    pub(super) fn iter_rel_ids(&self) -> impl Iterator<Item = RelationshipId> + '_ {
575        self.relationships
576            .slots_from(0)
577            .map(|i| i as RelationshipId)
578    }
579
580    pub(super) fn iter_rel_refs(&self) -> impl Iterator<Item = RelRef<'_>> + '_ {
581        self.iter_rels().map(|(_, rel)| rel)
582    }
583
584    pub(super) fn iter_nodes(&self) -> impl Iterator<Item = (NodeId, NodeRef<'_>)> + '_ {
585        let dicts = &self.dicts;
586        self.nodes.iter().map(move |(i, bytes)| {
587            let id = i as NodeId;
588            (id, NodeRef::stored(StoredNode::new(id, bytes, dicts)))
589        })
590    }
591
592    pub(super) fn iter_rels(&self) -> impl Iterator<Item = (RelationshipId, RelRef<'_>)> + '_ {
593        let dicts = &self.dicts;
594        self.relationships.iter().map(move |(i, bytes)| {
595            let id = i as RelationshipId;
596            (id, RelRef::stored(StoredRel::new(id, bytes, dicts)))
597        })
598    }
599
600    /// Add an entry to `node_id`'s list on `side`. Relies on
601    /// the monotonic-id invariant: relationship ids are allocated once and
602    /// never re-used, so the list can never see a duplicate.
603    fn adjacency_push(&mut self, node_id: NodeId, side: Side, entry: AdjEntry) {
604        if let Ok(idx) = self.ensure_node_slot_checked(node_id) {
605            self.adjacency[idx].push(side, entry);
606        }
607    }
608
609    fn adjacency_remove(&mut self, node_id: NodeId, side: Side, rel_id: RelationshipId) {
610        if let Some(adj) = Self::slot_index(node_id).and_then(|idx| self.adjacency.get_mut(idx)) {
611            adj.remove(side, rel_id);
612        }
613    }
614
615    pub(super) fn normalize_labels(labels: Vec<String>) -> Labels {
616        let mut out = Labels::new();
617        for label in &labels {
618            let label = label.trim();
619            if !label.is_empty() && !out.has(label) {
620                out.push(label);
621            }
622        }
623        out
624    }
625
626    pub(super) fn insert_node_label_index(&mut self, node_id: NodeId, label: &str) {
627        // Hot path: skip the `String` alloc when the label bucket already
628        // exists. The monotonic-id invariant on the create path guarantees
629        // `node_id` is unique, so we push unconditionally; the previous
630        // `contains` guard turned bulk CREATE into O(n²).
631        if let Some(bucket) = self.nodes_by_label.get_mut(label) {
632            bucket.insert(node_id);
633        } else {
634            self.nodes_by_label
635                .insert(label.to_string(), std::iter::once(node_id).collect());
636        }
637    }
638
639    fn remove_node_label_index(&mut self, node_id: NodeId, label: &str) {
640        if let Some(ids) = self.nodes_by_label.get_mut(label) {
641            ids.remove(node_id);
642            if ids.is_empty() {
643                self.nodes_by_label.remove(label);
644            }
645        }
646    }
647
648    fn insert_relationship_type_index(&mut self, rel_id: RelationshipId, rel_type: &str) {
649        // See `insert_node_label_index` for the same hot-path rationale.
650        if let Some(bucket) = self.relationships_by_type.get_mut(rel_type) {
651            bucket.insert(rel_id);
652        } else {
653            self.relationships_by_type
654                .insert(rel_type.to_string(), std::iter::once(rel_id).collect());
655        }
656    }
657
658    fn remove_relationship_type_index(&mut self, rel_id: RelationshipId, rel_type: &str) {
659        if let Some(ids) = self.relationships_by_type.get_mut(rel_type) {
660            ids.remove(rel_id);
661            if ids.is_empty() {
662                self.relationships_by_type.remove(rel_type);
663            }
664        }
665    }
666
667    /// Ids of `label` nodes whose `key` equals `value`, from the scoped
668    /// hash index. `None` when that index is not active for `key` or
669    /// `value` has no index image; the caller must then scan.
670    pub(super) fn indexed_node_ids(
671        &self,
672        label: &str,
673        key: &str,
674        value: &PropertyValue,
675    ) -> Option<Vec<NodeId>> {
676        super::property_index::PropertyIndexKey::from_value(value)?;
677        let indexes = self.indexes_read();
678        if !indexes.node_properties.is_active(key) {
679            return None;
680        }
681        Some(
682            indexes
683                .node_properties
684                .scoped_ids_for(label, key, value)
685                .map(|ids| ids.to_vec())
686                .unwrap_or_default(),
687        )
688    }
689
690    /// Relationship counterpart of [`Self::indexed_node_ids`].
691    pub(super) fn indexed_rel_ids(
692        &self,
693        rel_type: &str,
694        key: &str,
695        value: &PropertyValue,
696    ) -> Option<Vec<RelationshipId>> {
697        super::property_index::PropertyIndexKey::from_value(value)?;
698        let indexes = self.indexes_read();
699        if !indexes.relationship_properties.is_active(key) {
700            return None;
701        }
702        Some(
703            indexes
704                .relationship_properties
705                .scoped_ids_for(rel_type, key, value)
706                .map(|ids| ids.to_vec())
707                .unwrap_or_default(),
708        )
709    }
710
711    pub(super) fn indexes_read(&self) -> std::sync::RwLockReadGuard<'_, PropertyIndexRegistry> {
712        self.indexes
713            .properties
714            .read()
715            .unwrap_or_else(|poisoned| poisoned.into_inner())
716    }
717
718    pub(super) fn indexes_write(&self) -> RwLockWriteGuard<'_, PropertyIndexRegistry> {
719        self.indexes
720            .properties
721            .write()
722            .unwrap_or_else(|poisoned| poisoned.into_inner())
723    }
724
725    pub(super) fn indexes_mut(&mut self) -> &mut PropertyIndexRegistry {
726        self.indexes
727            .properties
728            .get_mut()
729            .unwrap_or_else(|poisoned| poisoned.into_inner())
730    }
731
732    #[inline]
733    pub(super) fn active_node_property_index_count(&self) -> usize {
734        self.indexes
735            .active_node_property_indexes
736            .load(Ordering::Relaxed)
737    }
738
739    #[inline]
740    pub(super) fn active_relationship_property_index_count(&self) -> usize {
741        self.indexes
742            .active_relationship_property_indexes
743            .load(Ordering::Relaxed)
744    }
745
746    #[inline]
747    pub(super) fn active_constraint_count(&self) -> usize {
748        self.active_constraints.load(Ordering::Relaxed)
749    }
750
751    #[inline]
752    pub(super) fn has_active_constraints(&self) -> bool {
753        self.active_constraint_count() != 0
754    }
755
756    #[inline]
757    pub(super) fn active_fulltext_index_count(&self) -> usize {
758        self.indexes.active_fulltext_indexes.load(Ordering::Relaxed)
759    }
760
761    #[inline]
762    pub(super) fn has_active_fulltext_indexes(&self) -> bool {
763        self.active_fulltext_index_count() != 0
764    }
765
766    pub(super) fn node_property_index_is_active(&mut self, key: &str) -> bool {
767        self.active_node_property_index_count() != 0
768            && self.indexes_mut().node_properties.is_active(key)
769    }
770
771    pub(super) fn relationship_property_index_is_active(&mut self, key: &str) -> bool {
772        self.active_relationship_property_index_count() != 0
773            && self.indexes_mut().relationship_properties.is_active(key)
774    }
775
776    /// Give `key` its across-labels map, for lookups that name no label.
777    /// Call after [`Self::ensure_node_property_index`].
778    pub(super) fn ensure_any_scope_node_property_index(&self, key: &str) {
779        if self.indexes_read().node_properties.any_scope_is_active(key) {
780            return;
781        }
782        self.indexes_write().node_properties.activate_any_scope(key);
783    }
784
785    pub(super) fn ensure_any_scope_relationship_property_index(&self, key: &str) {
786        if self
787            .indexes_read()
788            .relationship_properties
789            .any_scope_is_active(key)
790        {
791            return;
792        }
793        self.indexes_write()
794            .relationship_properties
795            .activate_any_scope(key);
796    }
797
798    pub(super) fn ensure_node_property_index(&self, key: &str) {
799        {
800            let indexes = self.indexes_read();
801            if indexes.node_properties.is_active(key) {
802                return;
803            }
804        }
805
806        let mut indexes = self.indexes_write();
807        if indexes.node_properties.is_active(key) {
808            return;
809        }
810
811        indexes.node_properties.insert_bulk(key, || {
812            self.iter_nodes().filter_map(|(id, node)| {
813                let value = node.properties().get(key)?;
814                Some((id, node.labels().strs(), value))
815            })
816        });
817        if indexes.node_properties.activate(key) {
818            self.indexes
819                .active_node_property_indexes
820                .fetch_add(1, Ordering::Relaxed);
821        }
822    }
823
824    pub(super) fn ensure_relationship_property_index(&self, key: &str) {
825        {
826            let indexes = self.indexes_read();
827            if indexes.relationship_properties.is_active(key) {
828                return;
829            }
830        }
831
832        let mut indexes = self.indexes_write();
833        if indexes.relationship_properties.is_active(key) {
834            return;
835        }
836
837        indexes.relationship_properties.insert_bulk(key, || {
838            self.iter_rels().filter_map(|(id, rel)| {
839                let value = rel.properties().get(key)?;
840                Some((id, [rel.rel_type()], value))
841            })
842        });
843        if indexes.relationship_properties.activate(key) {
844            self.indexes
845                .active_relationship_property_indexes
846                .fetch_add(1, Ordering::Relaxed);
847        }
848    }
849
850    pub(super) fn index_catalog_read(&self) -> IndexRead<'_, IndexCatalog> {
851        read_shared(&self.indexes.catalog)
852    }
853
854    /// Mutable access copies the catalog first if a clone still shares it.
855    pub(super) fn index_catalog_write(&self) -> IndexWrite<'_, IndexCatalog> {
856        write_shared(&self.indexes.catalog)
857    }
858
859    pub(super) fn constraint_catalog_read(&self) -> IndexRead<'_, ConstraintCatalog> {
860        read_shared(&self.constraint_catalog)
861    }
862
863    /// Mutable access copies the catalog first if a clone still shares it.
864    pub(super) fn constraint_catalog_write(&self) -> IndexWrite<'_, ConstraintCatalog> {
865        write_shared(&self.constraint_catalog)
866    }
867
868    /// Register an explicitly-declared index in the catalog and, when
869    /// applicable, force the underlying property-index buckets to be
870    /// populated so equality lookups can use them immediately.
871    ///
872    /// Named with a `register_` prefix to avoid colliding with the
873    /// trait method `GraphStorageMut::create_index` — the trait impl
874    /// in `impls.rs` delegates here.
875    #[allow(clippy::result_large_err)]
876    pub(super) fn register_index(
877        &self,
878        request: IndexRequest,
879        if_not_exists: bool,
880    ) -> Result<CreateIndexOutcome, CreateIndexError> {
881        self.register_index_with_recording(request, if_not_exists, true)
882    }
883
884    #[allow(clippy::result_large_err)]
885    fn register_index_with_recording(
886        &self,
887        request: IndexRequest,
888        if_not_exists: bool,
889        record_event: bool,
890    ) -> Result<CreateIndexOutcome, CreateIndexError> {
891        let request_for_event = record_event.then(|| request.clone());
892        let outcome = {
893            let mut catalog = self.index_catalog_write();
894            catalog.try_create(request, if_not_exists)?
895        };
896
897        if let CreateIndexOutcome::Created(def) = &outcome {
898            self.populate_index_data(def);
899        }
900
901        // Both Created and NoOpExists are committed catalog states; we
902        // log only Created because NoOpExists implies a redundant DDL
903        // that adds nothing to durable state.
904        if matches!(outcome, CreateIndexOutcome::Created(_)) {
905            if let Some(request_for_event) = request_for_event {
906                self.emit(|| crate::MutationEvent::CreateIndex {
907                    request: request_for_event,
908                    if_not_exists,
909                });
910            }
911        }
912
913        Ok(outcome)
914    }
915
916    /// Replay a CreateIndex event against an empty graph. Mirrors the
917    /// `replay_create_node` shape: callers must invoke before installing
918    /// a recorder so we don't re-emit during recovery.
919    #[doc(hidden)]
920    pub fn replay_create_index(
921        &mut self,
922        request: IndexRequest,
923        if_not_exists: bool,
924    ) -> Result<(), String> {
925        if self.recorder.is_some() {
926            return Err("cannot replay create_index while a mutation recorder is installed".into());
927        }
928        self.register_index(request, if_not_exists)
929            .map(|_| ())
930            .map_err(|e| e.to_string())
931    }
932
933    /// Replay a DropIndex event.
934    #[doc(hidden)]
935    pub fn replay_drop_index(&mut self, name: &str, if_exists: bool) -> Result<(), String> {
936        if self.recorder.is_some() {
937            return Err("cannot replay drop_index while a mutation recorder is installed".into());
938        }
939        self.drop_named_index(name, if_exists)
940            .map(|_| ())
941            .map_err(|e| e.to_string())
942    }
943
944    /// Register a constraint. For uniqueness/key kinds this also
945    /// registers a backing RANGE index in the index catalog under the
946    /// same name. Validation of existing data is the caller's
947    /// responsibility (the enforcement layer runs a pre-create scan
948    /// before this method commits).
949    pub(super) fn register_constraint(
950        &self,
951        request: ConstraintRequest,
952        if_not_exists: bool,
953    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
954        // Constraint-level conflicts (22N65/66/67) take precedence over
955        // index-catalog conflicts: if the request collides with an
956        // existing *constraint* shape or name, we never get to the
957        // backing-index step.
958        {
959            let constraint_catalog = self.constraint_catalog_read();
960            if let Some(existing) = constraint_catalog.find_equivalent(&request) {
961                let cloned = existing.clone();
962                drop(constraint_catalog);
963                if if_not_exists {
964                    return Ok(CreateConstraintOutcome::NoOpExists(cloned));
965                }
966                return Err(CreateConstraintError::EquivalentConstraintExists(
967                    cloned.name,
968                ));
969            }
970            if let Some(existing) = constraint_catalog.get(&request.name) {
971                let cloned = existing.clone();
972                drop(constraint_catalog);
973                if if_not_exists {
974                    return Ok(CreateConstraintOutcome::NoOpExists(cloned));
975                }
976                return Err(CreateConstraintError::DuplicateName(cloned.name));
977            }
978            if let Some(existing) = constraint_catalog.find_same_schema(&request) {
979                let cloned = existing.clone();
980                drop(constraint_catalog);
981                if super::constraint_catalog::kinds_conflict_for_validation(
982                    &cloned.kind,
983                    &request.kind,
984                ) {
985                    if if_not_exists {
986                        return Ok(CreateConstraintOutcome::NoOpExists(cloned));
987                    }
988                    return Err(CreateConstraintError::ConflictingConstraint(cloned.name));
989                }
990            }
991        }
992
993        // Index-catalog conflicts only matter for constraints that need
994        // a backing range index. The catalog won't yet own one for this
995        // request — that registration happens below — so any existing
996        // entry under the same name or schema is from a foreign index.
997        if request.kind.requires_backing_index() {
998            let idx_catalog = self.index_catalog_read();
999            if idx_catalog.get(&request.name).is_some() {
1000                return Err(CreateConstraintError::DuplicateIndexName(
1001                    request.name.clone(),
1002                ));
1003            }
1004            let conflict = idx_catalog.list().into_iter().find(|def| {
1005                def.kind == StoredIndexKind::Range
1006                    && def.entity == request.entity
1007                    && def.label.as_deref() == Some(request.label.as_str())
1008                    && def.properties == request.properties
1009                    && def.name != request.name
1010            });
1011            drop(idx_catalog);
1012            if let Some(def) = conflict {
1013                return Err(CreateConstraintError::BackingIndexConflict(format!(
1014                    "(:{} {{{}}}) already covered by index `{}`",
1015                    request.label,
1016                    request.properties.join(", "),
1017                    def.name,
1018                )));
1019            }
1020        }
1021
1022        let owns_backing = request.kind.requires_backing_index();
1023        let request_for_event = request.clone();
1024        let outcome = {
1025            let mut catalog = self.constraint_catalog_write();
1026            catalog.try_create(request, if_not_exists)?
1027        };
1028
1029        if let CreateConstraintOutcome::Created(def) = &outcome {
1030            // Pre-create data scan: if the live graph already violates
1031            // the constraint, fail and roll back the catalog write.
1032            // Creating constraints when conflicting data exists fails
1033            // before the catalog change is retained (22N77/79/80).
1034            if let Err(violation) = self.validate_existing_data_for_constraint(def) {
1035                let mut catalog = self.constraint_catalog_write();
1036                let _ = catalog.try_drop(&def.name, true);
1037                return Err(CreateConstraintError::DataViolation(violation.to_string()));
1038            }
1039        }
1040
1041        if let CreateConstraintOutcome::Created(def) = &outcome {
1042            if owns_backing {
1043                // Register a backing RANGE index under the same name. This
1044                // is an implementation detail of the constraint, so WAL and
1045                // snapshot replay record only the constraint mutation.
1046                let idx_request = IndexRequest {
1047                    explicit_name: Some(def.name.clone()),
1048                    kind: StoredIndexKind::Range,
1049                    entity: def.entity,
1050                    label: Some(def.label.clone()),
1051                    additional_labels: Vec::new(),
1052                    properties: def.properties.clone(),
1053                    options: Default::default(),
1054                };
1055                // Errors from the backing-index registration unwind the
1056                // constraint registration to keep the two catalogs in
1057                // step.
1058                if let Err(err) = self.register_index_with_recording(idx_request, true, false) {
1059                    let mut catalog = self.constraint_catalog_write();
1060                    let _ = catalog.try_drop(&def.name, true);
1061                    return Err(CreateConstraintError::BackingIndexConflict(err.to_string()));
1062                }
1063            }
1064            self.emit(|| crate::MutationEvent::CreateConstraint {
1065                request: request_for_event,
1066                if_not_exists,
1067            });
1068            self.active_constraints.fetch_add(1, Ordering::Relaxed);
1069        }
1070
1071        Ok(outcome)
1072    }
1073
1074    /// Replay a CreateConstraint event against a recorder-detached graph.
1075    #[doc(hidden)]
1076    pub fn replay_create_constraint(
1077        &mut self,
1078        request: ConstraintRequest,
1079        if_not_exists: bool,
1080    ) -> Result<(), String> {
1081        if self.recorder.is_some() {
1082            return Err(
1083                "cannot replay create_constraint while a mutation recorder is installed".into(),
1084            );
1085        }
1086        self.register_constraint(request, if_not_exists)
1087            .map(|_| ())
1088            .map_err(|e| e.to_string())
1089    }
1090
1091    /// Replay a DropConstraint event.
1092    #[doc(hidden)]
1093    pub fn replay_drop_constraint(&mut self, name: &str, if_exists: bool) -> Result<(), String> {
1094        if self.recorder.is_some() {
1095            return Err(
1096                "cannot replay drop_constraint while a mutation recorder is installed".into(),
1097            );
1098        }
1099        self.drop_named_constraint(name, if_exists)
1100            .map(|_| ())
1101            .map_err(|e| e.to_string())
1102    }
1103
1104    /// Inverse of [`Self::register_constraint`]. Cascades to the backing
1105    /// range index when one is owned.
1106    pub(super) fn drop_named_constraint(
1107        &self,
1108        name: &str,
1109        if_exists: bool,
1110    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1111        let outcome = {
1112            let mut catalog = self.constraint_catalog_write();
1113            catalog.try_drop(name, if_exists)?
1114        };
1115        if let DropConstraintOutcome::Dropped(def) = &outcome {
1116            if let Some(index_name) = def.owned_index.as_deref() {
1117                // The backing index is owned exclusively by the
1118                // constraint, so dropping it is unconditional.
1119                let _ = self.drop_named_index_inner(index_name, true, false);
1120            }
1121            self.active_constraints.fetch_sub(1, Ordering::Relaxed);
1122            self.emit(|| crate::MutationEvent::DropConstraint {
1123                name: name.to_string(),
1124                if_exists,
1125            });
1126        }
1127        Ok(outcome)
1128    }
1129
1130    /// Inverse of [`Self::register_index`]. Removes the catalog entry
1131    /// and (for RANGE) leaves the underlying property-index buckets in
1132    /// place — they may still be needed for lazy-activation lookups
1133    /// even after the explicit DDL declaration is gone.
1134    pub(super) fn drop_named_index(
1135        &self,
1136        name: &str,
1137        if_exists: bool,
1138    ) -> Result<DropIndexOutcome, DropIndexError> {
1139        self.drop_named_index_inner(name, if_exists, true)
1140    }
1141
1142    fn drop_named_index_inner(
1143        &self,
1144        name: &str,
1145        if_exists: bool,
1146        emit_event: bool,
1147    ) -> Result<DropIndexOutcome, DropIndexError> {
1148        if let Some(owner) = self
1149            .constraint_catalog_read()
1150            .constraint_owning_index(name)
1151            .cloned()
1152        {
1153            return Err(DropIndexError::ConstraintOwned {
1154                index: name.to_string(),
1155                constraint: owner.name,
1156            });
1157        }
1158
1159        let outcome = {
1160            let mut catalog = self.index_catalog_write();
1161            catalog.try_drop(name, if_exists)?
1162        };
1163        if let DropIndexOutcome::Dropped(def) = &outcome {
1164            // Release backing structures keyed off the dropped def.
1165            match def.kind {
1166                StoredIndexKind::Text => {
1167                    if let Some(label) = def.label.as_deref() {
1168                        for prop in &def.properties {
1169                            self.deactivate_text_scope(def.entity, label, prop);
1170                        }
1171                    }
1172                }
1173                StoredIndexKind::Range => {
1174                    if let Some(label) = def.label.as_deref() {
1175                        for prop in &def.properties {
1176                            self.deactivate_sorted_scope(def.entity, label, prop);
1177                        }
1178                    }
1179                }
1180                StoredIndexKind::Point => {
1181                    if let Some(label) = def.label.as_deref() {
1182                        for prop in &def.properties {
1183                            self.deactivate_point_scope(def.entity, label, prop);
1184                        }
1185                    }
1186                }
1187                StoredIndexKind::Lookup => {
1188                    // Lookup rides on the eagerly-maintained label/type
1189                    // indexes; nothing to release.
1190                }
1191                StoredIndexKind::Vector => {
1192                    self.deactivate_vector_index(def.entity, &def.name);
1193                }
1194                StoredIndexKind::Fulltext => {
1195                    self.deactivate_fulltext_index(def.entity, &def.name);
1196                }
1197            }
1198            if emit_event {
1199                self.emit(|| crate::MutationEvent::DropIndex {
1200                    name: name.to_string(),
1201                    if_exists,
1202                });
1203            }
1204        }
1205        Ok(outcome)
1206    }
1207
1208    /// `(entity, key)` pairs whose hash property index a declaration keeps
1209    /// active: every property of a RANGE index in the catalog, which
1210    /// includes the backing indexes of uniqueness / key constraints. These
1211    /// are the only hash indexes a restart rebuilds; any other active key
1212    /// was activated implicitly by an equality lookup.
1213    pub(super) fn declared_property_index_keys(
1214        &self,
1215    ) -> std::collections::BTreeSet<(StoredIndexEntity, String)> {
1216        self.index_catalog_read()
1217            .list()
1218            .into_iter()
1219            .filter(|def| def.kind == StoredIndexKind::Range)
1220            .flat_map(|def| {
1221                let entity = def.entity;
1222                def.properties.into_iter().map(move |p| (entity, p))
1223            })
1224            .collect()
1225    }
1226
1227    fn populate_index_data(&self, def: &IndexDefinition) {
1228        // RANGE: piggy-back on the existing lazy property-index buckets.
1229        // TEXT: build a trigram inverted index over the existing entity
1230        //       data for the (label, property) tuple.
1231        // POINT: build a grid-bucket spatial index over the existing
1232        //        entity data.
1233        // LOOKUP: catalog-only; existing label/type indexes already
1234        //         answer the predicates.
1235        match def.kind {
1236            StoredIndexKind::Range => {
1237                for key in &def.properties {
1238                    match def.entity {
1239                        StoredIndexEntity::Node => self.ensure_node_property_index(key),
1240                        StoredIndexEntity::Relationship => {
1241                            self.ensure_relationship_property_index(key)
1242                        }
1243                    }
1244                    if let Some(label) = def.label.as_deref() {
1245                        self.activate_sorted_scope(def.entity, label, key);
1246                    }
1247                }
1248            }
1249            StoredIndexKind::Text => {
1250                let label = match def.label.as_deref() {
1251                    Some(l) => l,
1252                    None => return,
1253                };
1254                for property in &def.properties {
1255                    self.activate_text_scope(def.entity, label, property);
1256                }
1257            }
1258            StoredIndexKind::Point => {
1259                let label = match def.label.as_deref() {
1260                    Some(l) => l,
1261                    None => return,
1262                };
1263                let cell_size = PointRegistry::cell_size_from_options(&def.options);
1264                for property in &def.properties {
1265                    self.activate_point_scope(def.entity, label, property, cell_size);
1266                }
1267            }
1268            StoredIndexKind::Fulltext => {
1269                let labels: Vec<String> = def.all_labels().map(String::from).collect();
1270                if labels.is_empty() {
1271                    return;
1272                }
1273                self.activate_fulltext_index(def.entity, &def.name, &labels, &def.properties);
1274            }
1275            StoredIndexKind::Vector => {
1276                let label = match def.label.as_deref() {
1277                    Some(l) => l,
1278                    None => return,
1279                };
1280                let property = match def.properties.first() {
1281                    Some(p) => p.as_str(),
1282                    None => return,
1283                };
1284                let similarity = VectorSimilarity::from_options(&def.options)
1285                    .unwrap_or(VectorSimilarity::Cosine);
1286                let provider = VectorIndexProvider::from_options(&def.options)
1287                    .unwrap_or(VectorIndexProvider::Flat);
1288                let hnsw = HnswParams::from_options(&def.options);
1289                let lazy = matches!(
1290                    def.options.get("vector.populate.async"),
1291                    Some(super::index_catalog::IndexConfigValue::Bool(true))
1292                );
1293                self.activate_vector_index(
1294                    def.entity, &def.name, label, property, similarity, provider, hnsw, lazy,
1295                );
1296                if lazy {
1297                    self.index_catalog_write()
1298                        .set_state(&def.name, StoredIndexState::Populating);
1299                }
1300            }
1301            // LOOKUP rides on the label/type indexes maintained eagerly.
1302            StoredIndexKind::Lookup => {}
1303        }
1304    }
1305
1306    pub(super) fn text_indexes_read(
1307        &self,
1308        entity: StoredIndexEntity,
1309    ) -> IndexRead<'_, TrigramRegistry> {
1310        self.indexes.text.read(entity)
1311    }
1312
1313    pub(super) fn text_indexes_write(
1314        &self,
1315        entity: StoredIndexEntity,
1316    ) -> IndexWrite<'_, TrigramRegistry> {
1317        self.indexes.text.write(entity)
1318    }
1319
1320    pub(super) fn fulltext_indexes_read(
1321        &self,
1322        entity: StoredIndexEntity,
1323    ) -> IndexRead<'_, FulltextRegistry> {
1324        self.indexes.fulltext.read(entity)
1325    }
1326
1327    #[allow(dead_code)]
1328    pub(super) fn fulltext_indexes_write(
1329        &self,
1330        entity: StoredIndexEntity,
1331    ) -> IndexWrite<'_, FulltextRegistry> {
1332        self.indexes.fulltext.write(entity)
1333    }
1334
1335    fn activate_text_scope(&self, entity: StoredIndexEntity, label: &str, property: &str) {
1336        if !self.text_indexes_write(entity).add_scope(label, property) {
1337            return;
1338        }
1339
1340        let backfill: Vec<(u64, String)> = match entity {
1341            StoredIndexEntity::Node => self
1342                .iter_nodes()
1343                .filter(|(_, node)| node.labels().strs().any(|l| l == label))
1344                .filter_map(|(id, node)| match node.properties().get(property) {
1345                    Some(crate::ValueRef::String(value)) => Some((id, value.to_owned())),
1346                    _ => None,
1347                })
1348                .collect(),
1349            StoredIndexEntity::Relationship => self
1350                .iter_rels()
1351                .filter(|(_, rel)| rel.rel_type() == label)
1352                .filter_map(|(id, rel)| match rel.properties().get(property) {
1353                    Some(crate::ValueRef::String(value)) => Some((id, value.to_owned())),
1354                    _ => None,
1355                })
1356                .collect(),
1357        };
1358
1359        let mut registry = self.text_indexes_write(entity);
1360        for (id, value) in backfill {
1361            registry.insert(label, property, id, &value);
1362        }
1363    }
1364
1365    /// Drop a (label, property) text scope, decrementing the refcount.
1366    pub(super) fn deactivate_text_scope(
1367        &self,
1368        entity: StoredIndexEntity,
1369        label: &str,
1370        property: &str,
1371    ) {
1372        self.text_indexes_write(entity)
1373            .remove_scope(label, property);
1374    }
1375
1376    fn activate_fulltext_index(
1377        &self,
1378        entity: StoredIndexEntity,
1379        name: &str,
1380        labels: &[String],
1381        properties: &[String],
1382    ) {
1383        use super::fulltext_index::{term_counts_for_properties, TermCounts};
1384
1385        {
1386            let mut registry = self.fulltext_indexes_write(entity);
1387            registry.register(name.to_string(), labels.to_vec(), properties.to_vec());
1388        }
1389        self.indexes
1390            .active_fulltext_indexes
1391            .fetch_add(1, Ordering::Relaxed);
1392
1393        // Backfill: walk every entity matching any label, tokenise covered
1394        // string properties, install one posting batch per entity.
1395        let backfill: Vec<(u64, TermCounts)> = match entity {
1396            StoredIndexEntity::Node => self
1397                .iter_nodes()
1398                .filter(|(_, node)| {
1399                    labels
1400                        .iter()
1401                        .any(|wanted| node.labels().strs().any(|l| l == wanted))
1402                })
1403                .map(|(id, node)| {
1404                    let counts = term_counts_for_properties(node.properties(), properties);
1405                    (id, counts)
1406                })
1407                .filter(|(_, c)| !c.is_empty())
1408                .collect(),
1409            StoredIndexEntity::Relationship => self
1410                .iter_rels()
1411                .filter(|(_, rel)| labels.iter().any(|wanted| wanted == rel.rel_type()))
1412                .map(|(id, rel)| {
1413                    let counts = term_counts_for_properties(rel.properties(), properties);
1414                    (id, counts)
1415                })
1416                .filter(|(_, c)| !c.is_empty())
1417                .collect(),
1418        };
1419
1420        let mut registry = self.fulltext_indexes_write(entity);
1421        if let Some(index) = registry.get_mut(name) {
1422            for (id, counts) in backfill {
1423                index.reindex_entity(id, counts);
1424            }
1425        }
1426    }
1427
1428    pub(super) fn deactivate_fulltext_index(&self, entity: StoredIndexEntity, name: &str) {
1429        self.fulltext_indexes_write(entity).deregister(name);
1430        self.indexes
1431            .active_fulltext_indexes
1432            .fetch_sub(1, Ordering::Relaxed);
1433    }
1434
1435    pub(super) fn sorted_indexes_read(
1436        &self,
1437        entity: StoredIndexEntity,
1438    ) -> IndexRead<'_, SortedPropertyIndex> {
1439        self.indexes.sorted.read(entity)
1440    }
1441
1442    pub(super) fn sorted_indexes_write(
1443        &self,
1444        entity: StoredIndexEntity,
1445    ) -> IndexWrite<'_, SortedPropertyIndex> {
1446        self.indexes.sorted.write(entity)
1447    }
1448
1449    fn activate_sorted_scope(&self, entity: StoredIndexEntity, label: &str, property: &str) {
1450        if !self.sorted_indexes_write(entity).add_scope(label, property) {
1451            return;
1452        }
1453
1454        // Inserted while scanning: collecting every value first held a
1455        // second copy of the whole column until the index was built.
1456        let mut registry = self.sorted_indexes_write(entity);
1457        match entity {
1458            StoredIndexEntity::Node => {
1459                for (id, node) in self.iter_nodes() {
1460                    if !node.labels().strs().any(|l| l == label) {
1461                        continue;
1462                    }
1463                    if let Some(value) = node.properties().get(property) {
1464                        registry.insert(label, property, id, &value.to_owned());
1465                    }
1466                }
1467            }
1468            StoredIndexEntity::Relationship => {
1469                for (id, rel) in self.iter_rels() {
1470                    if rel.rel_type() != label {
1471                        continue;
1472                    }
1473                    if let Some(value) = rel.properties().get(property) {
1474                        registry.insert(label, property, id, &value.to_owned());
1475                    }
1476                }
1477            }
1478        }
1479    }
1480
1481    pub(super) fn deactivate_sorted_scope(
1482        &self,
1483        entity: StoredIndexEntity,
1484        label: &str,
1485        property: &str,
1486    ) {
1487        self.sorted_indexes_write(entity)
1488            .remove_scope(label, property);
1489    }
1490
1491    pub(super) fn point_indexes_read(
1492        &self,
1493        entity: StoredIndexEntity,
1494    ) -> IndexRead<'_, PointRegistry> {
1495        self.indexes.point.read(entity)
1496    }
1497
1498    pub(super) fn point_indexes_write(
1499        &self,
1500        entity: StoredIndexEntity,
1501    ) -> IndexWrite<'_, PointRegistry> {
1502        self.indexes.point.write(entity)
1503    }
1504
1505    fn activate_point_scope(
1506        &self,
1507        entity: StoredIndexEntity,
1508        label: &str,
1509        property: &str,
1510        cell_size: Option<f64>,
1511    ) {
1512        if !self
1513            .point_indexes_write(entity)
1514            .add_scope(label, property, cell_size)
1515        {
1516            return;
1517        }
1518
1519        let backfill: Vec<(u64, LoraPoint)> = match entity {
1520            StoredIndexEntity::Node => self
1521                .iter_nodes()
1522                .filter(|(_, node)| node.labels().strs().any(|l| l == label))
1523                .filter_map(|(id, node)| {
1524                    match node.properties().get(property).map(|v| v.to_owned()) {
1525                        Some(PropertyValue::Point(point)) => Some((id, point)),
1526                        _ => None,
1527                    }
1528                })
1529                .collect(),
1530            StoredIndexEntity::Relationship => self
1531                .iter_rels()
1532                .filter(|(_, rel)| rel.rel_type() == label)
1533                .filter_map(|(id, rel)| {
1534                    match rel.properties().get(property).map(|v| v.to_owned()) {
1535                        Some(PropertyValue::Point(point)) => Some((id, point)),
1536                        _ => None,
1537                    }
1538                })
1539                .collect(),
1540        };
1541
1542        let mut registry = self.point_indexes_write(entity);
1543        for (id, point) in backfill {
1544            registry.insert(label, property, id, point);
1545        }
1546    }
1547
1548    pub(super) fn deactivate_point_scope(
1549        &self,
1550        entity: StoredIndexEntity,
1551        label: &str,
1552        property: &str,
1553    ) {
1554        self.point_indexes_write(entity)
1555            .remove_scope(label, property);
1556    }
1557
1558    pub(super) fn vector_indexes_read(
1559        &self,
1560        entity: StoredIndexEntity,
1561    ) -> IndexRead<'_, VectorIndexRegistry> {
1562        self.indexes.vector.read(entity)
1563    }
1564
1565    pub(super) fn vector_indexes_write(
1566        &self,
1567        entity: StoredIndexEntity,
1568    ) -> IndexWrite<'_, VectorIndexRegistry> {
1569        self.indexes.vector.write(entity)
1570    }
1571
1572    #[allow(clippy::too_many_arguments)]
1573    fn activate_vector_index(
1574        &self,
1575        entity: StoredIndexEntity,
1576        name: &str,
1577        label: &str,
1578        property: &str,
1579        similarity: VectorSimilarity,
1580        provider: VectorIndexProvider,
1581        hnsw: HnswParams,
1582        lazy: bool,
1583    ) {
1584        {
1585            let mut registry = self.vector_indexes_write(entity);
1586            registry.register(
1587                name.to_string(),
1588                label.to_string(),
1589                property.to_string(),
1590                similarity,
1591                provider,
1592                hnsw,
1593            );
1594        }
1595
1596        if lazy {
1597            // Skip the initial backfill — the catalog state is flipped
1598            // to Populating by the caller and the first query routed
1599            // to this index triggers `lazy_populate_vector_index`.
1600            // Mutations between CREATE and first query still feed the
1601            // registry via the maintenance hook, so the lazy phase
1602            // only handles vectors that existed before CREATE.
1603            return;
1604        }
1605
1606        self.backfill_vector_index(entity, label, property);
1607    }
1608
1609    /// Walk the property store for vectors matching this index's
1610    /// `(label, property)` scope and replay them into the registry.
1611    /// Shared by sync CREATE and lazy-populate flows.
1612    fn backfill_vector_index(&self, entity: StoredIndexEntity, label: &str, property: &str) {
1613        let backfill: Vec<(u64, crate::LoraVector)> = match entity {
1614            StoredIndexEntity::Node => self
1615                .iter_nodes()
1616                .filter(|(_, node)| node.labels().strs().any(|l| l == label))
1617                .filter_map(|(id, node)| {
1618                    match node.properties().get(property).map(|v| v.to_owned()) {
1619                        Some(PropertyValue::Vector(v)) => Some((id, v)),
1620                        _ => None,
1621                    }
1622                })
1623                .collect(),
1624            StoredIndexEntity::Relationship => self
1625                .iter_rels()
1626                .filter(|(_, rel)| rel.rel_type() == label)
1627                .filter_map(|(id, rel)| {
1628                    match rel.properties().get(property).map(|v| v.to_owned()) {
1629                        Some(PropertyValue::Vector(v)) => Some((id, v)),
1630                        _ => None,
1631                    }
1632                })
1633                .collect(),
1634        };
1635
1636        let mut registry = self.vector_indexes_write(entity);
1637        for (id, vector) in backfill {
1638            registry.insert_for(label, property, id, &vector);
1639        }
1640    }
1641
1642    /// Lazy-populate a `Populating` vector index: backfill from the
1643    /// property store, then flip catalog state to `Online`. Called
1644    /// from `GraphStorage::vector_search` on the first request that
1645    /// hits a still-populating index. Idempotent: a second concurrent
1646    /// caller finds the state already `Online` and does no work.
1647    pub(super) fn lazy_populate_vector_index(&self, name: &str) {
1648        let def = match self.index_catalog_read().get(name).cloned() {
1649            Some(d) => d,
1650            None => return,
1651        };
1652        if def.state != StoredIndexState::Populating || def.kind != StoredIndexKind::Vector {
1653            return;
1654        }
1655        let label = match def.label.as_deref() {
1656            Some(l) => l,
1657            None => return,
1658        };
1659        let property = match def.properties.first() {
1660            Some(p) => p.as_str(),
1661            None => return,
1662        };
1663        self.backfill_vector_index(def.entity, label, property);
1664        self.index_catalog_write()
1665            .set_state(name, StoredIndexState::Online);
1666    }
1667
1668    pub(super) fn deactivate_vector_index(&self, entity: StoredIndexEntity, name: &str) {
1669        self.vector_indexes_write(entity).deregister(name);
1670    }
1671
1672    /// Snapshot of cardinality stats. Cheap: derived from already-tracked
1673    /// `nodes_by_label` / `relationships_by_type` lengths and the
1674    /// distinct-value sketches (a cached estimate per (scope, key), and
1675    /// the previous map reused while those don't change). The cost model
1676    /// uses this to populate `estimated_rows` on plan-tree nodes.
1677    pub fn graph_stats(&self) -> GraphStats {
1678        let mut stats = GraphStats {
1679            node_count: self.live_node_count,
1680            relationship_count: self.live_rel_count,
1681            ..Default::default()
1682        };
1683        for (label, ids) in &self.nodes_by_label {
1684            stats.nodes_by_label.insert(label.clone(), ids.len());
1685        }
1686        for (rel_type, ids) in &self.relationships_by_type {
1687            stats
1688                .relationships_by_type
1689                .insert(rel_type.clone(), ids.len());
1690        }
1691        // Distinct values per (label / type, property): estimated from
1692        // the distinct-value sketches, which exist for every key and are
1693        // a function of the live data alone. Deliberately *not* the
1694        // exact bucket counts of active hash indexes: which keys are
1695        // active depends on lookup history and on whether the process
1696        // restarted, and plans must not.
1697        stats.node_distinct_values = self.distinct_stats.nodes.distinct_values();
1698        stats.relationship_distinct_values = self.distinct_stats.relationships.distinct_values();
1699
1700        for def in self.index_catalog_read().list() {
1701            if def.state != StoredIndexState::Online {
1702                continue;
1703            }
1704            let Some(label) = def.label else {
1705                continue;
1706            };
1707            for property in def.properties {
1708                let scope = (label.clone(), property);
1709                match (def.entity, def.kind) {
1710                    (StoredIndexEntity::Node, StoredIndexKind::Range) => {
1711                        stats.node_range_indexes.insert(scope);
1712                    }
1713                    (StoredIndexEntity::Node, StoredIndexKind::Text) => {
1714                        stats.node_text_indexes.insert(scope);
1715                    }
1716                    (StoredIndexEntity::Node, StoredIndexKind::Point) => {
1717                        stats.node_point_indexes.insert(scope);
1718                    }
1719                    (StoredIndexEntity::Relationship, StoredIndexKind::Range) => {
1720                        stats.relationship_range_indexes.insert(scope);
1721                    }
1722                    (StoredIndexEntity::Relationship, StoredIndexKind::Text) => {
1723                        stats.relationship_text_indexes.insert(scope);
1724                    }
1725                    (StoredIndexEntity::Relationship, StoredIndexKind::Point) => {
1726                        stats.relationship_point_indexes.insert(scope);
1727                    }
1728                    (StoredIndexEntity::Node, StoredIndexKind::Vector) => {
1729                        stats.node_vector_indexes.insert(scope);
1730                    }
1731                    (StoredIndexEntity::Relationship, StoredIndexKind::Vector) => {
1732                        stats.relationship_vector_indexes.insert(scope);
1733                    }
1734                    (_, StoredIndexKind::Lookup | StoredIndexKind::Fulltext) => {}
1735                }
1736            }
1737        }
1738        stats
1739    }
1740
1741    /// Approximate retained-heap breakdown of this graph. See
1742    /// [`super::MemoryReport`] for the methodology and per-component
1743    /// fields. Intended for benches and the `mem_probe*` examples;
1744    /// not on a hot path.
1745    pub fn memory_estimate(&self) -> super::MemoryReport {
1746        super::mem_report::estimate(self)
1747    }
1748
1749    /// Count (or uncount) every property of an entity under each of its
1750    /// scopes in the distinct-value sketches.
1751    pub(super) fn count_distinct_values<'a>(
1752        stats: &mut super::distinct_stats::DistinctStats,
1753        scopes: impl IntoIterator<Item = &'a str>,
1754        properties: &Properties,
1755        add: bool,
1756    ) {
1757        if properties.is_empty() {
1758            return;
1759        }
1760        for scope in scopes {
1761            if add {
1762                stats.add_all(scope, properties);
1763            } else {
1764                for (key, value) in properties {
1765                    stats.remove(scope, key, value);
1766                }
1767            }
1768        }
1769    }
1770
1771    pub(super) fn on_node_created(&mut self, node: &NodeRecord) {
1772        for label in &node.labels {
1773            self.insert_node_label_index(node.id, label);
1774        }
1775        Self::count_distinct_values(
1776            &mut self.distinct_stats.nodes,
1777            node.labels.strs(),
1778            &node.properties,
1779            true,
1780        );
1781        self.index_node_properties_if_active(node.id, node.labels.strs(), &node.properties);
1782        self.maintain_node_secondary_indexes(node, SecondaryIndexMutation::Insert);
1783    }
1784
1785    pub(super) fn on_node_property_set(
1786        &mut self,
1787        node_id: NodeId,
1788        key: &str,
1789        old: Option<&PropertyValue>,
1790        new: &PropertyValue,
1791    ) {
1792        let Some(labels) = self.node_at(node_id).map(|node| node.labels().to_owned()) else {
1793            return;
1794        };
1795        for label in &labels {
1796            self.distinct_stats.nodes.replace(label, key, old, new);
1797        }
1798
1799        if self.node_property_index_is_active(key) {
1800            if let Some(old) = old {
1801                self.unindex_node_property_if_active(node_id, labels.strs(), key, old);
1802            }
1803            self.index_node_property_if_active(node_id, labels.strs(), key, new);
1804        }
1805
1806        self.update_secondary_property(
1807            StoredIndexEntity::Node,
1808            labels.strs(),
1809            node_id,
1810            key,
1811            old,
1812            Some(new),
1813        );
1814    }
1815
1816    pub(super) fn on_node_property_removed(
1817        &mut self,
1818        node_id: NodeId,
1819        key: &str,
1820        old: &PropertyValue,
1821    ) {
1822        let Some(labels) = self.node_at(node_id).map(|node| node.labels().to_owned()) else {
1823            return;
1824        };
1825        for label in &labels {
1826            self.distinct_stats.nodes.remove(label, key, old);
1827        }
1828        if self.node_property_index_is_active(key) {
1829            self.unindex_node_property_if_active(node_id, labels.strs(), key, old);
1830        }
1831        self.update_secondary_property(
1832            StoredIndexEntity::Node,
1833            labels.strs(),
1834            node_id,
1835            key,
1836            Some(old),
1837            None,
1838        );
1839    }
1840
1841    pub(super) fn on_node_label_added(&mut self, node_id: NodeId, label: &str) {
1842        self.insert_node_label_index(node_id, label);
1843
1844        let Some(properties) = self
1845            .node_at(node_id)
1846            .map(|node| node.properties().to_owned())
1847        else {
1848            return;
1849        };
1850        Self::count_distinct_values(&mut self.distinct_stats.nodes, [label], &properties, true);
1851        if self.active_node_property_index_count() != 0 {
1852            self.index_node_scope_properties_if_active(node_id, label, &properties);
1853            // Its first label: it was indexed as a node without any.
1854            if self.node_at(node_id).is_some_and(|n| n.labels().len() == 1) {
1855                self.unindex_node_scope_properties_if_active(node_id, UNLABELLED, &properties);
1856            }
1857        }
1858        for (key, value) in &properties {
1859            self.update_secondary_property(
1860                StoredIndexEntity::Node,
1861                [label],
1862                node_id,
1863                key,
1864                None,
1865                Some(value),
1866            );
1867        }
1868    }
1869
1870    pub(super) fn on_node_label_removed(&mut self, node_id: NodeId, label: &str) {
1871        self.remove_node_label_index(node_id, label);
1872
1873        let Some(properties) = self
1874            .node_at(node_id)
1875            .map(|node| node.properties().to_owned())
1876        else {
1877            return;
1878        };
1879        Self::count_distinct_values(&mut self.distinct_stats.nodes, [label], &properties, false);
1880        if self.active_node_property_index_count() != 0 {
1881            self.unindex_node_scope_properties_if_active(node_id, label, &properties);
1882            // Its last label: index it as a node without any.
1883            if self.node_at(node_id).is_some_and(|n| n.labels().is_empty()) {
1884                self.index_node_scope_properties_if_active(node_id, UNLABELLED, &properties);
1885            }
1886        }
1887        for (key, value) in &properties {
1888            self.update_secondary_property(
1889                StoredIndexEntity::Node,
1890                [label],
1891                node_id,
1892                key,
1893                Some(value),
1894                None,
1895            );
1896        }
1897    }
1898
1899    pub(super) fn on_node_deleted(&mut self, node: &NodeRecord) {
1900        for label in &node.labels {
1901            self.remove_node_label_index(node.id, label);
1902        }
1903        Self::count_distinct_values(
1904            &mut self.distinct_stats.nodes,
1905            node.labels.strs(),
1906            &node.properties,
1907            false,
1908        );
1909        self.unindex_active_node_properties(node.id, node.labels.strs(), &node.properties);
1910        self.maintain_node_secondary_indexes(node, SecondaryIndexMutation::Remove);
1911    }
1912
1913    pub(super) fn on_relationship_created(&mut self, rel: &RelationshipRecord) {
1914        self.attach_relationship(rel);
1915        Self::count_distinct_values(
1916            &mut self.distinct_stats.relationships,
1917            [rel.rel_type.as_str()],
1918            &rel.properties,
1919            true,
1920        );
1921        self.index_relationship_properties_if_active(
1922            rel.id,
1923            [rel.rel_type.as_str()],
1924            &rel.properties,
1925        );
1926        self.maintain_relationship_secondary_indexes(rel, SecondaryIndexMutation::Insert);
1927    }
1928
1929    pub(super) fn on_relationship_property_set(
1930        &mut self,
1931        rel_id: RelationshipId,
1932        key: &str,
1933        old: Option<&PropertyValue>,
1934        new: &PropertyValue,
1935    ) {
1936        let Some(rel_type) = self.rel_at(rel_id).map(|rel| rel.type_name().clone()) else {
1937            return;
1938        };
1939        self.distinct_stats
1940            .relationships
1941            .replace(&rel_type, key, old, new);
1942
1943        if self.relationship_property_index_is_active(key) {
1944            if let Some(old) = old {
1945                self.unindex_relationship_property_if_active(rel_id, [rel_type.as_str()], key, old);
1946            }
1947            self.index_relationship_property_if_active(rel_id, [rel_type.as_str()], key, new);
1948        }
1949
1950        self.update_secondary_property(
1951            StoredIndexEntity::Relationship,
1952            [rel_type.as_str()],
1953            rel_id,
1954            key,
1955            old,
1956            Some(new),
1957        );
1958    }
1959
1960    pub(super) fn on_relationship_property_removed(
1961        &mut self,
1962        rel_id: RelationshipId,
1963        key: &str,
1964        old: &PropertyValue,
1965    ) {
1966        let Some(rel_type) = self.rel_at(rel_id).map(|rel| rel.type_name().clone()) else {
1967            return;
1968        };
1969        self.distinct_stats
1970            .relationships
1971            .remove(&rel_type, key, old);
1972        if self.relationship_property_index_is_active(key) {
1973            self.unindex_relationship_property_if_active(rel_id, [rel_type.as_str()], key, old);
1974        }
1975        self.update_secondary_property(
1976            StoredIndexEntity::Relationship,
1977            [rel_type.as_str()],
1978            rel_id,
1979            key,
1980            Some(old),
1981            None,
1982        );
1983    }
1984
1985    pub(super) fn on_relationship_deleted(&mut self, rel: &RelationshipRecord) {
1986        self.detach_relationship_indexes(rel);
1987        Self::count_distinct_values(
1988            &mut self.distinct_stats.relationships,
1989            [rel.rel_type.as_str()],
1990            &rel.properties,
1991            false,
1992        );
1993        self.unindex_active_relationship_properties(
1994            rel.id,
1995            [rel.rel_type.as_str()],
1996            &rel.properties,
1997        );
1998        self.maintain_relationship_secondary_indexes(rel, SecondaryIndexMutation::Remove);
1999    }
2000
2001    fn index_node_property_if_active<'a>(
2002        &mut self,
2003        node_id: NodeId,
2004        labels: impl IntoIterator<Item = &'a str>,
2005        key: &str,
2006        value: &PropertyValue,
2007    ) {
2008        if self.active_node_property_index_count() == 0 {
2009            return;
2010        }
2011        let indexes = self.indexes_mut();
2012        if indexes.node_properties.is_active(key) {
2013            indexes
2014                .node_properties
2015                .insert_with_scopes(node_id, labels, key, value);
2016        }
2017    }
2018
2019    fn index_node_properties_if_active<'a>(
2020        &mut self,
2021        node_id: NodeId,
2022        labels: impl IntoIterator<Item = &'a str> + Clone,
2023        properties: &Properties,
2024    ) {
2025        if self.active_node_property_index_count() == 0 {
2026            return;
2027        }
2028        let indexes = self.indexes_mut();
2029        for (key, value) in properties {
2030            if indexes.node_properties.is_active(key) {
2031                indexes
2032                    .node_properties
2033                    .insert_with_scopes(node_id, labels.clone(), key, value);
2034            }
2035        }
2036    }
2037
2038    fn unindex_node_property_if_active<'a>(
2039        &mut self,
2040        node_id: NodeId,
2041        labels: impl IntoIterator<Item = &'a str>,
2042        key: &str,
2043        value: &PropertyValue,
2044    ) {
2045        if self.active_node_property_index_count() == 0 {
2046            return;
2047        }
2048        let indexes = self.indexes_mut();
2049        if indexes.node_properties.is_active(key) {
2050            indexes
2051                .node_properties
2052                .remove_with_scopes(node_id, labels, key, value);
2053        }
2054    }
2055
2056    fn index_node_scope_properties_if_active(
2057        &mut self,
2058        node_id: NodeId,
2059        scope: &str,
2060        properties: &Properties,
2061    ) {
2062        if self.active_node_property_index_count() == 0 {
2063            return;
2064        }
2065        let indexes = self.indexes_mut();
2066        for (key, value) in properties {
2067            if indexes.node_properties.is_active(key) {
2068                indexes
2069                    .node_properties
2070                    .insert_scoped(node_id, scope, key, value);
2071            }
2072        }
2073    }
2074
2075    fn unindex_node_scope_properties_if_active(
2076        &mut self,
2077        node_id: NodeId,
2078        scope: &str,
2079        properties: &Properties,
2080    ) {
2081        if self.active_node_property_index_count() == 0 {
2082            return;
2083        }
2084        let indexes = self.indexes_mut();
2085        for (key, value) in properties {
2086            if indexes.node_properties.is_active(key) {
2087                indexes
2088                    .node_properties
2089                    .remove_scoped(node_id, scope, key, value);
2090            }
2091        }
2092    }
2093
2094    fn unindex_active_node_properties<'a>(
2095        &mut self,
2096        node_id: NodeId,
2097        labels: impl IntoIterator<Item = &'a str> + Clone,
2098        properties: &Properties,
2099    ) {
2100        if self.active_node_property_index_count() == 0 {
2101            return;
2102        }
2103        let indexes = self.indexes_mut();
2104        for (key, value) in properties {
2105            if indexes.node_properties.is_active(key) {
2106                indexes
2107                    .node_properties
2108                    .remove_with_scopes(node_id, labels.clone(), key, value);
2109            }
2110        }
2111    }
2112
2113    fn index_relationship_property_if_active<'a>(
2114        &mut self,
2115        rel_id: RelationshipId,
2116        scopes: impl IntoIterator<Item = &'a str>,
2117        key: &str,
2118        value: &PropertyValue,
2119    ) {
2120        if self.active_relationship_property_index_count() == 0 {
2121            return;
2122        }
2123        let indexes = self.indexes_mut();
2124        if indexes.relationship_properties.is_active(key) {
2125            indexes
2126                .relationship_properties
2127                .insert_with_scopes(rel_id, scopes, key, value);
2128        }
2129    }
2130
2131    fn index_relationship_properties_if_active<'a>(
2132        &mut self,
2133        rel_id: RelationshipId,
2134        scopes: impl IntoIterator<Item = &'a str> + Clone,
2135        properties: &Properties,
2136    ) {
2137        if self.active_relationship_property_index_count() == 0 {
2138            return;
2139        }
2140        let indexes = self.indexes_mut();
2141        for (key, value) in properties {
2142            if indexes.relationship_properties.is_active(key) {
2143                indexes.relationship_properties.insert_with_scopes(
2144                    rel_id,
2145                    scopes.clone(),
2146                    key,
2147                    value,
2148                );
2149            }
2150        }
2151    }
2152
2153    fn unindex_relationship_property_if_active<'a>(
2154        &mut self,
2155        rel_id: RelationshipId,
2156        scopes: impl IntoIterator<Item = &'a str>,
2157        key: &str,
2158        value: &PropertyValue,
2159    ) {
2160        if self.active_relationship_property_index_count() == 0 {
2161            return;
2162        }
2163        let indexes = self.indexes_mut();
2164        if indexes.relationship_properties.is_active(key) {
2165            indexes
2166                .relationship_properties
2167                .remove_with_scopes(rel_id, scopes, key, value);
2168        }
2169    }
2170
2171    fn unindex_active_relationship_properties<'a>(
2172        &mut self,
2173        rel_id: RelationshipId,
2174        scopes: impl IntoIterator<Item = &'a str> + Clone,
2175        properties: &Properties,
2176    ) {
2177        if self.active_relationship_property_index_count() == 0 {
2178            return;
2179        }
2180        let indexes = self.indexes_mut();
2181        for (key, value) in properties {
2182            if indexes.relationship_properties.is_active(key) {
2183                indexes.relationship_properties.remove_with_scopes(
2184                    rel_id,
2185                    scopes.clone(),
2186                    key,
2187                    value,
2188                );
2189            }
2190        }
2191    }
2192
2193    pub(super) fn scan_nodes_by_property(
2194        &self,
2195        label: Option<&str>,
2196        key: &str,
2197        value: &PropertyValue,
2198    ) -> Vec<NodeRecord> {
2199        match label {
2200            Some(label) => self
2201                .nodes_by_label
2202                .get(label)
2203                .into_iter()
2204                .flat_map(|ids| ids.iter())
2205                .filter_map(|id| self.node_at(id))
2206                .filter(|node| node.properties().get(key).is_some_and(|v| v == *value))
2207                .map(|node| node.to_record())
2208                .collect(),
2209            None => self
2210                .iter_node_refs()
2211                .filter(|node| node.properties().get(key).is_some_and(|v| v == *value))
2212                .map(|node| node.to_record())
2213                .collect(),
2214        }
2215    }
2216
2217    pub(super) fn scan_node_ids_by_property(
2218        &self,
2219        label: Option<&str>,
2220        key: &str,
2221        value: &PropertyValue,
2222    ) -> Vec<NodeId> {
2223        match label {
2224            Some(label) => self
2225                .nodes_by_label
2226                .get(label)
2227                .into_iter()
2228                .flat_map(|ids| ids.iter())
2229                .filter_map(|id| {
2230                    (self
2231                        .node_at(id)?
2232                        .properties()
2233                        .get(key)
2234                        .is_some_and(|v| v == *value))
2235                    .then_some(id)
2236                })
2237                .collect(),
2238            None => self
2239                .iter_nodes()
2240                .filter_map(|(id, node)| {
2241                    (node.properties().get(key).is_some_and(|v| v == *value)).then_some(id)
2242                })
2243                .collect(),
2244        }
2245    }
2246
2247    pub(super) fn any_node_by_property(
2248        &self,
2249        label: &str,
2250        key: &str,
2251        value: &PropertyValue,
2252    ) -> bool {
2253        self.nodes_by_label
2254            .get(label)
2255            .into_iter()
2256            .flat_map(|ids| ids.iter())
2257            .filter_map(|id| self.node_at(id))
2258            .any(|node| node.properties().get(key).is_some_and(|v| v == *value))
2259    }
2260
2261    pub(super) fn scan_relationships_by_property(
2262        &self,
2263        rel_type: Option<&str>,
2264        key: &str,
2265        value: &PropertyValue,
2266    ) -> Vec<RelationshipRecord> {
2267        match rel_type {
2268            Some(rel_type) => self
2269                .relationships_by_type
2270                .get(rel_type)
2271                .into_iter()
2272                .flat_map(|ids| ids.iter())
2273                .filter_map(|id| self.rel_at(id))
2274                .filter(|rel| rel.properties().get(key).is_some_and(|v| v == *value))
2275                .map(|rel| rel.to_record())
2276                .collect(),
2277            None => self
2278                .iter_rel_refs()
2279                .filter(|rel| rel.properties().get(key).is_some_and(|v| v == *value))
2280                .map(|rel| rel.to_record())
2281                .collect(),
2282        }
2283    }
2284
2285    pub(super) fn scan_relationship_ids_by_property(
2286        &self,
2287        rel_type: Option<&str>,
2288        key: &str,
2289        value: &PropertyValue,
2290    ) -> Vec<RelationshipId> {
2291        match rel_type {
2292            Some(rel_type) => self
2293                .relationships_by_type
2294                .get(rel_type)
2295                .into_iter()
2296                .flat_map(|ids| ids.iter())
2297                .filter_map(|id| {
2298                    (self
2299                        .rel_at(id)?
2300                        .properties()
2301                        .get(key)
2302                        .is_some_and(|v| v == *value))
2303                    .then_some(id)
2304                })
2305                .collect(),
2306            None => self
2307                .iter_rels()
2308                .filter_map(|(id, rel)| {
2309                    (rel.properties().get(key).is_some_and(|v| v == *value)).then_some(id)
2310                })
2311                .collect(),
2312        }
2313    }
2314
2315    pub(super) fn any_relationship_by_property(
2316        &self,
2317        rel_type: &str,
2318        key: &str,
2319        value: &PropertyValue,
2320    ) -> bool {
2321        self.relationships_by_type
2322            .get(rel_type)
2323            .into_iter()
2324            .flat_map(|ids| ids.iter())
2325            .filter_map(|id| self.rel_at(id))
2326            .any(|rel| rel.properties().get(key).is_some_and(|v| v == *value))
2327    }
2328
2329    pub(super) fn attach_relationship(&mut self, rel: &RelationshipRecord) {
2330        let type_id = self.dicts.types.id_or_insert(&rel.rel_type);
2331        self.adjacency_push(
2332            rel.src,
2333            Side::Out,
2334            AdjEntry {
2335                type_id,
2336                neighbour: rel.dst,
2337                rel: rel.id,
2338            },
2339        );
2340        self.adjacency_push(
2341            rel.dst,
2342            Side::In,
2343            AdjEntry {
2344                type_id,
2345                neighbour: rel.src,
2346                rel: rel.id,
2347            },
2348        );
2349        self.insert_relationship_type_index(rel.id, &rel.rel_type);
2350    }
2351
2352    fn detach_relationship_indexes(&mut self, rel: &RelationshipRecord) {
2353        self.adjacency_remove(rel.src, Side::Out, rel.id);
2354        self.adjacency_remove(rel.dst, Side::In, rel.id);
2355
2356        self.remove_relationship_type_index(rel.id, &rel.rel_type);
2357    }
2358
2359    pub(super) fn relationship_ids_for_direction(
2360        &self,
2361        node_id: NodeId,
2362        direction: Direction,
2363    ) -> Vec<RelationshipId> {
2364        let mut ids = Vec::new();
2365        let _ = self.try_for_each_adjacent_id_unchecked(node_id, direction, &[], |rel_id, _| {
2366            ids.push(rel_id);
2367            Ok::<(), ()>(())
2368        });
2369        ids
2370    }
2371
2372    pub(super) fn has_incident_relationships(&self, node_id: NodeId) -> bool {
2373        self.outgoing_at(node_id).is_some_and(|adj| !adj.is_empty())
2374            || self.incoming_at(node_id).is_some_and(|adj| !adj.is_empty())
2375    }
2376
2377    pub(super) fn incident_relationship_ids(&self, node_id: NodeId) -> Vec<RelationshipId> {
2378        self.relationship_ids_for_direction(node_id, Direction::Undirected)
2379    }
2380
2381    /// Replay a node creation using the id captured in a durable mutation
2382    /// event. This intentionally does not emit a new mutation event: callers
2383    /// must invoke it before installing a recorder on the graph.
2384    #[doc(hidden)]
2385    pub fn replay_create_node(
2386        &mut self,
2387        id: NodeId,
2388        labels: Vec<String>,
2389        properties: Properties,
2390    ) -> Result<NodeRecord, String> {
2391        if self.recorder.is_some() {
2392            return Err(
2393                "cannot replay node creation while a mutation recorder is installed".into(),
2394            );
2395        }
2396        if self.has_node_at(id) {
2397            return Err(format!("node id {id} already exists"));
2398        }
2399        let idx = self.ensure_node_slot_checked(id)?;
2400        self.bump_next_node_id_past(id)?;
2401
2402        let labels = Self::normalize_labels(labels);
2403        let node = NodeRecord {
2404            id,
2405            labels,
2406            properties,
2407        };
2408
2409        self.put_node_at_slot(idx, &node);
2410        // Same index maintenance as a live create: only hash indexes that
2411        // are already active (declared by a replayed CREATE INDEX /
2412        // CREATE CONSTRAINT, which backfills from the data replayed so
2413        // far) are kept current. Lookup-activated (implicit) indexes are
2414        // rebuilt lazily on first use, exactly as in a fresh process.
2415        self.on_node_created(&node);
2416
2417        Ok(node)
2418    }
2419
2420    /// Replay a relationship creation using the id captured in a durable
2421    /// mutation event. This intentionally does not emit a new mutation event:
2422    /// callers must invoke it before installing a recorder on the graph.
2423    #[doc(hidden)]
2424    pub fn replay_create_relationship(
2425        &mut self,
2426        id: RelationshipId,
2427        src: NodeId,
2428        dst: NodeId,
2429        rel_type: &str,
2430        properties: Properties,
2431    ) -> Result<RelationshipRecord, String> {
2432        if self.recorder.is_some() {
2433            return Err(
2434                "cannot replay relationship creation while a mutation recorder is installed".into(),
2435            );
2436        }
2437        if self.has_rel_at(id) {
2438            return Err(format!("relationship id {id} already exists"));
2439        }
2440        if !self.has_node_at(src) {
2441            return Err(format!(
2442                "relationship {id} references missing source node {src}"
2443            ));
2444        }
2445        if !self.has_node_at(dst) {
2446            return Err(format!(
2447                "relationship {id} references missing target node {dst}"
2448            ));
2449        }
2450
2451        let trimmed = rel_type.trim();
2452        if trimmed.is_empty() {
2453            return Err(format!("relationship {id} has an empty type"));
2454        }
2455        let idx = self.ensure_rel_slot_checked(id)?;
2456        self.bump_next_rel_id_past(id)?;
2457
2458        let rel = RelationshipRecord {
2459            id,
2460            src,
2461            dst,
2462            rel_type: trimmed.into(),
2463            properties,
2464        };
2465
2466        self.put_rel_at_slot(idx, &rel);
2467        // See `replay_create_node`: active (declared) indexes only.
2468        self.on_relationship_created(&rel);
2469
2470        Ok(rel)
2471    }
2472
2473    #[cfg(test)]
2474    pub(super) fn assert_property_indexes_match_scan(&self) {
2475        let indexes = self.indexes_read();
2476        assert_eq!(
2477            indexes.node_properties.active_keys.len(),
2478            self.active_node_property_index_count(),
2479            "node property index counter diverged from active key set"
2480        );
2481        assert_eq!(
2482            indexes.relationship_properties.active_keys.len(),
2483            self.active_relationship_property_index_count(),
2484            "relationship property index counter diverged from active key set"
2485        );
2486
2487        let mut expected_nodes = PropertyIndexState {
2488            active_keys: indexes.node_properties.active_keys.clone(),
2489            any_scope_keys: indexes.node_properties.any_scope_keys.clone(),
2490            ..PropertyIndexState::default()
2491        };
2492        for (id, node) in self.iter_nodes() {
2493            for (key, value) in node.properties() {
2494                if expected_nodes.is_active(key) {
2495                    expected_nodes.insert_with_scopes(
2496                        id,
2497                        node.labels().strs(),
2498                        key,
2499                        &value.to_owned(),
2500                    );
2501                }
2502            }
2503        }
2504        assert_eq!(
2505            indexes.node_properties.scoped_values, expected_nodes.scoped_values,
2506            "node property scoped index values diverged from scan"
2507        );
2508        assert_eq!(
2509            indexes.node_properties.any_scope, expected_nodes.any_scope,
2510            "node property across-scopes index values diverged from scan"
2511        );
2512
2513        let mut expected_relationships = PropertyIndexState {
2514            active_keys: indexes.relationship_properties.active_keys.clone(),
2515            any_scope_keys: indexes.relationship_properties.any_scope_keys.clone(),
2516            ..PropertyIndexState::default()
2517        };
2518        for (id, rel) in self.iter_rels() {
2519            for (key, value) in rel.properties() {
2520                if expected_relationships.is_active(key) {
2521                    expected_relationships.insert_with_scopes(
2522                        id,
2523                        [rel.rel_type()],
2524                        key,
2525                        &value.to_owned(),
2526                    );
2527                }
2528            }
2529        }
2530        assert_eq!(
2531            indexes.relationship_properties.scoped_values, expected_relationships.scoped_values,
2532            "relationship property scoped index values diverged from scan"
2533        );
2534        assert_eq!(
2535            indexes.relationship_properties.any_scope, expected_relationships.any_scope,
2536            "relationship property across-scopes index values diverged from scan"
2537        );
2538    }
2539}
2540
2541/// Set or remove one property of the record in `slab` slot `idx`,
2542/// rewriting only that entry. The previous value; `None` when the slot is
2543/// empty or there was nothing to remove.
2544#[inline]
2545fn edit_record_property(
2546    slab: &mut RecordSlab,
2547    idx: usize,
2548    kind: encoded::RecordKind,
2549    keys: &mut crate::dict::NameDict,
2550    key: &str,
2551    edit: encoded::PropEdit<'_>,
2552) -> Option<Option<PropertyValue>> {
2553    let bytes = slab.get(idx)?;
2554    let mut out = Scratch::take();
2555    let old = encoded::edit_property(bytes, kind, keys, key, edit, &mut out)?;
2556    slab.set(idx, &out);
2557    Some(old)
2558}