Skip to main content

hawdb_core/
schema.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::{LogicalType, Value};
16use std::collections::BTreeMap;
17use std::sync::Arc;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct LabelId(pub u32);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub struct RelTypeId(pub u32);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct TableId(pub u32);
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Label {
30    pub id: LabelId,
31    pub name: String,
32}
33
34/// The lookup directory shares the immutable symbol rather than allocating a
35/// second name. Ordering and borrowed lookup retain their string semantics.
36#[derive(Debug, Clone)]
37struct LabelNameKey(Arc<Label>);
38
39impl std::borrow::Borrow<str> for LabelNameKey {
40    fn borrow(&self) -> &str {
41        &self.0.name
42    }
43}
44
45impl PartialEq for LabelNameKey {
46    fn eq(&self, other: &Self) -> bool {
47        self.0.name == other.0.name
48    }
49}
50
51impl Eq for LabelNameKey {}
52
53impl PartialOrd for LabelNameKey {
54    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
55        Some(self.cmp(other))
56    }
57}
58
59impl Ord for LabelNameKey {
60    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
61        self.0.name.cmp(&other.0.name)
62    }
63}
64
65/// Admission token for a retained immutable symbol. Standalone callers remain
66/// responsible for their allocation policy and complete backing capacity.
67#[doc(hidden)]
68pub trait RetainedLabelAllocation {
69    fn bytes(&self) -> usize;
70}
71
72/// Own one admitted immutable symbol without retaining the catalog directory.
73#[doc(hidden)]
74#[derive(Debug)]
75pub struct RetainedLabelName<A> {
76    label: Arc<Label>,
77    _allocation: A,
78}
79
80impl<A> RetainedLabelName<A> {
81    pub fn id(&self) -> LabelId {
82        self.label.id
83    }
84
85    pub fn name(&self) -> &str {
86        &self.label.name
87    }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct RelType {
92    pub id: RelTypeId,
93    pub name: String,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
97pub struct IndexId(pub u32);
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
100pub struct ConstraintId(pub u32);
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
103pub struct PropertyId(pub u32);
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
106pub enum IndexKind {
107    Equality,
108    Range,
109    FullText,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
113pub enum ConstraintKind {
114    NodePropertyUnique,
115    NodePropertyExists,
116    RelationshipPropertyUnique,
117    RelationshipPropertyExists,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
121pub enum ConstraintSubject {
122    Node(LabelId),
123    Relationship(RelTypeId),
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
127pub enum TableKind {
128    Node,
129    Relationship,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum SchemaObjectState {
134    DeleteOnly,
135    WriteOnly,
136    Backfill,
137    Validating,
138    Public,
139    Gc,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
143pub enum PropertyType {
144    Any,
145    Bool,
146    Int,
147    Float,
148    /// Application string eligible for statistics; exposed as VARCHAR-like DDL.
149    String,
150    /// Unbounded text payload. Optimizer value statistics intentionally skip it.
151    Text,
152    List,
153    Vector(crate::VectorDimension),
154}
155
156impl PropertyType {
157    pub const fn logical_type(self) -> LogicalType {
158        match self {
159            Self::Any => LogicalType::Any,
160            Self::Bool => LogicalType::Boolean,
161            Self::Int => LogicalType::Int64,
162            Self::Float => LogicalType::Float64,
163            Self::String => LogicalType::String,
164            Self::Text => LogicalType::Text,
165            Self::List => LogicalType::List,
166            Self::Vector(dimension) => LogicalType::Vector(dimension),
167        }
168    }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct IndexDescriptor {
173    pub id: IndexId,
174    pub label_id: LabelId,
175    pub property: String,
176    pub kind: IndexKind,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct CompositeIndexDescriptor {
181    pub id: IndexId,
182    pub label_id: LabelId,
183    pub properties: Vec<String>,
184}
185
186/// A compact, payload-free sample for one explicit property index.
187///
188/// `index_size`, `unique_values`, and `sample_size` describe one coherent
189/// sampling epoch. Mutations after that epoch only advance
190/// `updates_since_sample`; they never rewrite the sampled counters in place.
191#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
192pub struct IndexStatisticsSample {
193    pub index_size: u64,
194    pub unique_values: u64,
195    pub sample_size: u64,
196    pub updates_since_sample: u64,
197}
198
199impl IndexStatisticsSample {
200    pub const STALE_UPDATE_PERCENT: u64 = 5;
201
202    pub fn exact(index_size: u64, unique_values: u64) -> Self {
203        Self {
204            index_size,
205            unique_values,
206            sample_size: index_size,
207            updates_since_sample: 0,
208        }
209    }
210
211    pub fn is_valid(self) -> bool {
212        self.sample_size <= self.index_size
213            && self.unique_values <= self.sample_size
214            && (self.sample_size != 0 || self.unique_values == 0)
215    }
216
217    pub fn max_fresh_updates(self) -> u64 {
218        self.index_size
219            .saturating_mul(Self::STALE_UPDATE_PERCENT)
220            .div_ceil(100)
221            .max(1)
222    }
223
224    pub fn is_stale(self) -> bool {
225        self.updates_since_sample > self.max_fresh_updates()
226    }
227
228    pub fn estimated_unique_values(self) -> Option<u64> {
229        if !self.is_valid() || self.is_stale() || self.sample_size == 0 {
230            return None;
231        }
232        Some(
233            self.unique_values
234                .saturating_mul(self.index_size)
235                .div_ceil(self.sample_size)
236                .clamp(1, self.index_size.max(1)),
237        )
238    }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct ConstraintDescriptor {
243    pub id: ConstraintId,
244    pub subject: ConstraintSubject,
245    pub property: String,
246    pub kind: ConstraintKind,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct TableDescriptor {
251    pub id: TableId,
252    pub name: String,
253    pub kind: TableKind,
254    pub state: SchemaObjectState,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct PropertyDescriptor {
259    pub id: PropertyId,
260    pub table_id: TableId,
261    pub name: String,
262    pub value_type: PropertyType,
263    pub nullable: bool,
264    pub state: SchemaObjectState,
265}
266
267#[derive(Debug, Default, Clone, PartialEq, Eq)]
268pub struct BasicGraphStatistics {
269    pub computed_at_commit_epoch: u64,
270    pub node_count: u64,
271    pub relationship_count: u64,
272    pub label_counts: BTreeMap<LabelId, u64>,
273    pub rel_type_counts: BTreeMap<RelTypeId, u64>,
274}
275
276/// Whether a complete advanced-statistics snapshot describes the current graph epoch.
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub enum AdvancedStatisticsFreshness {
279    /// The snapshot is complete and was computed at the current graph epoch.
280    Fresh,
281    /// A complete snapshot exists, but it describes a different graph epoch.
282    Stale,
283    /// No complete advanced-statistics snapshot is available.
284    Unavailable,
285}
286
287impl AdvancedStatisticsFreshness {
288    pub const fn as_str(self) -> &'static str {
289        match self {
290            Self::Fresh => "fresh",
291            Self::Stale => "stale",
292            Self::Unavailable => "unavailable",
293        }
294    }
295}
296
297#[derive(Debug, Default, Clone, PartialEq, Eq)]
298pub struct GraphStatistics {
299    pub computed_at_commit_epoch: u64,
300    pub advanced_statistics_complete: bool,
301    pub histogram_sample_limit: usize,
302    pub node_count: u64,
303    pub relationship_count: u64,
304    pub label_counts: BTreeMap<LabelId, u64>,
305    pub rel_type_counts: BTreeMap<RelTypeId, u64>,
306    pub rel_type_source_counts: BTreeMap<RelTypeId, u64>,
307    pub rel_type_target_counts: BTreeMap<RelTypeId, u64>,
308    pub path_counts: BTreeMap<(LabelId, RelTypeId, LabelId), u64>,
309    pub path_source_distinct_counts: BTreeMap<(LabelId, RelTypeId, LabelId), u64>,
310    pub path_target_distinct_counts: BTreeMap<(LabelId, RelTypeId, LabelId), u64>,
311    pub bounded_path_counts: BTreeMap<(LabelId, RelTypeId, LabelId, usize), u64>,
312    pub bounded_path_source_distinct_counts: BTreeMap<(LabelId, RelTypeId, LabelId, usize), u64>,
313    pub bounded_path_target_distinct_counts: BTreeMap<(LabelId, RelTypeId, LabelId, usize), u64>,
314    pub index_samples: BTreeMap<IndexId, IndexStatisticsSample>,
315    pub property_distinct_counts: BTreeMap<(LabelId, String), u64>,
316    pub rel_property_distinct_counts: BTreeMap<(RelTypeId, String), u64>,
317    pub property_histograms: BTreeMap<(LabelId, String), Vec<Value>>,
318    pub rel_property_histograms: BTreeMap<(RelTypeId, String), Vec<Value>>,
319    pub sampled_property_histograms: BTreeMap<(LabelId, String), bool>,
320    pub sampled_rel_property_histograms: BTreeMap<(RelTypeId, String), bool>,
321}
322
323impl GraphStatistics {
324    /// Classifies only advanced statistics; basic counts may be newer than this snapshot.
325    pub fn advanced_statistics_freshness(
326        &self,
327        current_commit_epoch: u64,
328    ) -> AdvancedStatisticsFreshness {
329        if !self.advanced_statistics_complete {
330            AdvancedStatisticsFreshness::Unavailable
331        } else if self.computed_at_commit_epoch == current_commit_epoch {
332            AdvancedStatisticsFreshness::Fresh
333        } else {
334            AdvancedStatisticsFreshness::Stale
335        }
336    }
337
338    /// Returns the observable lag without underflowing on an invalid future snapshot epoch.
339    pub fn advanced_statistics_commit_lag(&self, current_commit_epoch: u64) -> u64 {
340        current_commit_epoch.saturating_sub(self.computed_at_commit_epoch)
341    }
342}
343
344mod snapshot;
345#[cfg(test)]
346mod snapshot_tests;
347
348#[derive(Debug, Default, Clone, PartialEq, Eq)]
349pub struct Catalog {
350    labels_by_name: snapshot::CatalogSnapshot<BTreeMap<LabelNameKey, LabelId>>,
351    labels: snapshot::CatalogSnapshot<Vec<Arc<Label>>>,
352    rel_types_by_name: snapshot::CatalogSnapshot<BTreeMap<String, RelTypeId>>,
353    rel_types: snapshot::CatalogSnapshot<Vec<RelType>>,
354    tables_by_key: snapshot::CatalogSnapshot<BTreeMap<(TableKind, String), TableId>>,
355    tables: snapshot::CatalogSnapshot<Vec<TableDescriptor>>,
356    properties_by_key: snapshot::CatalogSnapshot<BTreeMap<(TableId, String), PropertyId>>,
357    properties: snapshot::CatalogSnapshot<Vec<PropertyDescriptor>>,
358    property_indexes_by_key:
359        snapshot::CatalogSnapshot<BTreeMap<(LabelId, String, IndexKind), IndexId>>,
360    property_indexes: snapshot::CatalogSnapshot<Vec<IndexDescriptor>>,
361    composite_property_indexes_by_key:
362        snapshot::CatalogSnapshot<BTreeMap<(LabelId, Vec<String>), IndexId>>,
363    composite_property_indexes: snapshot::CatalogSnapshot<Vec<CompositeIndexDescriptor>>,
364    constraints_by_key: snapshot::CatalogSnapshot<
365        BTreeMap<(ConstraintSubject, String, ConstraintKind), ConstraintId>,
366    >,
367    constraints: snapshot::CatalogSnapshot<Vec<ConstraintDescriptor>>,
368}
369
370impl Catalog {
371    pub fn is_empty(&self) -> bool {
372        self.labels_by_name.is_empty()
373            && self.rel_types_by_name.is_empty()
374            && self.tables_by_key.is_empty()
375            && self.properties_by_key.is_empty()
376            && self.property_indexes_by_key.is_empty()
377            && self.composite_property_indexes_by_key.is_empty()
378            && self.constraints_by_key.is_empty()
379    }
380
381    pub fn get_or_create_label(&mut self, name: &str) -> LabelId {
382        if let Some(id) = self.labels_by_name.get(name) {
383            return *id;
384        }
385        let id = LabelId(self.labels.len() as u32);
386        let label = Arc::new(Label {
387            id,
388            name: name.to_string(),
389        });
390        self.labels_by_name
391            .insert(LabelNameKey(Arc::clone(&label)), id);
392        self.labels.push(label);
393        id
394    }
395
396    pub fn label_id(&self, name: &str) -> Option<LabelId> {
397        self.labels_by_name.get(name).copied()
398    }
399
400    pub fn label_name(&self, id: LabelId) -> Option<&str> {
401        self.labels
402            .get(id.0 as usize)
403            .map(|label| label.name.as_str())
404    }
405
406    pub fn labels(&self) -> impl Iterator<Item = &Label> {
407        self.labels.iter().map(Arc::as_ref)
408    }
409
410    /// Complete immutable symbol backing capacity, observed without capture.
411    #[doc(hidden)]
412    pub fn retained_label_name_capacity_bytes(&self, id: LabelId) -> crate::Result<Option<usize>> {
413        self.labels
414            .get(id.0 as usize)
415            .map(|label| Self::label_name_capacity_bytes(label))
416            .transpose()
417    }
418
419    fn label_name_capacity_bytes(label: &Label) -> crate::Result<usize> {
420        std::mem::size_of::<Label>()
421            .checked_add(2 * std::mem::size_of::<usize>())
422            .and_then(|bytes| bytes.checked_add(label.name.capacity()))
423            .ok_or_else(|| crate::HawDBError::Execution("label capacity overflow".into()))
424    }
425
426    /// Reserve a label's complete name capacity before retaining its immutable
427    /// owner. Missing IDs do not invoke admission; host refusals stay typed.
428    /// Container and adapter metadata require separate caller admission.
429    #[doc(hidden)]
430    pub fn retain_label_name_with_admission<A: RetainedLabelAllocation, E>(
431        &self,
432        id: LabelId,
433        allocate: impl FnOnce(usize) -> std::result::Result<A, E>,
434    ) -> std::result::Result<Option<RetainedLabelName<A>>, E>
435    where
436        E: From<crate::HawDBError>,
437    {
438        let Some(label) = self.labels.get(id.0 as usize) else {
439            return Ok(None);
440        };
441        let bytes = Self::label_name_capacity_bytes(label)?;
442        let allocation = allocate(bytes)?;
443        if allocation.bytes() < bytes {
444            return Err(crate::HawDBError::Execution(
445                "retained label admission returned an insufficient allocation permit".into(),
446            )
447            .into());
448        }
449        Ok(Some(RetainedLabelName {
450            label: Arc::clone(label),
451            _allocation: allocation,
452        }))
453    }
454
455    pub fn get_or_create_rel_type(&mut self, name: &str) -> RelTypeId {
456        if let Some(id) = self.rel_types_by_name.get(name) {
457            return *id;
458        }
459        let id = RelTypeId(self.rel_types.len() as u32);
460        self.rel_types.push(RelType {
461            id,
462            name: name.to_string(),
463        });
464        self.rel_types_by_name.insert(name.to_string(), id);
465        id
466    }
467
468    pub fn rel_type_id(&self, name: &str) -> Option<RelTypeId> {
469        self.rel_types_by_name.get(name).copied()
470    }
471
472    pub fn rel_type_name(&self, id: RelTypeId) -> Option<&str> {
473        self.rel_types
474            .get(id.0 as usize)
475            .map(|rel_type| rel_type.name.as_str())
476    }
477
478    pub fn rel_types(&self) -> impl Iterator<Item = &RelType> {
479        self.rel_types.iter()
480    }
481
482    pub fn get_or_create_table(&mut self, kind: TableKind, name: &str) -> TableId {
483        let key = (kind, name.to_string());
484        if let Some(id) = self.tables_by_key.get(&key) {
485            return *id;
486        }
487        let id = TableId(self.tables.len() as u32);
488        self.tables.push(TableDescriptor {
489            id,
490            name: name.to_string(),
491            kind,
492            state: SchemaObjectState::Public,
493        });
494        self.tables_by_key.insert(key, id);
495        id
496    }
497
498    pub fn table_id(&self, kind: TableKind, name: &str) -> Option<TableId> {
499        self.tables_by_key.get(&(kind, name.to_string())).copied()
500    }
501
502    pub fn table_descriptor(&self, id: TableId) -> Option<&TableDescriptor> {
503        self.tables
504            .get(id.0 as usize)
505            .filter(|table| !table.name.is_empty())
506    }
507
508    pub fn set_table_state(&mut self, id: TableId, state: SchemaObjectState) -> bool {
509        let Some(table) = self.tables.get_mut(id.0 as usize) else {
510            return false;
511        };
512        if table.name.is_empty() {
513            return false;
514        }
515        table.state = state;
516        true
517    }
518
519    pub fn remove_table_descriptor(&mut self, id: TableId) -> bool {
520        let Some(table) = self.tables.get_mut(id.0 as usize) else {
521            return false;
522        };
523        if table.name.is_empty() {
524            return false;
525        }
526        let key = (table.kind, table.name.clone());
527        table.name.clear();
528        self.tables_by_key.remove(&key);
529
530        let property_ids = self
531            .properties
532            .iter()
533            .filter(|property| property.table_id == id && !property.name.is_empty())
534            .map(|property| property.id)
535            .collect::<Vec<_>>();
536        for property_id in property_ids {
537            self.remove_property_descriptor(property_id);
538        }
539        true
540    }
541
542    pub fn import_table(
543        &mut self,
544        id: TableId,
545        kind: TableKind,
546        name: String,
547        state: SchemaObjectState,
548    ) {
549        let index = id.0 as usize;
550        while self.tables.len() <= index {
551            let next = TableId(self.tables.len() as u32);
552            self.tables.push(TableDescriptor {
553                id: next,
554                name: String::new(),
555                kind: TableKind::Node,
556                state: SchemaObjectState::Public,
557            });
558        }
559        self.tables[index] = TableDescriptor {
560            id,
561            name: name.clone(),
562            kind,
563            state,
564        };
565        self.tables_by_key.insert((kind, name), id);
566    }
567
568    pub fn table_descriptors(&self) -> impl Iterator<Item = &TableDescriptor> {
569        self.tables.iter().filter(|table| !table.name.is_empty())
570    }
571
572    pub fn get_or_create_property(
573        &mut self,
574        table_id: TableId,
575        name: &str,
576        value_type: PropertyType,
577        nullable: bool,
578    ) -> PropertyId {
579        let key = (table_id, name.to_string());
580        if let Some(id) = self.properties_by_key.get(&key) {
581            return *id;
582        }
583        let id = PropertyId(self.properties.len() as u32);
584        self.properties.push(PropertyDescriptor {
585            id,
586            table_id,
587            name: name.to_string(),
588            value_type,
589            nullable,
590            state: SchemaObjectState::Public,
591        });
592        self.properties_by_key.insert(key, id);
593        id
594    }
595
596    pub fn property_descriptor_id(&self, table_id: TableId, name: &str) -> Option<PropertyId> {
597        self.properties_by_key
598            .get(&(table_id, name.to_string()))
599            .copied()
600    }
601
602    pub fn import_property_descriptor(
603        &mut self,
604        id: PropertyId,
605        table_id: TableId,
606        name: String,
607        value_type: PropertyType,
608        nullable: bool,
609        state: SchemaObjectState,
610    ) {
611        let index = id.0 as usize;
612        while self.properties.len() <= index {
613            let next = PropertyId(self.properties.len() as u32);
614            self.properties.push(PropertyDescriptor {
615                id: next,
616                table_id: TableId(0),
617                name: String::new(),
618                value_type: PropertyType::Any,
619                nullable: true,
620                state: SchemaObjectState::Public,
621            });
622        }
623        self.properties[index] = PropertyDescriptor {
624            id,
625            table_id,
626            name: name.clone(),
627            value_type,
628            nullable,
629            state,
630        };
631        self.properties_by_key.insert((table_id, name), id);
632    }
633
634    pub fn property_descriptors(&self) -> impl Iterator<Item = &PropertyDescriptor> {
635        self.properties
636            .iter()
637            .filter(|property| !property.name.is_empty())
638    }
639
640    pub fn property_descriptor(&self, id: PropertyId) -> Option<&PropertyDescriptor> {
641        self.properties
642            .get(id.0 as usize)
643            .filter(|property| !property.name.is_empty())
644    }
645
646    pub fn set_property_state(&mut self, id: PropertyId, state: SchemaObjectState) -> bool {
647        let Some(property) = self.properties.get_mut(id.0 as usize) else {
648            return false;
649        };
650        if property.name.is_empty() {
651            return false;
652        }
653        property.state = state;
654        true
655    }
656
657    pub fn remove_property_descriptor(&mut self, id: PropertyId) -> bool {
658        let Some(property) = self.properties.get_mut(id.0 as usize) else {
659            return false;
660        };
661        if property.name.is_empty() {
662            return false;
663        }
664        let key = (property.table_id, property.name.clone());
665        property.name.clear();
666        self.properties_by_key.remove(&key);
667        true
668    }
669
670    pub fn get_or_create_property_index(&mut self, label_id: LabelId, property: &str) -> IndexId {
671        self.get_or_create_property_index_with_kind(label_id, property, IndexKind::Equality)
672    }
673
674    pub fn get_or_create_property_index_with_kind(
675        &mut self,
676        label_id: LabelId,
677        property: &str,
678        kind: IndexKind,
679    ) -> IndexId {
680        let key = (label_id, property.to_string());
681        let key = (key.0, key.1, kind);
682        if let Some(id) = self.property_indexes_by_key.get(&key) {
683            return *id;
684        }
685        let id = self.next_index_id();
686        self.property_indexes.push(IndexDescriptor {
687            id,
688            label_id,
689            property: property.to_string(),
690            kind,
691        });
692        self.property_indexes_by_key.insert(key, id);
693        id
694    }
695
696    pub fn property_index_id(&self, label_id: LabelId, property: &str) -> Option<IndexId> {
697        self.property_index_id_with_kind(label_id, property, IndexKind::Equality)
698    }
699
700    pub fn property_index_id_with_kind(
701        &self,
702        label_id: LabelId,
703        property: &str,
704        kind: IndexKind,
705    ) -> Option<IndexId> {
706        self.property_indexes_by_key
707            .get(&(label_id, property.to_string(), kind))
708            .copied()
709    }
710
711    pub fn import_property_index(&mut self, id: IndexId, label_id: LabelId, property: String) {
712        self.import_property_index_with_kind(id, label_id, property, IndexKind::Equality);
713    }
714
715    pub fn import_property_index_with_kind(
716        &mut self,
717        id: IndexId,
718        label_id: LabelId,
719        property: String,
720        kind: IndexKind,
721    ) {
722        let index = id.0 as usize;
723        while self.property_indexes.len() <= index {
724            let next = IndexId(self.property_indexes.len() as u32);
725            self.property_indexes.push(IndexDescriptor {
726                id: next,
727                label_id: LabelId(0),
728                property: String::new(),
729                kind: IndexKind::Equality,
730            });
731        }
732        self.property_indexes[index] = IndexDescriptor {
733            id,
734            label_id,
735            property: property.clone(),
736            kind,
737        };
738        self.property_indexes_by_key
739            .insert((label_id, property, kind), id);
740    }
741
742    pub fn property_indexes(&self) -> impl Iterator<Item = &IndexDescriptor> {
743        self.property_indexes
744            .iter()
745            .filter(|index| !index.property.is_empty())
746    }
747
748    pub fn property_index_descriptor(&self, id: IndexId) -> Option<&IndexDescriptor> {
749        self.property_indexes().find(|index| index.id == id)
750    }
751
752    pub fn get_or_create_composite_property_index(
753        &mut self,
754        label_id: LabelId,
755        properties: &[String],
756    ) -> IndexId {
757        let key = (label_id, properties.to_vec());
758        if let Some(id) = self.composite_property_indexes_by_key.get(&key) {
759            return *id;
760        }
761        let id = self.next_index_id();
762        self.composite_property_indexes
763            .push(CompositeIndexDescriptor {
764                id,
765                label_id,
766                properties: properties.to_vec(),
767            });
768        self.composite_property_indexes_by_key.insert(key, id);
769        id
770    }
771
772    pub fn composite_property_index_id(
773        &self,
774        label_id: LabelId,
775        properties: &[String],
776    ) -> Option<IndexId> {
777        self.composite_property_indexes_by_key
778            .get(&(label_id, properties.to_vec()))
779            .copied()
780    }
781
782    pub fn import_composite_property_index(
783        &mut self,
784        id: IndexId,
785        label_id: LabelId,
786        properties: Vec<String>,
787    ) {
788        self.composite_property_indexes
789            .push(CompositeIndexDescriptor {
790                id,
791                label_id,
792                properties: properties.clone(),
793            });
794        self.composite_property_indexes_by_key
795            .insert((label_id, properties), id);
796    }
797
798    pub fn composite_property_indexes(&self) -> impl Iterator<Item = &CompositeIndexDescriptor> {
799        self.composite_property_indexes
800            .iter()
801            .filter(|index| !index.properties.is_empty())
802    }
803
804    pub fn composite_property_index_descriptor(
805        &self,
806        id: IndexId,
807    ) -> Option<&CompositeIndexDescriptor> {
808        self.composite_property_indexes()
809            .find(|index| index.id == id)
810    }
811
812    pub fn has_scalar_property_index(&self, label_id: LabelId, property: &str) -> bool {
813        [IndexKind::Equality, IndexKind::Range]
814            .into_iter()
815            .any(|kind| {
816                self.property_index_id_with_kind(label_id, property, kind)
817                    .is_some()
818            })
819    }
820
821    pub fn supports_index_statistics(&self, id: IndexId) -> bool {
822        match (
823            self.property_index_descriptor(id),
824            self.composite_property_index_descriptor(id),
825        ) {
826            (Some(index), None) => index.kind != IndexKind::FullText,
827            (None, Some(_)) => true,
828            _ => false,
829        }
830    }
831
832    fn next_index_id(&self) -> IndexId {
833        let next = self
834            .property_indexes()
835            .map(|index| index.id.0)
836            .chain(self.composite_property_indexes().map(|index| index.id.0))
837            .max()
838            .map_or(0, |id| {
839                id.checked_add(1)
840                    .expect("schema index identifier space is exhausted")
841            });
842        IndexId(next)
843    }
844
845    pub fn get_or_create_unique_constraint(
846        &mut self,
847        label_id: LabelId,
848        property: &str,
849    ) -> ConstraintId {
850        self.get_or_create_constraint(
851            ConstraintSubject::Node(label_id),
852            property,
853            ConstraintKind::NodePropertyUnique,
854        )
855    }
856
857    pub fn unique_constraint_id(&self, label_id: LabelId, property: &str) -> Option<ConstraintId> {
858        self.constraint_id(
859            ConstraintSubject::Node(label_id),
860            property,
861            ConstraintKind::NodePropertyUnique,
862        )
863    }
864
865    pub fn get_or_create_node_property_exists_constraint(
866        &mut self,
867        label_id: LabelId,
868        property: &str,
869    ) -> ConstraintId {
870        self.get_or_create_constraint(
871            ConstraintSubject::Node(label_id),
872            property,
873            ConstraintKind::NodePropertyExists,
874        )
875    }
876
877    pub fn node_property_exists_constraint_id(
878        &self,
879        label_id: LabelId,
880        property: &str,
881    ) -> Option<ConstraintId> {
882        self.constraint_id(
883            ConstraintSubject::Node(label_id),
884            property,
885            ConstraintKind::NodePropertyExists,
886        )
887    }
888
889    pub fn get_or_create_relationship_property_exists_constraint(
890        &mut self,
891        rel_type_id: RelTypeId,
892        property: &str,
893    ) -> ConstraintId {
894        self.get_or_create_constraint(
895            ConstraintSubject::Relationship(rel_type_id),
896            property,
897            ConstraintKind::RelationshipPropertyExists,
898        )
899    }
900
901    pub fn relationship_property_exists_constraint_id(
902        &self,
903        rel_type_id: RelTypeId,
904        property: &str,
905    ) -> Option<ConstraintId> {
906        self.constraint_id(
907            ConstraintSubject::Relationship(rel_type_id),
908            property,
909            ConstraintKind::RelationshipPropertyExists,
910        )
911    }
912
913    pub fn get_or_create_relationship_unique_constraint(
914        &mut self,
915        rel_type_id: RelTypeId,
916        property: &str,
917    ) -> ConstraintId {
918        self.get_or_create_constraint(
919            ConstraintSubject::Relationship(rel_type_id),
920            property,
921            ConstraintKind::RelationshipPropertyUnique,
922        )
923    }
924
925    pub fn relationship_unique_constraint_id(
926        &self,
927        rel_type_id: RelTypeId,
928        property: &str,
929    ) -> Option<ConstraintId> {
930        self.constraint_id(
931            ConstraintSubject::Relationship(rel_type_id),
932            property,
933            ConstraintKind::RelationshipPropertyUnique,
934        )
935    }
936
937    pub fn import_unique_constraint(
938        &mut self,
939        id: ConstraintId,
940        label_id: LabelId,
941        property: String,
942    ) {
943        self.import_constraint(
944            id,
945            ConstraintSubject::Node(label_id),
946            property,
947            ConstraintKind::NodePropertyUnique,
948        );
949    }
950
951    pub fn import_node_property_exists_constraint(
952        &mut self,
953        id: ConstraintId,
954        label_id: LabelId,
955        property: String,
956    ) {
957        self.import_constraint(
958            id,
959            ConstraintSubject::Node(label_id),
960            property,
961            ConstraintKind::NodePropertyExists,
962        );
963    }
964
965    pub fn import_relationship_property_exists_constraint(
966        &mut self,
967        id: ConstraintId,
968        rel_type_id: RelTypeId,
969        property: String,
970    ) {
971        self.import_constraint(
972            id,
973            ConstraintSubject::Relationship(rel_type_id),
974            property,
975            ConstraintKind::RelationshipPropertyExists,
976        );
977    }
978
979    pub fn import_relationship_unique_constraint(
980        &mut self,
981        id: ConstraintId,
982        rel_type_id: RelTypeId,
983        property: String,
984    ) {
985        self.import_constraint(
986            id,
987            ConstraintSubject::Relationship(rel_type_id),
988            property,
989            ConstraintKind::RelationshipPropertyUnique,
990        );
991    }
992
993    fn get_or_create_constraint(
994        &mut self,
995        subject: ConstraintSubject,
996        property: &str,
997        kind: ConstraintKind,
998    ) -> ConstraintId {
999        let key = (subject, property.to_string(), kind);
1000        if let Some(id) = self.constraints_by_key.get(&key) {
1001            return *id;
1002        }
1003        let id = ConstraintId(self.constraints.len() as u32);
1004        self.constraints.push(ConstraintDescriptor {
1005            id,
1006            subject,
1007            property: property.to_string(),
1008            kind,
1009        });
1010        self.constraints_by_key.insert(key, id);
1011        id
1012    }
1013
1014    fn constraint_id(
1015        &self,
1016        subject: ConstraintSubject,
1017        property: &str,
1018        kind: ConstraintKind,
1019    ) -> Option<ConstraintId> {
1020        self.constraints_by_key
1021            .get(&(subject, property.to_string(), kind))
1022            .copied()
1023    }
1024
1025    fn import_constraint(
1026        &mut self,
1027        id: ConstraintId,
1028        subject: ConstraintSubject,
1029        property: String,
1030        kind: ConstraintKind,
1031    ) {
1032        let index = id.0 as usize;
1033        while self.constraints.len() <= index {
1034            let next = ConstraintId(self.constraints.len() as u32);
1035            self.constraints.push(ConstraintDescriptor {
1036                id: next,
1037                subject,
1038                property: String::new(),
1039                kind,
1040            });
1041        }
1042        self.constraints[index] = ConstraintDescriptor {
1043            id,
1044            subject,
1045            property: property.clone(),
1046            kind,
1047        };
1048        self.constraints_by_key
1049            .insert((subject, property, kind), id);
1050    }
1051
1052    pub fn unique_constraints(&self) -> impl Iterator<Item = &ConstraintDescriptor> {
1053        self.constraints.iter().filter(|constraint| {
1054            !constraint.property.is_empty() && constraint.kind == ConstraintKind::NodePropertyUnique
1055        })
1056    }
1057
1058    pub fn node_property_exists_constraints(&self) -> impl Iterator<Item = &ConstraintDescriptor> {
1059        self.constraints.iter().filter(|constraint| {
1060            !constraint.property.is_empty() && constraint.kind == ConstraintKind::NodePropertyExists
1061        })
1062    }
1063
1064    pub fn relationship_property_exists_constraints(
1065        &self,
1066    ) -> impl Iterator<Item = &ConstraintDescriptor> {
1067        self.constraints.iter().filter(|constraint| {
1068            !constraint.property.is_empty()
1069                && constraint.kind == ConstraintKind::RelationshipPropertyExists
1070        })
1071    }
1072
1073    pub fn relationship_unique_constraints(&self) -> impl Iterator<Item = &ConstraintDescriptor> {
1074        self.constraints.iter().filter(|constraint| {
1075            !constraint.property.is_empty()
1076                && constraint.kind == ConstraintKind::RelationshipPropertyUnique
1077        })
1078    }
1079
1080    pub fn import_label(&mut self, id: LabelId, name: String) {
1081        let index = id.0 as usize;
1082        while self.labels.len() <= index {
1083            let next = LabelId(self.labels.len() as u32);
1084            self.labels.push(Arc::new(Label {
1085                id: next,
1086                name: String::new(),
1087            }));
1088        }
1089        let previous = self.labels[index].name.as_str();
1090        if self.labels_by_name.get(previous).copied() == Some(id) {
1091            self.labels_by_name.remove(previous);
1092        }
1093        self.labels_by_name.remove(name.as_str());
1094        let label = Arc::new(Label { id, name });
1095        self.labels_by_name
1096            .insert(LabelNameKey(Arc::clone(&label)), id);
1097        self.labels[index] = label;
1098    }
1099
1100    pub fn import_rel_type(&mut self, id: RelTypeId, name: String) {
1101        let index = id.0 as usize;
1102        while self.rel_types.len() <= index {
1103            let next = RelTypeId(self.rel_types.len() as u32);
1104            self.rel_types.push(RelType {
1105                id: next,
1106                name: String::new(),
1107            });
1108        }
1109        self.rel_types[index] = RelType {
1110            id,
1111            name: name.clone(),
1112        };
1113        self.rel_types_by_name.insert(name, id);
1114    }
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119    use super::*;
1120
1121    #[derive(Debug)]
1122    struct LabelAllocation {
1123        bytes: usize,
1124        source_on_release: Option<std::sync::Weak<Label>>,
1125    }
1126
1127    impl RetainedLabelAllocation for LabelAllocation {
1128        fn bytes(&self) -> usize {
1129            self.bytes
1130        }
1131    }
1132
1133    impl Drop for LabelAllocation {
1134        fn drop(&mut self) {
1135            if let Some(source) = &self.source_on_release {
1136                assert_eq!(source.strong_count(), 0, "symbol must drop before permit");
1137            }
1138        }
1139    }
1140
1141    #[test]
1142    fn retained_label_name_preserves_source_and_snapshot_without_catalog_pin() {
1143        let mut catalog = Catalog::default();
1144        let id = LabelId(0);
1145        let mut name = String::with_capacity(16 * 1024);
1146        name.push_str("original");
1147        catalog.import_label(id, name);
1148        let other = catalog.get_or_create_label("unrelated");
1149        let capacity = catalog.labels[id.0 as usize].name.capacity();
1150        let snapshot = catalog.clone();
1151        assert_eq!(
1152            snapshot.label_name(id).unwrap().as_ptr(),
1153            catalog.label_name(id).unwrap().as_ptr()
1154        );
1155        drop(snapshot);
1156        let original = Arc::downgrade(&catalog.labels[id.0 as usize]);
1157        let unrelated = Arc::downgrade(&catalog.labels[other.0 as usize]);
1158        let address = catalog.label_name(id).unwrap().as_ptr() as usize;
1159        let preflight = catalog
1160            .retained_label_name_capacity_bytes(id)
1161            .unwrap()
1162            .unwrap();
1163        let source = catalog
1164            .retain_label_name_with_admission(id, |bytes| -> crate::Result<_> {
1165                assert_eq!(original.strong_count(), 2);
1166                assert_eq!(bytes, preflight);
1167                assert!(
1168                    bytes
1169                        >= std::mem::size_of::<Label>()
1170                            + 2 * std::mem::size_of::<usize>()
1171                            + capacity
1172                );
1173                Ok(LabelAllocation {
1174                    bytes,
1175                    source_on_release: Some(original.clone()),
1176                })
1177            })
1178            .unwrap()
1179            .unwrap();
1180        catalog.import_label(id, "replacement".into());
1181        assert_eq!(catalog.label_name(id), Some("replacement"));
1182        assert_eq!(catalog.label_id("original"), None);
1183        assert_eq!(catalog.label_id("replacement"), Some(id));
1184        assert_eq!(
1185            original.strong_count(),
1186            1,
1187            "only the retained name survives"
1188        );
1189        drop(catalog);
1190        assert_eq!(unrelated.strong_count(), 0);
1191        assert_eq!(source.id(), id);
1192        assert_eq!(source.name(), "original");
1193        assert_eq!(source.name().as_ptr() as usize, address);
1194        drop(source);
1195        assert_eq!(original.strong_count(), 0);
1196    }
1197
1198    #[test]
1199    fn import_label_removes_only_its_own_previous_name_mapping() {
1200        let mut catalog = Catalog::default();
1201        let original = catalog.get_or_create_label("original");
1202        let snapshot = catalog.clone();
1203        catalog.import_label(original, "replacement".into());
1204        assert_eq!(catalog.label_id("original"), None);
1205        assert_eq!(snapshot.label_id("original"), Some(original));
1206        assert_eq!(snapshot.label_id("replacement"), None);
1207        let renamed = catalog.get_or_create_label("original");
1208        assert_ne!(renamed, original);
1209        catalog.import_label(original, "original".into());
1210        assert_eq!(catalog.label_id("original"), Some(original));
1211        catalog.import_label(renamed, "third".into());
1212        assert_eq!(catalog.label_id("original"), Some(original));
1213        assert_eq!(catalog.label_id("replacement"), None);
1214        assert_eq!(catalog.label_id("third"), Some(renamed));
1215    }
1216
1217    #[test]
1218    fn label_admission_refusal_and_missing_do_not_retain_source() {
1219        let mut catalog = Catalog::default();
1220        let id = catalog.get_or_create_label("Item");
1221        let source = &catalog.labels[id.0 as usize];
1222        let owners = Arc::strong_count(source);
1223        let result =
1224            catalog.retain_label_name_with_admission(id, |_| -> crate::Result<LabelAllocation> {
1225                assert_eq!(Arc::strong_count(source), owners);
1226                Err(crate::HawDBError::Execution(
1227                    "label admission sentinel".into(),
1228                ))
1229            });
1230        assert!(
1231            matches!(result, Err(crate::HawDBError::Execution(reason)) if reason == "label admission sentinel")
1232        );
1233        assert_eq!(Arc::strong_count(source), owners);
1234        let result = catalog.retain_label_name_with_admission(id, |_| -> crate::Result<_> {
1235            Ok(LabelAllocation {
1236                bytes: 0,
1237                source_on_release: None,
1238            })
1239        });
1240        assert!(
1241            matches!(result, Err(crate::HawDBError::Execution(reason)) if reason.contains("insufficient allocation permit"))
1242        );
1243        assert_eq!(Arc::strong_count(source), owners);
1244        assert!(catalog
1245            .retain_label_name_with_admission(
1246                LabelId(u32::MAX),
1247                |_| -> crate::Result<LabelAllocation> {
1248                    panic!("missing label must skip admission");
1249                }
1250            )
1251            .unwrap()
1252            .is_none());
1253    }
1254
1255    #[test]
1256    fn label_admission_preserves_typed_host_refusal() {
1257        #[derive(Debug)]
1258        enum HostError {
1259            Refused { retryable: bool, identity: Arc<()> },
1260            Storage(crate::HawDBError),
1261        }
1262        impl From<crate::HawDBError> for HostError {
1263            fn from(error: crate::HawDBError) -> Self {
1264                Self::Storage(error)
1265            }
1266        }
1267        let mut catalog = Catalog::default();
1268        let id = catalog.get_or_create_label("Item");
1269        for retryable in [true, false] {
1270            let identity = Arc::new(());
1271            let result = catalog.retain_label_name_with_admission(
1272                id,
1273                |_| -> std::result::Result<LabelAllocation, HostError> {
1274                    Err(HostError::Refused {
1275                        retryable,
1276                        identity: Arc::clone(&identity),
1277                    })
1278                },
1279            );
1280            match result {
1281                Err(HostError::Refused {
1282                    retryable: observed,
1283                    identity: observed_identity,
1284                }) => {
1285                    assert_eq!(observed, retryable);
1286                    assert!(Arc::ptr_eq(&identity, &observed_identity));
1287                }
1288                Err(HostError::Storage(error)) => panic!("host error was converted: {error}"),
1289                Ok(_) => panic!("host refusal was ignored"),
1290            }
1291            assert_eq!(Arc::strong_count(&catalog.labels[id.0 as usize]), 2);
1292        }
1293    }
1294
1295    #[test]
1296    fn label_directory_shares_names_and_preserves_borrowed_lookup_order() {
1297        let mut catalog = Catalog::default();
1298        for name in ["zeta", "alpha", "beta"] {
1299            let id = catalog.get_or_create_label(name);
1300            let (key, indexed_id) = catalog.labels_by_name.get_key_value(name).unwrap();
1301            assert_eq!(*indexed_id, id);
1302            assert!(Arc::ptr_eq(&key.0, &catalog.labels[id.0 as usize]));
1303        }
1304        assert_eq!(
1305            catalog
1306                .labels_by_name
1307                .keys()
1308                .map(|key| key.0.name.as_str())
1309                .collect::<Vec<_>>(),
1310            ["alpha", "beta", "zeta"]
1311        );
1312        let id = catalog.label_id("alpha").unwrap();
1313        catalog.import_label(id, "alpha".into());
1314        let (key, indexed_id) = catalog.labels_by_name.get_key_value("alpha").unwrap();
1315        assert_eq!(*indexed_id, id);
1316        assert!(Arc::ptr_eq(&key.0, &catalog.labels[id.0 as usize]));
1317        assert_eq!(catalog.get_or_create_label("alpha"), id);
1318    }
1319
1320    #[test]
1321    fn advanced_statistics_freshness_requires_complete_current_epoch_data() {
1322        let unavailable = GraphStatistics {
1323            computed_at_commit_epoch: 7,
1324            advanced_statistics_complete: false,
1325            ..GraphStatistics::default()
1326        };
1327        assert_eq!(
1328            unavailable.advanced_statistics_freshness(7),
1329            AdvancedStatisticsFreshness::Unavailable
1330        );
1331
1332        let complete = GraphStatistics {
1333            advanced_statistics_complete: true,
1334            ..unavailable
1335        };
1336        assert_eq!(
1337            complete.advanced_statistics_freshness(7),
1338            AdvancedStatisticsFreshness::Fresh
1339        );
1340        assert_eq!(
1341            complete.advanced_statistics_freshness(8),
1342            AdvancedStatisticsFreshness::Stale
1343        );
1344        assert_eq!(complete.advanced_statistics_commit_lag(9), 2);
1345    }
1346
1347    #[test]
1348    fn property_types_map_to_shared_logical_types() {
1349        assert_eq!(PropertyType::Bool.logical_type(), LogicalType::Boolean);
1350        assert_eq!(PropertyType::Int.logical_type(), LogicalType::Int64);
1351        assert_eq!(PropertyType::Float.logical_type(), LogicalType::Float64);
1352        assert_eq!(PropertyType::String.logical_type(), LogicalType::String);
1353        assert_eq!(PropertyType::Text.logical_type(), LogicalType::Text);
1354        assert_eq!(PropertyType::List.logical_type(), LogicalType::List);
1355        assert_eq!(PropertyType::Any.logical_type(), LogicalType::Any);
1356    }
1357
1358    #[test]
1359    fn index_identifiers_are_unique_across_descriptor_kinds() {
1360        let mut catalog = Catalog::default();
1361        let label = catalog.get_or_create_label("Memory");
1362        let composite = catalog.get_or_create_composite_property_index(
1363            label,
1364            &["space_id".to_string(), "external_id".to_string()],
1365        );
1366        let equality = catalog.get_or_create_property_index(label, "id");
1367        let range =
1368            catalog.get_or_create_property_index_with_kind(label, "created_at", IndexKind::Range);
1369
1370        assert_ne!(composite, equality);
1371        assert_ne!(composite, range);
1372        assert_ne!(equality, range);
1373    }
1374
1375    #[test]
1376    fn imported_identifier_collision_disables_ambiguous_statistics() {
1377        let mut catalog = Catalog::default();
1378        let label = catalog.get_or_create_label("Memory");
1379        let id = catalog.get_or_create_property_index(label, "id");
1380        catalog.import_composite_property_index(
1381            id,
1382            label,
1383            vec!["space_id".to_string(), "external_id".to_string()],
1384        );
1385
1386        assert!(!catalog.supports_index_statistics(id));
1387    }
1388
1389    #[test]
1390    fn index_sample_estimate_is_bounded_and_rejects_stale_churn() {
1391        let sampled = IndexStatisticsSample {
1392            index_size: 1_000,
1393            unique_values: 40,
1394            sample_size: 100,
1395            updates_since_sample: 50,
1396        };
1397        assert_eq!(sampled.estimated_unique_values(), Some(400));
1398
1399        let stale = IndexStatisticsSample {
1400            updates_since_sample: 51,
1401            ..sampled
1402        };
1403        assert!(stale.is_stale());
1404        assert_eq!(stale.estimated_unique_values(), None);
1405
1406        let invalid = IndexStatisticsSample {
1407            index_size: 10,
1408            unique_values: 11,
1409            sample_size: 10,
1410            updates_since_sample: 0,
1411        };
1412        assert!(!invalid.is_valid());
1413        assert_eq!(invalid.estimated_unique_values(), None);
1414    }
1415}