Skip to main content

oxgraph_db/
state.rs

1//! Canonical incidence-first database state.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    Catalog, DbError, ElementId, IncidenceId, IndexId, LabelId, ProjectionDefinition, ProjectionId,
9    PropertyKeyId, RelationId, RelationTypeId, RoleId,
10    catalog::{IndexDefinition, PropertyFamily, PropertyKeyDefinition},
11    value::PropertyValue,
12};
13
14/// One visible canonical element.
15///
16/// # Performance
17///
18/// Cloning is `O(label count)`.
19#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
20pub struct ElementRecord {
21    /// Stable element identifier.
22    pub id: ElementId,
23    /// Labels assigned to this element.
24    pub labels: BTreeSet<LabelId>,
25}
26
27/// One visible canonical relation.
28///
29/// # Performance
30///
31/// Cloning is `O(label count)`.
32#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
33pub struct RelationRecord {
34    /// Stable relation identifier.
35    pub id: RelationId,
36    /// Optional relation type.
37    pub relation_type: Option<RelationTypeId>,
38    /// Labels assigned to this relation.
39    pub labels: BTreeSet<LabelId>,
40}
41
42/// One visible incidence in canonical database coordinates.
43///
44/// # Performance
45///
46/// Copying and comparing this record are `O(1)`.
47#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
48pub struct IncidenceRecord {
49    /// Stable incidence id.
50    pub id: IncidenceId,
51    /// Stable relation id containing the incidence.
52    pub relation: RelationId,
53    /// Stable element id participating in the relation.
54    pub element: ElementId,
55    /// Structural role of the incidence.
56    pub role: RoleId,
57}
58
59/// Subject that can own properties.
60///
61/// # Performance
62///
63/// Copying, comparing, ordering, and hashing are `O(1)`.
64#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
65pub enum PropertySubject {
66    /// Element property subject.
67    Element(ElementId),
68    /// Relation property subject.
69    Relation(RelationId),
70    /// Incidence property subject.
71    Incidence(IncidenceId),
72}
73
74impl PropertySubject {
75    /// Returns the property family for this subject.
76    ///
77    /// # Performance
78    ///
79    /// This function is `O(1)`.
80    #[must_use]
81    pub const fn family(self) -> PropertyFamily {
82        match self {
83            Self::Element(_id) => PropertyFamily::Element,
84            Self::Relation(_id) => PropertyFamily::Relation,
85            Self::Incidence(_id) => PropertyFamily::Incidence,
86        }
87    }
88}
89
90/// Durable canonical database state.
91#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
92pub(crate) struct DatabaseState {
93    /// Visible elements keyed by ID.
94    #[serde(with = "crate::serde_map")]
95    elements: BTreeMap<ElementId, ElementRecord>,
96    /// Visible relations keyed by ID.
97    #[serde(with = "crate::serde_map")]
98    relations: BTreeMap<RelationId, RelationRecord>,
99    /// Visible incidences keyed by ID.
100    #[serde(with = "crate::serde_map")]
101    incidences: BTreeMap<IncidenceId, IncidenceRecord>,
102    /// Property values keyed by subject and property key.
103    #[serde(with = "crate::serde_map::nested")]
104    properties: BTreeMap<PropertySubject, BTreeMap<PropertyKeyId, PropertyValue>>,
105    /// Catalog metadata.
106    catalog: Catalog,
107    /// Next element ID candidate.
108    next_element: ElementId,
109    /// Next relation ID candidate.
110    next_relation: RelationId,
111    /// Next incidence ID candidate.
112    next_incidence: IncidenceId,
113    /// Next role ID candidate.
114    next_role: RoleId,
115    /// Next label ID candidate.
116    next_label: LabelId,
117    /// Next relation type ID candidate.
118    next_relation_type: RelationTypeId,
119    /// Next property key ID candidate.
120    next_property_key: PropertyKeyId,
121    /// Next projection ID candidate.
122    next_projection: ProjectionId,
123    /// Next index ID candidate.
124    next_index: IndexId,
125}
126
127impl DatabaseState {
128    /// Creates an empty canonical state.
129    ///
130    /// # Performance
131    ///
132    /// This function is `O(1)`.
133    #[must_use]
134    pub(crate) const fn empty() -> Self {
135        Self {
136            elements: BTreeMap::new(),
137            relations: BTreeMap::new(),
138            incidences: BTreeMap::new(),
139            properties: BTreeMap::new(),
140            catalog: Catalog::empty(),
141            next_element: ElementId::new(1),
142            next_relation: RelationId::new(1),
143            next_incidence: IncidenceId::new(1),
144            next_role: RoleId::new(1),
145            next_label: LabelId::new(1),
146            next_relation_type: RelationTypeId::new(1),
147            next_property_key: PropertyKeyId::new(1),
148            next_projection: ProjectionId::new(1),
149            next_index: IndexId::new(1),
150        }
151    }
152
153    /// Returns catalog metadata.
154    ///
155    /// # Performance
156    ///
157    /// This method is `O(1)`.
158    #[must_use]
159    pub(crate) const fn catalog(&self) -> &Catalog {
160        &self.catalog
161    }
162
163    /// Returns whether an element exists.
164    ///
165    /// # Performance
166    ///
167    /// This method is `O(log n)`.
168    #[must_use]
169    pub(crate) fn contains_element(&self, id: ElementId) -> bool {
170        self.elements.contains_key(&id)
171    }
172
173    /// Returns whether a relation exists.
174    ///
175    /// # Performance
176    ///
177    /// This method is `O(log n)`.
178    #[must_use]
179    pub(crate) fn contains_relation(&self, id: RelationId) -> bool {
180        self.relations.contains_key(&id)
181    }
182
183    /// Returns whether an incidence exists.
184    ///
185    /// # Performance
186    ///
187    /// This method is `O(log n)`.
188    #[must_use]
189    pub(crate) fn contains_incidence(&self, id: IncidenceId) -> bool {
190        self.incidences.contains_key(&id)
191    }
192
193    /// Returns the number of visible elements.
194    ///
195    /// # Performance
196    ///
197    /// This method is `O(1)`.
198    #[must_use]
199    pub(crate) fn element_count(&self) -> usize {
200        self.elements.len()
201    }
202
203    /// Returns the number of visible relations.
204    ///
205    /// # Performance
206    ///
207    /// This method is `O(1)`.
208    #[must_use]
209    pub(crate) fn relation_count(&self) -> usize {
210        self.relations.len()
211    }
212
213    /// Returns the number of visible incidences.
214    ///
215    /// # Performance
216    ///
217    /// This method is `O(1)`.
218    #[must_use]
219    pub(crate) fn incidence_count(&self) -> usize {
220        self.incidences.len()
221    }
222
223    /// Iterates elements in ID order.
224    ///
225    /// # Performance
226    ///
227    /// Creating the iterator is `O(1)`.
228    pub(crate) fn elements(&self) -> impl Iterator<Item = &ElementRecord> {
229        self.elements.values()
230    }
231
232    /// Iterates relations in ID order.
233    ///
234    /// # Performance
235    ///
236    /// Creating the iterator is `O(1)`.
237    pub(crate) fn relations(&self) -> impl Iterator<Item = &RelationRecord> {
238        self.relations.values()
239    }
240
241    /// Iterates incidences in ID order.
242    ///
243    /// # Performance
244    ///
245    /// Creating the iterator is `O(1)`.
246    pub(crate) fn incidences(&self) -> impl Iterator<Item = &IncidenceRecord> {
247        self.incidences.values()
248    }
249
250    /// Returns one element record.
251    ///
252    /// # Performance
253    ///
254    /// This method is `O(log n)`.
255    #[must_use]
256    pub(crate) fn element(&self, id: ElementId) -> Option<&ElementRecord> {
257        self.elements.get(&id)
258    }
259
260    /// Returns one relation record.
261    ///
262    /// # Performance
263    ///
264    /// This method is `O(log n)`.
265    #[must_use]
266    pub(crate) fn relation(&self, id: RelationId) -> Option<&RelationRecord> {
267        self.relations.get(&id)
268    }
269
270    /// Returns one incidence record.
271    ///
272    /// # Performance
273    ///
274    /// This method is `O(log n)`.
275    #[must_use]
276    pub(crate) fn incidence(&self, id: IncidenceId) -> Option<&IncidenceRecord> {
277        self.incidences.get(&id)
278    }
279
280    /// Returns all incidences for a relation.
281    ///
282    /// # Performance
283    ///
284    /// This method is `O(i)` for visible incidence count.
285    pub(crate) fn relation_incidences(
286        &self,
287        id: RelationId,
288    ) -> impl Iterator<Item = &IncidenceRecord> {
289        self.incidences
290            .values()
291            .filter(move |record| record.relation == id)
292    }
293
294    /// Returns all incidences for an element.
295    ///
296    /// # Performance
297    ///
298    /// This method is `O(i)` for visible incidence count.
299    pub(crate) fn element_incidences(
300        &self,
301        id: ElementId,
302    ) -> impl Iterator<Item = &IncidenceRecord> {
303        self.incidences
304            .values()
305            .filter(move |record| record.element == id)
306    }
307
308    /// Creates an element.
309    pub(crate) fn create_element(&mut self) -> Result<ElementId, DbError> {
310        let id = self.next_element;
311        self.next_element = id.checked_next().ok_or(DbError::IdOverflow)?;
312        let previous = self.elements.insert(
313            id,
314            ElementRecord {
315                id,
316                labels: BTreeSet::new(),
317            },
318        );
319        if previous.is_some() {
320            return Err(DbError::DuplicateId);
321        }
322        Ok(id)
323    }
324
325    /// Creates a relation.
326    pub(crate) fn create_relation(&mut self) -> Result<RelationId, DbError> {
327        let id = self.next_relation;
328        self.next_relation = id.checked_next().ok_or(DbError::IdOverflow)?;
329        let previous = self.relations.insert(
330            id,
331            RelationRecord {
332                id,
333                relation_type: None,
334                labels: BTreeSet::new(),
335            },
336        );
337        if previous.is_some() {
338            return Err(DbError::DuplicateId);
339        }
340        Ok(id)
341    }
342
343    /// Creates an incidence.
344    pub(crate) fn create_incidence(
345        &mut self,
346        relation: RelationId,
347        element: ElementId,
348        role: RoleId,
349    ) -> Result<IncidenceId, DbError> {
350        self.require_relation(relation)?;
351        self.require_element(element)?;
352        self.require_role(role)?;
353        let id = self.next_incidence;
354        self.next_incidence = id.checked_next().ok_or(DbError::IdOverflow)?;
355        let previous = self.incidences.insert(
356            id,
357            IncidenceRecord {
358                id,
359                relation,
360                element,
361                role,
362            },
363        );
364        if previous.is_some() {
365            return Err(DbError::DuplicateId);
366        }
367        Ok(id)
368    }
369
370    /// Tombstones an element and its incidences.
371    pub(crate) fn tombstone_element(&mut self, id: ElementId) -> Result<(), DbError> {
372        self.elements
373            .remove(&id)
374            .ok_or(DbError::UnknownElement { id })?;
375        self.properties.remove(&PropertySubject::Element(id));
376        self.remove_incidences(|record| record.element == id);
377        Ok(())
378    }
379
380    /// Tombstones a relation and its incidences.
381    pub(crate) fn tombstone_relation(&mut self, id: RelationId) -> Result<(), DbError> {
382        self.relations
383            .remove(&id)
384            .ok_or(DbError::UnknownRelation { id })?;
385        self.properties.remove(&PropertySubject::Relation(id));
386        self.remove_incidences(|record| record.relation == id);
387        Ok(())
388    }
389
390    /// Tombstones one incidence.
391    pub(crate) fn tombstone_incidence(&mut self, id: IncidenceId) -> Result<(), DbError> {
392        self.incidences
393            .remove(&id)
394            .ok_or(DbError::UnknownIncidence { id })?;
395        self.properties.remove(&PropertySubject::Incidence(id));
396        Ok(())
397    }
398
399    /// Registers a role.
400    pub(crate) fn register_role(&mut self, name: String) -> Result<RoleId, DbError> {
401        let id = self.next_role;
402        self.next_role = id.checked_next().ok_or(DbError::IdOverflow)?;
403        self.catalog.insert_role(id, name)?;
404        Ok(id)
405    }
406
407    /// Registers a label.
408    pub(crate) fn register_label(&mut self, name: String) -> Result<LabelId, DbError> {
409        let id = self.next_label;
410        self.next_label = id.checked_next().ok_or(DbError::IdOverflow)?;
411        self.catalog.insert_label(id, name)?;
412        Ok(id)
413    }
414
415    /// Registers a relation type.
416    pub(crate) fn register_relation_type(
417        &mut self,
418        name: String,
419    ) -> Result<RelationTypeId, DbError> {
420        let id = self.next_relation_type;
421        self.next_relation_type = id.checked_next().ok_or(DbError::IdOverflow)?;
422        self.catalog.insert_relation_type(id, name)?;
423        Ok(id)
424    }
425
426    /// Registers a property key.
427    pub(crate) fn register_property_key(
428        &mut self,
429        name: String,
430        family: PropertyFamily,
431        value_type: crate::PropertyType,
432    ) -> Result<PropertyKeyId, DbError> {
433        let id = self.next_property_key;
434        self.next_property_key = id.checked_next().ok_or(DbError::IdOverflow)?;
435        self.catalog.insert_property_key(PropertyKeyDefinition {
436            id,
437            name,
438            family,
439            value_type,
440        })?;
441        Ok(id)
442    }
443
444    /// Defines a physical projection.
445    pub(crate) fn define_projection(
446        &mut self,
447        definition: ProjectionDefinition,
448    ) -> Result<ProjectionId, DbError> {
449        self.validate_projection_definition(&definition)?;
450        let id = self.next_projection;
451        self.next_projection = id.checked_next().ok_or(DbError::IdOverflow)?;
452        self.catalog.insert_projection(id, definition)?;
453        Ok(id)
454    }
455
456    /// Defines an index.
457    pub(crate) fn define_index(
458        &mut self,
459        name: String,
460        definition: IndexDefinition,
461    ) -> Result<IndexId, DbError> {
462        self.validate_index_definition(&definition)?;
463        let id = self.next_index;
464        self.next_index = id.checked_next().ok_or(DbError::IdOverflow)?;
465        self.catalog.insert_index(id, name, definition)?;
466        Ok(id)
467    }
468
469    /// Assigns a label to an element.
470    pub(crate) fn add_element_label(
471        &mut self,
472        element: ElementId,
473        label: LabelId,
474    ) -> Result<(), DbError> {
475        self.require_label(label)?;
476        let record = self
477            .elements
478            .get_mut(&element)
479            .ok_or(DbError::UnknownElement { id: element })?;
480        record.labels.insert(label);
481        Ok(())
482    }
483
484    /// Assigns a label to a relation.
485    pub(crate) fn add_relation_label(
486        &mut self,
487        relation: RelationId,
488        label: LabelId,
489    ) -> Result<(), DbError> {
490        self.require_label(label)?;
491        let record = self
492            .relations
493            .get_mut(&relation)
494            .ok_or(DbError::UnknownRelation { id: relation })?;
495        record.labels.insert(label);
496        Ok(())
497    }
498
499    /// Sets a relation type.
500    pub(crate) fn set_relation_type(
501        &mut self,
502        relation: RelationId,
503        relation_type: RelationTypeId,
504    ) -> Result<(), DbError> {
505        self.require_relation_type(relation_type)?;
506        let record = self
507            .relations
508            .get_mut(&relation)
509            .ok_or(DbError::UnknownRelation { id: relation })?;
510        record.relation_type = Some(relation_type);
511        Ok(())
512    }
513
514    /// Sets a typed property value.
515    pub(crate) fn set_property(
516        &mut self,
517        subject: PropertySubject,
518        key: PropertyKeyId,
519        value: PropertyValue,
520    ) -> Result<(), DbError> {
521        self.require_subject(subject)?;
522        let definition = self
523            .catalog
524            .property_key(key)
525            .ok_or(DbError::UnknownPropertyKey { id: key })?;
526        if definition.family != subject.family() {
527            return Err(DbError::WrongPropertyFamily {
528                expected: definition.family,
529                actual: subject.family(),
530            });
531        }
532        if definition.value_type != value.value_type() {
533            return Err(DbError::PropertyTypeMismatch {
534                expected: definition.value_type,
535                actual: value.value_type(),
536            });
537        }
538        self.properties
539            .entry(subject)
540            .or_default()
541            .insert(key, value);
542        Ok(())
543    }
544
545    /// Removes one property value.
546    pub(crate) fn remove_property(
547        &mut self,
548        subject: PropertySubject,
549        key: PropertyKeyId,
550    ) -> Result<(), DbError> {
551        self.require_subject(subject)?;
552        self.require_property_key(key)?;
553        if let Some(values) = self.properties.get_mut(&subject) {
554            values.remove(&key);
555            if values.is_empty() {
556                self.properties.remove(&subject);
557            }
558        }
559        Ok(())
560    }
561
562    /// Returns one property value.
563    #[must_use]
564    pub(crate) fn property(
565        &self,
566        subject: PropertySubject,
567        key: PropertyKeyId,
568    ) -> Option<&PropertyValue> {
569        self.properties
570            .get(&subject)
571            .and_then(|values| values.get(&key))
572    }
573
574    /// Returns subjects whose property equals `value`.
575    pub(crate) fn property_equal(
576        &self,
577        key: PropertyKeyId,
578        value: &PropertyValue,
579    ) -> Vec<PropertySubject> {
580        self.properties
581            .iter()
582            .filter_map(|(subject, values)| (values.get(&key) == Some(value)).then_some(*subject))
583            .collect()
584    }
585
586    /// Returns subjects whose typed property equals `value`.
587    pub(crate) fn typed_property_equal(
588        &self,
589        key: PropertyKeyId,
590        value: &PropertyValue,
591    ) -> Result<Vec<PropertySubject>, DbError> {
592        self.validate_lookup_value(key, value)?;
593        Ok(self.property_equal(key, value))
594    }
595
596    /// Returns subjects in `family` whose typed property equals `value`.
597    pub(crate) fn typed_property_equal_for_family(
598        &self,
599        key: PropertyKeyId,
600        family: PropertyFamily,
601        value: &PropertyValue,
602    ) -> Result<Vec<PropertySubject>, DbError> {
603        self.validate_lookup_value_for_family(key, family, value)?;
604        Ok(self.property_equal(key, value))
605    }
606
607    /// Returns subjects whose ordered property falls inside an inclusive range.
608    pub(crate) fn property_range(
609        &self,
610        key: PropertyKeyId,
611        min: &PropertyValue,
612        max: &PropertyValue,
613    ) -> Vec<PropertySubject> {
614        self.properties
615            .iter()
616            .filter_map(|(subject, values)| {
617                let value = values.get(&key)?;
618                (value >= min && value <= max).then_some(*subject)
619            })
620            .collect()
621    }
622
623    /// Returns subjects whose typed property falls inside an inclusive range.
624    pub(crate) fn typed_property_range(
625        &self,
626        key: PropertyKeyId,
627        min: &PropertyValue,
628        max: &PropertyValue,
629    ) -> Result<Vec<PropertySubject>, DbError> {
630        self.validate_lookup_value(key, min)?;
631        self.validate_lookup_value(key, max)?;
632        if min > max {
633            return Ok(Vec::new());
634        }
635        Ok(self.property_range(key, min, max))
636    }
637
638    /// Returns subjects matching an ordered typed property tuple.
639    pub(crate) fn typed_property_composite_equal(
640        &self,
641        keys: &[PropertyKeyId],
642        values: &[PropertyValue],
643    ) -> Result<Vec<PropertySubject>, DbError> {
644        if keys.len() != values.len() {
645            return Err(DbError::unsupported(
646                "composite equality tuple arity mismatch",
647            ));
648        }
649        for (key, value) in keys.iter().copied().zip(values) {
650            self.validate_lookup_value(key, value)?;
651        }
652        Ok(self
653            .properties
654            .iter()
655            .filter_map(|(subject, property_values)| {
656                keys.iter()
657                    .copied()
658                    .zip(values)
659                    .all(|(key, value)| property_values.get(&key) == Some(value))
660                    .then_some(*subject)
661            })
662            .collect())
663    }
664
665    /// Returns elements that have `label`.
666    pub(crate) fn elements_with_label(&self, label: LabelId) -> Vec<ElementId> {
667        self.elements
668            .values()
669            .filter_map(|record| record.labels.contains(&label).then_some(record.id))
670            .collect()
671    }
672
673    /// Returns relations that have `relation_type`.
674    pub(crate) fn relations_with_type(&self, relation_type: RelationTypeId) -> Vec<RelationId> {
675        self.relations
676            .values()
677            .filter_map(|record| (record.relation_type == Some(relation_type)).then_some(record.id))
678            .collect()
679    }
680
681    /// Validates all persisted references.
682    pub(crate) fn validate(&self) -> Result<(), DbError> {
683        for record in self.incidences.values() {
684            self.require_relation(record.relation)?;
685            self.require_element(record.element)?;
686            self.require_role(record.role)?;
687        }
688        for record in self.elements.values() {
689            self.require_labels(&record.labels)?;
690        }
691        for record in self.relations.values() {
692            self.require_labels(&record.labels)?;
693            if let Some(relation_type) = record.relation_type {
694                self.require_relation_type(relation_type)?;
695            }
696        }
697        self.validate_properties()?;
698        self.validate_catalog_definitions()
699    }
700
701    /// Requires an element to exist.
702    fn require_element(&self, id: ElementId) -> Result<(), DbError> {
703        self.elements
704            .contains_key(&id)
705            .then_some(())
706            .ok_or(DbError::UnknownElement { id })
707    }
708
709    /// Requires a relation to exist.
710    fn require_relation(&self, id: RelationId) -> Result<(), DbError> {
711        self.relations
712            .contains_key(&id)
713            .then_some(())
714            .ok_or(DbError::UnknownRelation { id })
715    }
716
717    /// Requires an incidence to exist.
718    fn require_incidence(&self, id: IncidenceId) -> Result<(), DbError> {
719        self.incidences
720            .contains_key(&id)
721            .then_some(())
722            .ok_or(DbError::UnknownIncidence { id })
723    }
724
725    /// Requires a role to exist.
726    fn require_role(&self, id: RoleId) -> Result<(), DbError> {
727        self.catalog
728            .role(id)
729            .is_some()
730            .then_some(())
731            .ok_or(DbError::UnknownRole { id })
732    }
733
734    /// Requires a label to exist.
735    fn require_label(&self, id: LabelId) -> Result<(), DbError> {
736        self.catalog
737            .label(id)
738            .is_some()
739            .then_some(())
740            .ok_or(DbError::UnknownLabel { id })
741    }
742
743    /// Requires a relation type to exist.
744    fn require_relation_type(&self, id: RelationTypeId) -> Result<(), DbError> {
745        self.catalog
746            .relation_type(id)
747            .is_some()
748            .then_some(())
749            .ok_or(DbError::UnknownRelationType { id })
750    }
751
752    /// Requires a property key to exist.
753    fn require_property_key(&self, id: PropertyKeyId) -> Result<(), DbError> {
754        self.catalog
755            .property_key(id)
756            .is_some()
757            .then_some(())
758            .ok_or(DbError::UnknownPropertyKey { id })
759    }
760
761    /// Requires a property subject to exist.
762    fn require_subject(&self, subject: PropertySubject) -> Result<(), DbError> {
763        match subject {
764            PropertySubject::Element(id) => self.require_element(id),
765            PropertySubject::Relation(id) => self.require_relation(id),
766            PropertySubject::Incidence(id) => self.require_incidence(id),
767        }
768    }
769
770    /// Requires all labels to exist.
771    fn require_labels(&self, labels: &BTreeSet<LabelId>) -> Result<(), DbError> {
772        for label in labels {
773            self.require_label(*label)?;
774        }
775        Ok(())
776    }
777
778    /// Removes matching incidences and their properties.
779    fn remove_incidences(&mut self, mut should_remove: impl FnMut(&IncidenceRecord) -> bool) {
780        let removed: Vec<_> = self
781            .incidences
782            .values()
783            .filter_map(|record| should_remove(record).then_some(record.id))
784            .collect();
785        for id in removed {
786            self.incidences.remove(&id);
787            self.properties.remove(&PropertySubject::Incidence(id));
788        }
789    }
790
791    /// Validates one projection definition.
792    fn validate_projection_definition(
793        &self,
794        definition: &ProjectionDefinition,
795    ) -> Result<(), DbError> {
796        match definition {
797            ProjectionDefinition::Graph(graph) => {
798                self.require_role(graph.source_role)?;
799                self.require_role(graph.target_role)?;
800                self.require_relation_types(&graph.relation_types)
801            }
802            ProjectionDefinition::Hypergraph(hypergraph) => {
803                self.require_roles(&hypergraph.source_roles)?;
804                self.require_roles(&hypergraph.target_roles)?;
805                self.require_relation_types(&hypergraph.relation_types)
806            }
807        }
808    }
809
810    /// Validates one index definition.
811    fn validate_index_definition(&self, definition: &IndexDefinition) -> Result<(), DbError> {
812        match definition {
813            IndexDefinition::Label { label } => self.require_label(*label),
814            IndexDefinition::RelationType { relation_type } => {
815                self.require_relation_type(*relation_type)
816            }
817            IndexDefinition::PropertyEquality { key } | IndexDefinition::PropertyRange { key } => {
818                self.require_property_key(*key)
819            }
820            IndexDefinition::CompositeEquality { keys } => {
821                if keys.is_empty() {
822                    return Err(DbError::unsupported(
823                        "composite equality index requires at least one key",
824                    ));
825                }
826                for key in keys {
827                    self.require_property_key(*key)?;
828                }
829                Ok(())
830            }
831            IndexDefinition::Projection { projection } => self
832                .catalog
833                .projection(*projection)
834                .is_some()
835                .then_some(())
836                .ok_or(DbError::UnknownProjection { id: *projection }),
837        }
838    }
839
840    /// Requires every role in a set to exist.
841    fn require_roles(&self, roles: &BTreeSet<RoleId>) -> Result<(), DbError> {
842        for role in roles {
843            self.require_role(*role)?;
844        }
845        Ok(())
846    }
847
848    /// Requires every relation type in a set to exist.
849    fn require_relation_types(
850        &self,
851        relation_types: &BTreeSet<RelationTypeId>,
852    ) -> Result<(), DbError> {
853        for relation_type in relation_types {
854            self.require_relation_type(*relation_type)?;
855        }
856        Ok(())
857    }
858
859    /// Validates property maps.
860    fn validate_properties(&self) -> Result<(), DbError> {
861        for (subject, values) in &self.properties {
862            self.require_subject(*subject)?;
863            for (key, value) in values {
864                self.validate_property_value(*subject, *key, value)?;
865            }
866        }
867        Ok(())
868    }
869
870    /// Validates a lookup value against a property key schema.
871    fn validate_lookup_value(
872        &self,
873        key: PropertyKeyId,
874        value: &PropertyValue,
875    ) -> Result<(), DbError> {
876        let definition = self
877            .catalog
878            .property_key(key)
879            .ok_or(DbError::UnknownPropertyKey { id: key })?;
880        let actual = value.value_type();
881        if definition.value_type != actual {
882            return Err(DbError::PropertyTypeMismatch {
883                expected: definition.value_type,
884                actual,
885            });
886        }
887        Ok(())
888    }
889
890    /// Validates a lookup value and subject family against a property key schema.
891    pub(crate) fn validate_lookup_value_for_family(
892        &self,
893        key: PropertyKeyId,
894        family: PropertyFamily,
895        value: &PropertyValue,
896    ) -> Result<(), DbError> {
897        let definition = self
898            .catalog
899            .property_key(key)
900            .ok_or(DbError::UnknownPropertyKey { id: key })?;
901        if definition.family != family {
902            return Err(DbError::WrongPropertyFamily {
903                expected: definition.family,
904                actual: family,
905            });
906        }
907        if definition.value_type != value.value_type() {
908            return Err(DbError::PropertyTypeMismatch {
909                expected: definition.value_type,
910                actual: value.value_type(),
911            });
912        }
913        Ok(())
914    }
915
916    /// Validates one property value against the catalog schema.
917    fn validate_property_value(
918        &self,
919        subject: PropertySubject,
920        key: PropertyKeyId,
921        value: &PropertyValue,
922    ) -> Result<(), DbError> {
923        let definition = self
924            .catalog
925            .property_key(key)
926            .ok_or(DbError::UnknownPropertyKey { id: key })?;
927        if definition.family != subject.family() {
928            return Err(DbError::WrongPropertyFamily {
929                expected: definition.family,
930                actual: subject.family(),
931            });
932        }
933        if definition.value_type != value.value_type() {
934            return Err(DbError::PropertyTypeMismatch {
935                expected: definition.value_type,
936                actual: value.value_type(),
937            });
938        }
939        Ok(())
940    }
941
942    /// Validates catalog projection and index references.
943    fn validate_catalog_definitions(&self) -> Result<(), DbError> {
944        for entry in self.catalog.projections() {
945            self.validate_projection_definition(&entry.definition)?;
946        }
947        for entry in self.catalog.indexes() {
948            self.validate_index_definition(&entry.definition)?;
949        }
950        Ok(())
951    }
952}