Skip to main content

lora_store/memory/
graph.rs

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