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