1use 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#[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#[doc(hidden)]
68pub trait RetainedLabelAllocation {
69 fn bytes(&self) -> usize;
70}
71
72#[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 String,
150 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub enum AdvancedStatisticsFreshness {
279 Fresh,
281 Stale,
283 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 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 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 #[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 #[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}