Skip to main content

lora_store/memory/
graph.rs

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