Skip to main content

grafeo_engine/catalog/
mod.rs

1//! Database catalog.
2//!
3//! Manages schema definitions, name-to-ID mappings, and index metadata.
4//!
5//! The catalog provides:
6//! - Bidirectional mappings for labels, property keys, and edge types
7//! - Index definitions and lookups
8//! - Optional schema constraints
9
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU32, Ordering};
13
14use parking_lot::RwLock;
15
16use grafeo_common::types::{EdgeTypeId, IndexId, LabelId, PropertyKeyId};
17
18/// The main database catalog.
19///
20/// Manages all metadata about the database schema including:
21/// - Labels (node categories)
22/// - Property keys (property names)
23/// - Edge types (relationship types)
24/// - Index definitions
25pub struct Catalog {
26    /// Label name-to-ID mappings.
27    labels: LabelCatalog,
28    /// Property key name-to-ID mappings.
29    property_keys: PropertyCatalog,
30    /// Edge type name-to-ID mappings.
31    edge_types: EdgeTypeCatalog,
32    /// Index definitions.
33    indexes: IndexCatalog,
34    /// Optional schema constraints.
35    schema: Option<SchemaCatalog>,
36}
37
38impl Catalog {
39    /// Creates a new empty catalog.
40    #[must_use]
41    pub fn new() -> Self {
42        Self {
43            labels: LabelCatalog::new(),
44            property_keys: PropertyCatalog::new(),
45            edge_types: EdgeTypeCatalog::new(),
46            indexes: IndexCatalog::new(),
47            schema: None,
48        }
49    }
50
51    /// Creates a new catalog with schema constraints enabled.
52    #[must_use]
53    pub fn with_schema() -> Self {
54        Self {
55            labels: LabelCatalog::new(),
56            property_keys: PropertyCatalog::new(),
57            edge_types: EdgeTypeCatalog::new(),
58            indexes: IndexCatalog::new(),
59            schema: Some(SchemaCatalog::new()),
60        }
61    }
62
63    // === Label Operations ===
64
65    /// Gets or creates a label ID for the given label name.
66    pub fn get_or_create_label(&self, name: &str) -> LabelId {
67        self.labels.get_or_create(name)
68    }
69
70    /// Gets the label ID for a label name, if it exists.
71    #[must_use]
72    pub fn get_label_id(&self, name: &str) -> Option<LabelId> {
73        self.labels.get_id(name)
74    }
75
76    /// Gets the label name for a label ID, if it exists.
77    #[must_use]
78    pub fn get_label_name(&self, id: LabelId) -> Option<Arc<str>> {
79        self.labels.get_name(id)
80    }
81
82    /// Returns the number of distinct labels.
83    #[must_use]
84    pub fn label_count(&self) -> usize {
85        self.labels.count()
86    }
87
88    /// Returns all label names.
89    #[must_use]
90    pub fn all_labels(&self) -> Vec<Arc<str>> {
91        self.labels.all_names()
92    }
93
94    // === Property Key Operations ===
95
96    /// Gets or creates a property key ID for the given property key name.
97    pub fn get_or_create_property_key(&self, name: &str) -> PropertyKeyId {
98        self.property_keys.get_or_create(name)
99    }
100
101    /// Gets the property key ID for a property key name, if it exists.
102    #[must_use]
103    pub fn get_property_key_id(&self, name: &str) -> Option<PropertyKeyId> {
104        self.property_keys.get_id(name)
105    }
106
107    /// Gets the property key name for a property key ID, if it exists.
108    #[must_use]
109    pub fn get_property_key_name(&self, id: PropertyKeyId) -> Option<Arc<str>> {
110        self.property_keys.get_name(id)
111    }
112
113    /// Returns the number of distinct property keys.
114    #[must_use]
115    pub fn property_key_count(&self) -> usize {
116        self.property_keys.count()
117    }
118
119    /// Returns all property key names.
120    #[must_use]
121    pub fn all_property_keys(&self) -> Vec<Arc<str>> {
122        self.property_keys.all_names()
123    }
124
125    // === Edge Type Operations ===
126
127    /// Gets or creates an edge type ID for the given edge type name.
128    pub fn get_or_create_edge_type(&self, name: &str) -> EdgeTypeId {
129        self.edge_types.get_or_create(name)
130    }
131
132    /// Gets the edge type ID for an edge type name, if it exists.
133    #[must_use]
134    pub fn get_edge_type_id(&self, name: &str) -> Option<EdgeTypeId> {
135        self.edge_types.get_id(name)
136    }
137
138    /// Gets the edge type name for an edge type ID, if it exists.
139    #[must_use]
140    pub fn get_edge_type_name(&self, id: EdgeTypeId) -> Option<Arc<str>> {
141        self.edge_types.get_name(id)
142    }
143
144    /// Returns the number of distinct edge types.
145    #[must_use]
146    pub fn edge_type_count(&self) -> usize {
147        self.edge_types.count()
148    }
149
150    /// Returns all edge type names.
151    #[must_use]
152    pub fn all_edge_types(&self) -> Vec<Arc<str>> {
153        self.edge_types.all_names()
154    }
155
156    // === Index Operations ===
157
158    /// Creates a new index on a label and property key.
159    pub fn create_index(
160        &self,
161        label: LabelId,
162        property_key: PropertyKeyId,
163        index_type: IndexType,
164    ) -> IndexId {
165        self.indexes.create(label, property_key, index_type)
166    }
167
168    /// Drops an index by ID.
169    pub fn drop_index(&self, id: IndexId) -> bool {
170        self.indexes.drop(id)
171    }
172
173    /// Gets the index definition for an index ID.
174    #[must_use]
175    pub fn get_index(&self, id: IndexId) -> Option<IndexDefinition> {
176        self.indexes.get(id)
177    }
178
179    /// Finds indexes for a given label.
180    #[must_use]
181    pub fn indexes_for_label(&self, label: LabelId) -> Vec<IndexId> {
182        self.indexes.for_label(label)
183    }
184
185    /// Finds indexes for a given label and property key.
186    #[must_use]
187    pub fn indexes_for_label_property(
188        &self,
189        label: LabelId,
190        property_key: PropertyKeyId,
191    ) -> Vec<IndexId> {
192        self.indexes.for_label_property(label, property_key)
193    }
194
195    /// Returns the number of indexes.
196    #[must_use]
197    pub fn index_count(&self) -> usize {
198        self.indexes.count()
199    }
200
201    // === Schema Operations ===
202
203    /// Returns whether schema constraints are enabled.
204    #[must_use]
205    pub fn has_schema(&self) -> bool {
206        self.schema.is_some()
207    }
208
209    /// Adds a uniqueness constraint.
210    ///
211    /// Returns an error if schema is not enabled or constraint already exists.
212    pub fn add_unique_constraint(
213        &self,
214        label: LabelId,
215        property_key: PropertyKeyId,
216    ) -> Result<(), CatalogError> {
217        match &self.schema {
218            Some(schema) => schema.add_unique_constraint(label, property_key),
219            None => Err(CatalogError::SchemaNotEnabled),
220        }
221    }
222
223    /// Adds a required property constraint (NOT NULL).
224    ///
225    /// Returns an error if schema is not enabled or constraint already exists.
226    pub fn add_required_property(
227        &self,
228        label: LabelId,
229        property_key: PropertyKeyId,
230    ) -> Result<(), CatalogError> {
231        match &self.schema {
232            Some(schema) => schema.add_required_property(label, property_key),
233            None => Err(CatalogError::SchemaNotEnabled),
234        }
235    }
236
237    /// Checks if a property is required for a label.
238    #[must_use]
239    pub fn is_property_required(&self, label: LabelId, property_key: PropertyKeyId) -> bool {
240        self.schema
241            .as_ref()
242            .is_some_and(|s| s.is_property_required(label, property_key))
243    }
244
245    /// Checks if a property must be unique for a label.
246    #[must_use]
247    pub fn is_property_unique(&self, label: LabelId, property_key: PropertyKeyId) -> bool {
248        self.schema
249            .as_ref()
250            .is_some_and(|s| s.is_property_unique(label, property_key))
251    }
252}
253
254impl Default for Catalog {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260// === Label Catalog ===
261
262/// Bidirectional mapping between label names and IDs.
263struct LabelCatalog {
264    name_to_id: RwLock<HashMap<Arc<str>, LabelId>>,
265    id_to_name: RwLock<Vec<Arc<str>>>,
266    next_id: AtomicU32,
267}
268
269impl LabelCatalog {
270    fn new() -> Self {
271        Self {
272            name_to_id: RwLock::new(HashMap::new()),
273            id_to_name: RwLock::new(Vec::new()),
274            next_id: AtomicU32::new(0),
275        }
276    }
277
278    fn get_or_create(&self, name: &str) -> LabelId {
279        // Fast path: check if already exists
280        {
281            let name_to_id = self.name_to_id.read();
282            if let Some(&id) = name_to_id.get(name) {
283                return id;
284            }
285        }
286
287        // Slow path: create new entry
288        let mut name_to_id = self.name_to_id.write();
289        let mut id_to_name = self.id_to_name.write();
290
291        // Double-check after acquiring write lock
292        if let Some(&id) = name_to_id.get(name) {
293            return id;
294        }
295
296        let id = LabelId::new(self.next_id.fetch_add(1, Ordering::Relaxed));
297        let name: Arc<str> = name.into();
298        name_to_id.insert(Arc::clone(&name), id);
299        id_to_name.push(name);
300        id
301    }
302
303    fn get_id(&self, name: &str) -> Option<LabelId> {
304        self.name_to_id.read().get(name).copied()
305    }
306
307    fn get_name(&self, id: LabelId) -> Option<Arc<str>> {
308        self.id_to_name.read().get(id.as_u32() as usize).cloned()
309    }
310
311    fn count(&self) -> usize {
312        self.id_to_name.read().len()
313    }
314
315    fn all_names(&self) -> Vec<Arc<str>> {
316        self.id_to_name.read().clone()
317    }
318}
319
320// === Property Catalog ===
321
322/// Bidirectional mapping between property key names and IDs.
323struct PropertyCatalog {
324    name_to_id: RwLock<HashMap<Arc<str>, PropertyKeyId>>,
325    id_to_name: RwLock<Vec<Arc<str>>>,
326    next_id: AtomicU32,
327}
328
329impl PropertyCatalog {
330    fn new() -> Self {
331        Self {
332            name_to_id: RwLock::new(HashMap::new()),
333            id_to_name: RwLock::new(Vec::new()),
334            next_id: AtomicU32::new(0),
335        }
336    }
337
338    fn get_or_create(&self, name: &str) -> PropertyKeyId {
339        // Fast path: check if already exists
340        {
341            let name_to_id = self.name_to_id.read();
342            if let Some(&id) = name_to_id.get(name) {
343                return id;
344            }
345        }
346
347        // Slow path: create new entry
348        let mut name_to_id = self.name_to_id.write();
349        let mut id_to_name = self.id_to_name.write();
350
351        // Double-check after acquiring write lock
352        if let Some(&id) = name_to_id.get(name) {
353            return id;
354        }
355
356        let id = PropertyKeyId::new(self.next_id.fetch_add(1, Ordering::Relaxed));
357        let name: Arc<str> = name.into();
358        name_to_id.insert(Arc::clone(&name), id);
359        id_to_name.push(name);
360        id
361    }
362
363    fn get_id(&self, name: &str) -> Option<PropertyKeyId> {
364        self.name_to_id.read().get(name).copied()
365    }
366
367    fn get_name(&self, id: PropertyKeyId) -> Option<Arc<str>> {
368        self.id_to_name.read().get(id.as_u32() as usize).cloned()
369    }
370
371    fn count(&self) -> usize {
372        self.id_to_name.read().len()
373    }
374
375    fn all_names(&self) -> Vec<Arc<str>> {
376        self.id_to_name.read().clone()
377    }
378}
379
380// === Edge Type Catalog ===
381
382/// Bidirectional mapping between edge type names and IDs.
383struct EdgeTypeCatalog {
384    name_to_id: RwLock<HashMap<Arc<str>, EdgeTypeId>>,
385    id_to_name: RwLock<Vec<Arc<str>>>,
386    next_id: AtomicU32,
387}
388
389impl EdgeTypeCatalog {
390    fn new() -> Self {
391        Self {
392            name_to_id: RwLock::new(HashMap::new()),
393            id_to_name: RwLock::new(Vec::new()),
394            next_id: AtomicU32::new(0),
395        }
396    }
397
398    fn get_or_create(&self, name: &str) -> EdgeTypeId {
399        // Fast path: check if already exists
400        {
401            let name_to_id = self.name_to_id.read();
402            if let Some(&id) = name_to_id.get(name) {
403                return id;
404            }
405        }
406
407        // Slow path: create new entry
408        let mut name_to_id = self.name_to_id.write();
409        let mut id_to_name = self.id_to_name.write();
410
411        // Double-check after acquiring write lock
412        if let Some(&id) = name_to_id.get(name) {
413            return id;
414        }
415
416        let id = EdgeTypeId::new(self.next_id.fetch_add(1, Ordering::Relaxed));
417        let name: Arc<str> = name.into();
418        name_to_id.insert(Arc::clone(&name), id);
419        id_to_name.push(name);
420        id
421    }
422
423    fn get_id(&self, name: &str) -> Option<EdgeTypeId> {
424        self.name_to_id.read().get(name).copied()
425    }
426
427    fn get_name(&self, id: EdgeTypeId) -> Option<Arc<str>> {
428        self.id_to_name.read().get(id.as_u32() as usize).cloned()
429    }
430
431    fn count(&self) -> usize {
432        self.id_to_name.read().len()
433    }
434
435    fn all_names(&self) -> Vec<Arc<str>> {
436        self.id_to_name.read().clone()
437    }
438}
439
440// === Index Catalog ===
441
442/// Type of index.
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum IndexType {
445    /// Hash index for equality lookups.
446    Hash,
447    /// BTree index for range queries.
448    BTree,
449    /// Full-text index for text search.
450    FullText,
451}
452
453/// Index definition.
454#[derive(Debug, Clone)]
455pub struct IndexDefinition {
456    /// The index ID.
457    pub id: IndexId,
458    /// The label this index applies to.
459    pub label: LabelId,
460    /// The property key being indexed.
461    pub property_key: PropertyKeyId,
462    /// The type of index.
463    pub index_type: IndexType,
464}
465
466/// Manages index definitions.
467struct IndexCatalog {
468    indexes: RwLock<HashMap<IndexId, IndexDefinition>>,
469    label_indexes: RwLock<HashMap<LabelId, Vec<IndexId>>>,
470    label_property_indexes: RwLock<HashMap<(LabelId, PropertyKeyId), Vec<IndexId>>>,
471    next_id: AtomicU32,
472}
473
474impl IndexCatalog {
475    fn new() -> Self {
476        Self {
477            indexes: RwLock::new(HashMap::new()),
478            label_indexes: RwLock::new(HashMap::new()),
479            label_property_indexes: RwLock::new(HashMap::new()),
480            next_id: AtomicU32::new(0),
481        }
482    }
483
484    fn create(
485        &self,
486        label: LabelId,
487        property_key: PropertyKeyId,
488        index_type: IndexType,
489    ) -> IndexId {
490        let id = IndexId::new(self.next_id.fetch_add(1, Ordering::Relaxed));
491        let definition = IndexDefinition {
492            id,
493            label,
494            property_key,
495            index_type,
496        };
497
498        let mut indexes = self.indexes.write();
499        let mut label_indexes = self.label_indexes.write();
500        let mut label_property_indexes = self.label_property_indexes.write();
501
502        indexes.insert(id, definition);
503        label_indexes.entry(label).or_default().push(id);
504        label_property_indexes
505            .entry((label, property_key))
506            .or_default()
507            .push(id);
508
509        id
510    }
511
512    fn drop(&self, id: IndexId) -> bool {
513        let mut indexes = self.indexes.write();
514        let mut label_indexes = self.label_indexes.write();
515        let mut label_property_indexes = self.label_property_indexes.write();
516
517        if let Some(definition) = indexes.remove(&id) {
518            // Remove from label index
519            if let Some(ids) = label_indexes.get_mut(&definition.label) {
520                ids.retain(|&i| i != id);
521            }
522            // Remove from label-property index
523            if let Some(ids) =
524                label_property_indexes.get_mut(&(definition.label, definition.property_key))
525            {
526                ids.retain(|&i| i != id);
527            }
528            true
529        } else {
530            false
531        }
532    }
533
534    fn get(&self, id: IndexId) -> Option<IndexDefinition> {
535        self.indexes.read().get(&id).cloned()
536    }
537
538    fn for_label(&self, label: LabelId) -> Vec<IndexId> {
539        self.label_indexes
540            .read()
541            .get(&label)
542            .cloned()
543            .unwrap_or_default()
544    }
545
546    fn for_label_property(&self, label: LabelId, property_key: PropertyKeyId) -> Vec<IndexId> {
547        self.label_property_indexes
548            .read()
549            .get(&(label, property_key))
550            .cloned()
551            .unwrap_or_default()
552    }
553
554    fn count(&self) -> usize {
555        self.indexes.read().len()
556    }
557}
558
559// === Schema Catalog ===
560
561/// Schema constraints.
562struct SchemaCatalog {
563    /// Properties that must be unique for a given label.
564    unique_constraints: RwLock<HashMap<(LabelId, PropertyKeyId), ()>>,
565    /// Properties that are required (NOT NULL) for a given label.
566    required_properties: RwLock<HashMap<(LabelId, PropertyKeyId), ()>>,
567}
568
569impl SchemaCatalog {
570    fn new() -> Self {
571        Self {
572            unique_constraints: RwLock::new(HashMap::new()),
573            required_properties: RwLock::new(HashMap::new()),
574        }
575    }
576
577    fn add_unique_constraint(
578        &self,
579        label: LabelId,
580        property_key: PropertyKeyId,
581    ) -> Result<(), CatalogError> {
582        let mut constraints = self.unique_constraints.write();
583        let key = (label, property_key);
584        if constraints.contains_key(&key) {
585            return Err(CatalogError::ConstraintAlreadyExists);
586        }
587        constraints.insert(key, ());
588        Ok(())
589    }
590
591    fn add_required_property(
592        &self,
593        label: LabelId,
594        property_key: PropertyKeyId,
595    ) -> Result<(), CatalogError> {
596        let mut required = self.required_properties.write();
597        let key = (label, property_key);
598        if required.contains_key(&key) {
599            return Err(CatalogError::ConstraintAlreadyExists);
600        }
601        required.insert(key, ());
602        Ok(())
603    }
604
605    fn is_property_required(&self, label: LabelId, property_key: PropertyKeyId) -> bool {
606        self.required_properties
607            .read()
608            .contains_key(&(label, property_key))
609    }
610
611    fn is_property_unique(&self, label: LabelId, property_key: PropertyKeyId) -> bool {
612        self.unique_constraints
613            .read()
614            .contains_key(&(label, property_key))
615    }
616}
617
618// === Errors ===
619
620/// Catalog-related errors.
621#[derive(Debug, Clone, PartialEq, Eq)]
622pub enum CatalogError {
623    /// Schema constraints are not enabled.
624    SchemaNotEnabled,
625    /// The constraint already exists.
626    ConstraintAlreadyExists,
627    /// The label does not exist.
628    LabelNotFound(String),
629    /// The property key does not exist.
630    PropertyKeyNotFound(String),
631    /// The edge type does not exist.
632    EdgeTypeNotFound(String),
633    /// The index does not exist.
634    IndexNotFound(IndexId),
635}
636
637impl std::fmt::Display for CatalogError {
638    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
639        match self {
640            Self::SchemaNotEnabled => write!(f, "Schema constraints are not enabled"),
641            Self::ConstraintAlreadyExists => write!(f, "Constraint already exists"),
642            Self::LabelNotFound(name) => write!(f, "Label not found: {name}"),
643            Self::PropertyKeyNotFound(name) => write!(f, "Property key not found: {name}"),
644            Self::EdgeTypeNotFound(name) => write!(f, "Edge type not found: {name}"),
645            Self::IndexNotFound(id) => write!(f, "Index not found: {id}"),
646        }
647    }
648}
649
650impl std::error::Error for CatalogError {}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655    use std::thread;
656
657    #[test]
658    fn test_catalog_labels() {
659        let catalog = Catalog::new();
660
661        // Get or create labels
662        let person_id = catalog.get_or_create_label("Person");
663        let company_id = catalog.get_or_create_label("Company");
664
665        // IDs should be different
666        assert_ne!(person_id, company_id);
667
668        // Getting the same label should return the same ID
669        assert_eq!(catalog.get_or_create_label("Person"), person_id);
670
671        // Should be able to look up by name
672        assert_eq!(catalog.get_label_id("Person"), Some(person_id));
673        assert_eq!(catalog.get_label_id("Company"), Some(company_id));
674        assert_eq!(catalog.get_label_id("Unknown"), None);
675
676        // Should be able to look up by ID
677        assert_eq!(catalog.get_label_name(person_id).as_deref(), Some("Person"));
678        assert_eq!(
679            catalog.get_label_name(company_id).as_deref(),
680            Some("Company")
681        );
682
683        // Count should be correct
684        assert_eq!(catalog.label_count(), 2);
685    }
686
687    #[test]
688    fn test_catalog_property_keys() {
689        let catalog = Catalog::new();
690
691        let name_id = catalog.get_or_create_property_key("name");
692        let age_id = catalog.get_or_create_property_key("age");
693
694        assert_ne!(name_id, age_id);
695        assert_eq!(catalog.get_or_create_property_key("name"), name_id);
696        assert_eq!(catalog.get_property_key_id("name"), Some(name_id));
697        assert_eq!(
698            catalog.get_property_key_name(name_id).as_deref(),
699            Some("name")
700        );
701        assert_eq!(catalog.property_key_count(), 2);
702    }
703
704    #[test]
705    fn test_catalog_edge_types() {
706        let catalog = Catalog::new();
707
708        let knows_id = catalog.get_or_create_edge_type("KNOWS");
709        let works_at_id = catalog.get_or_create_edge_type("WORKS_AT");
710
711        assert_ne!(knows_id, works_at_id);
712        assert_eq!(catalog.get_or_create_edge_type("KNOWS"), knows_id);
713        assert_eq!(catalog.get_edge_type_id("KNOWS"), Some(knows_id));
714        assert_eq!(
715            catalog.get_edge_type_name(knows_id).as_deref(),
716            Some("KNOWS")
717        );
718        assert_eq!(catalog.edge_type_count(), 2);
719    }
720
721    #[test]
722    fn test_catalog_indexes() {
723        let catalog = Catalog::new();
724
725        let person_id = catalog.get_or_create_label("Person");
726        let name_id = catalog.get_or_create_property_key("name");
727        let age_id = catalog.get_or_create_property_key("age");
728
729        // Create indexes
730        let idx1 = catalog.create_index(person_id, name_id, IndexType::Hash);
731        let idx2 = catalog.create_index(person_id, age_id, IndexType::BTree);
732
733        assert_ne!(idx1, idx2);
734        assert_eq!(catalog.index_count(), 2);
735
736        // Look up by label
737        let label_indexes = catalog.indexes_for_label(person_id);
738        assert_eq!(label_indexes.len(), 2);
739        assert!(label_indexes.contains(&idx1));
740        assert!(label_indexes.contains(&idx2));
741
742        // Look up by label and property
743        let name_indexes = catalog.indexes_for_label_property(person_id, name_id);
744        assert_eq!(name_indexes.len(), 1);
745        assert_eq!(name_indexes[0], idx1);
746
747        // Get definition
748        let def = catalog.get_index(idx1).unwrap();
749        assert_eq!(def.label, person_id);
750        assert_eq!(def.property_key, name_id);
751        assert_eq!(def.index_type, IndexType::Hash);
752
753        // Drop index
754        assert!(catalog.drop_index(idx1));
755        assert_eq!(catalog.index_count(), 1);
756        assert!(catalog.get_index(idx1).is_none());
757        assert_eq!(catalog.indexes_for_label(person_id).len(), 1);
758    }
759
760    #[test]
761    fn test_catalog_schema_constraints() {
762        let catalog = Catalog::with_schema();
763
764        let person_id = catalog.get_or_create_label("Person");
765        let email_id = catalog.get_or_create_property_key("email");
766        let name_id = catalog.get_or_create_property_key("name");
767
768        // Add constraints
769        assert!(catalog.add_unique_constraint(person_id, email_id).is_ok());
770        assert!(catalog.add_required_property(person_id, name_id).is_ok());
771
772        // Check constraints
773        assert!(catalog.is_property_unique(person_id, email_id));
774        assert!(!catalog.is_property_unique(person_id, name_id));
775        assert!(catalog.is_property_required(person_id, name_id));
776        assert!(!catalog.is_property_required(person_id, email_id));
777
778        // Duplicate constraint should fail
779        assert_eq!(
780            catalog.add_unique_constraint(person_id, email_id),
781            Err(CatalogError::ConstraintAlreadyExists)
782        );
783    }
784
785    #[test]
786    fn test_catalog_no_schema() {
787        let catalog = Catalog::new();
788
789        let person_id = catalog.get_or_create_label("Person");
790        let email_id = catalog.get_or_create_property_key("email");
791
792        // Should fail without schema
793        assert_eq!(
794            catalog.add_unique_constraint(person_id, email_id),
795            Err(CatalogError::SchemaNotEnabled)
796        );
797    }
798
799    // === Additional tests for comprehensive coverage ===
800
801    #[test]
802    fn test_catalog_default() {
803        let catalog = Catalog::default();
804        assert!(!catalog.has_schema());
805        assert_eq!(catalog.label_count(), 0);
806        assert_eq!(catalog.property_key_count(), 0);
807        assert_eq!(catalog.edge_type_count(), 0);
808        assert_eq!(catalog.index_count(), 0);
809    }
810
811    #[test]
812    fn test_catalog_all_labels() {
813        let catalog = Catalog::new();
814
815        catalog.get_or_create_label("Person");
816        catalog.get_or_create_label("Company");
817        catalog.get_or_create_label("Product");
818
819        let all = catalog.all_labels();
820        assert_eq!(all.len(), 3);
821        assert!(all.iter().any(|l| l.as_ref() == "Person"));
822        assert!(all.iter().any(|l| l.as_ref() == "Company"));
823        assert!(all.iter().any(|l| l.as_ref() == "Product"));
824    }
825
826    #[test]
827    fn test_catalog_all_property_keys() {
828        let catalog = Catalog::new();
829
830        catalog.get_or_create_property_key("name");
831        catalog.get_or_create_property_key("age");
832        catalog.get_or_create_property_key("email");
833
834        let all = catalog.all_property_keys();
835        assert_eq!(all.len(), 3);
836        assert!(all.iter().any(|k| k.as_ref() == "name"));
837        assert!(all.iter().any(|k| k.as_ref() == "age"));
838        assert!(all.iter().any(|k| k.as_ref() == "email"));
839    }
840
841    #[test]
842    fn test_catalog_all_edge_types() {
843        let catalog = Catalog::new();
844
845        catalog.get_or_create_edge_type("KNOWS");
846        catalog.get_or_create_edge_type("WORKS_AT");
847        catalog.get_or_create_edge_type("LIVES_IN");
848
849        let all = catalog.all_edge_types();
850        assert_eq!(all.len(), 3);
851        assert!(all.iter().any(|t| t.as_ref() == "KNOWS"));
852        assert!(all.iter().any(|t| t.as_ref() == "WORKS_AT"));
853        assert!(all.iter().any(|t| t.as_ref() == "LIVES_IN"));
854    }
855
856    #[test]
857    fn test_catalog_invalid_id_lookup() {
858        let catalog = Catalog::new();
859
860        // Create one label to ensure IDs are allocated
861        let _ = catalog.get_or_create_label("Person");
862
863        // Try to look up non-existent IDs
864        let invalid_label = LabelId::new(999);
865        let invalid_property = PropertyKeyId::new(999);
866        let invalid_edge_type = EdgeTypeId::new(999);
867        let invalid_index = IndexId::new(999);
868
869        assert!(catalog.get_label_name(invalid_label).is_none());
870        assert!(catalog.get_property_key_name(invalid_property).is_none());
871        assert!(catalog.get_edge_type_name(invalid_edge_type).is_none());
872        assert!(catalog.get_index(invalid_index).is_none());
873    }
874
875    #[test]
876    fn test_catalog_drop_nonexistent_index() {
877        let catalog = Catalog::new();
878        let invalid_index = IndexId::new(999);
879        assert!(!catalog.drop_index(invalid_index));
880    }
881
882    #[test]
883    fn test_catalog_indexes_for_nonexistent_label() {
884        let catalog = Catalog::new();
885        let invalid_label = LabelId::new(999);
886        let invalid_property = PropertyKeyId::new(999);
887
888        assert!(catalog.indexes_for_label(invalid_label).is_empty());
889        assert!(
890            catalog
891                .indexes_for_label_property(invalid_label, invalid_property)
892                .is_empty()
893        );
894    }
895
896    #[test]
897    fn test_catalog_multiple_indexes_same_property() {
898        let catalog = Catalog::new();
899
900        let person_id = catalog.get_or_create_label("Person");
901        let name_id = catalog.get_or_create_property_key("name");
902
903        // Create multiple indexes on the same property with different types
904        let hash_idx = catalog.create_index(person_id, name_id, IndexType::Hash);
905        let btree_idx = catalog.create_index(person_id, name_id, IndexType::BTree);
906        let fulltext_idx = catalog.create_index(person_id, name_id, IndexType::FullText);
907
908        assert_eq!(catalog.index_count(), 3);
909
910        let indexes = catalog.indexes_for_label_property(person_id, name_id);
911        assert_eq!(indexes.len(), 3);
912        assert!(indexes.contains(&hash_idx));
913        assert!(indexes.contains(&btree_idx));
914        assert!(indexes.contains(&fulltext_idx));
915
916        // Verify each has the correct type
917        assert_eq!(
918            catalog.get_index(hash_idx).unwrap().index_type,
919            IndexType::Hash
920        );
921        assert_eq!(
922            catalog.get_index(btree_idx).unwrap().index_type,
923            IndexType::BTree
924        );
925        assert_eq!(
926            catalog.get_index(fulltext_idx).unwrap().index_type,
927            IndexType::FullText
928        );
929    }
930
931    #[test]
932    fn test_catalog_schema_required_property_duplicate() {
933        let catalog = Catalog::with_schema();
934
935        let person_id = catalog.get_or_create_label("Person");
936        let name_id = catalog.get_or_create_property_key("name");
937
938        // First should succeed
939        assert!(catalog.add_required_property(person_id, name_id).is_ok());
940
941        // Duplicate should fail
942        assert_eq!(
943            catalog.add_required_property(person_id, name_id),
944            Err(CatalogError::ConstraintAlreadyExists)
945        );
946    }
947
948    #[test]
949    fn test_catalog_schema_check_without_constraints() {
950        let catalog = Catalog::new();
951
952        let person_id = catalog.get_or_create_label("Person");
953        let name_id = catalog.get_or_create_property_key("name");
954
955        // Without schema enabled, these should return false
956        assert!(!catalog.is_property_unique(person_id, name_id));
957        assert!(!catalog.is_property_required(person_id, name_id));
958    }
959
960    #[test]
961    fn test_catalog_has_schema() {
962        let without_schema = Catalog::new();
963        assert!(!without_schema.has_schema());
964
965        let with_schema = Catalog::with_schema();
966        assert!(with_schema.has_schema());
967    }
968
969    #[test]
970    fn test_catalog_error_display() {
971        assert_eq!(
972            CatalogError::SchemaNotEnabled.to_string(),
973            "Schema constraints are not enabled"
974        );
975        assert_eq!(
976            CatalogError::ConstraintAlreadyExists.to_string(),
977            "Constraint already exists"
978        );
979        assert_eq!(
980            CatalogError::LabelNotFound("Person".to_string()).to_string(),
981            "Label not found: Person"
982        );
983        assert_eq!(
984            CatalogError::PropertyKeyNotFound("name".to_string()).to_string(),
985            "Property key not found: name"
986        );
987        assert_eq!(
988            CatalogError::EdgeTypeNotFound("KNOWS".to_string()).to_string(),
989            "Edge type not found: KNOWS"
990        );
991        let idx = IndexId::new(42);
992        assert!(CatalogError::IndexNotFound(idx).to_string().contains("42"));
993    }
994
995    #[test]
996    fn test_catalog_concurrent_label_creation() {
997        use std::sync::Arc;
998
999        let catalog = Arc::new(Catalog::new());
1000        let mut handles = vec![];
1001
1002        // Spawn multiple threads trying to create the same labels
1003        for i in 0..10 {
1004            let catalog = Arc::clone(&catalog);
1005            handles.push(thread::spawn(move || {
1006                let label_name = format!("Label{}", i % 3); // Only 3 unique labels
1007                catalog.get_or_create_label(&label_name)
1008            }));
1009        }
1010
1011        let mut ids: Vec<LabelId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1012        ids.sort_by_key(|id| id.as_u32());
1013        ids.dedup();
1014
1015        // Should only have 3 unique label IDs
1016        assert_eq!(ids.len(), 3);
1017        assert_eq!(catalog.label_count(), 3);
1018    }
1019
1020    #[test]
1021    fn test_catalog_concurrent_property_key_creation() {
1022        use std::sync::Arc;
1023
1024        let catalog = Arc::new(Catalog::new());
1025        let mut handles = vec![];
1026
1027        for i in 0..10 {
1028            let catalog = Arc::clone(&catalog);
1029            handles.push(thread::spawn(move || {
1030                let key_name = format!("key{}", i % 4);
1031                catalog.get_or_create_property_key(&key_name)
1032            }));
1033        }
1034
1035        let mut ids: Vec<PropertyKeyId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1036        ids.sort_by_key(|id| id.as_u32());
1037        ids.dedup();
1038
1039        assert_eq!(ids.len(), 4);
1040        assert_eq!(catalog.property_key_count(), 4);
1041    }
1042
1043    #[test]
1044    fn test_catalog_concurrent_index_operations() {
1045        use std::sync::Arc;
1046
1047        let catalog = Arc::new(Catalog::new());
1048        let label = catalog.get_or_create_label("Node");
1049
1050        let mut handles = vec![];
1051
1052        // Create indexes concurrently
1053        for i in 0..5 {
1054            let catalog = Arc::clone(&catalog);
1055            handles.push(thread::spawn(move || {
1056                let prop = PropertyKeyId::new(i);
1057                catalog.create_index(label, prop, IndexType::Hash)
1058            }));
1059        }
1060
1061        let ids: Vec<IndexId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1062        assert_eq!(ids.len(), 5);
1063        assert_eq!(catalog.index_count(), 5);
1064    }
1065
1066    #[test]
1067    fn test_catalog_special_characters_in_names() {
1068        let catalog = Catalog::new();
1069
1070        // Test with various special characters
1071        let label1 = catalog.get_or_create_label("Label With Spaces");
1072        let label2 = catalog.get_or_create_label("Label-With-Dashes");
1073        let label3 = catalog.get_or_create_label("Label_With_Underscores");
1074        let label4 = catalog.get_or_create_label("LabelWithUnicode\u{00E9}");
1075
1076        assert_ne!(label1, label2);
1077        assert_ne!(label2, label3);
1078        assert_ne!(label3, label4);
1079
1080        assert_eq!(
1081            catalog.get_label_name(label1).as_deref(),
1082            Some("Label With Spaces")
1083        );
1084        assert_eq!(
1085            catalog.get_label_name(label4).as_deref(),
1086            Some("LabelWithUnicode\u{00E9}")
1087        );
1088    }
1089
1090    #[test]
1091    fn test_catalog_empty_names() {
1092        let catalog = Catalog::new();
1093
1094        // Empty names should be valid (edge case)
1095        let empty_label = catalog.get_or_create_label("");
1096        let empty_prop = catalog.get_or_create_property_key("");
1097        let empty_edge = catalog.get_or_create_edge_type("");
1098
1099        assert_eq!(catalog.get_label_name(empty_label).as_deref(), Some(""));
1100        assert_eq!(
1101            catalog.get_property_key_name(empty_prop).as_deref(),
1102            Some("")
1103        );
1104        assert_eq!(catalog.get_edge_type_name(empty_edge).as_deref(), Some(""));
1105
1106        // Calling again should return same ID
1107        assert_eq!(catalog.get_or_create_label(""), empty_label);
1108    }
1109
1110    #[test]
1111    fn test_catalog_large_number_of_entries() {
1112        let catalog = Catalog::new();
1113
1114        // Create many labels
1115        for i in 0..1000 {
1116            catalog.get_or_create_label(&format!("Label{}", i));
1117        }
1118
1119        assert_eq!(catalog.label_count(), 1000);
1120
1121        // Verify we can retrieve them all
1122        let all = catalog.all_labels();
1123        assert_eq!(all.len(), 1000);
1124
1125        // Verify a specific one
1126        let id = catalog.get_label_id("Label500").unwrap();
1127        assert_eq!(catalog.get_label_name(id).as_deref(), Some("Label500"));
1128    }
1129
1130    #[test]
1131    fn test_index_definition_debug() {
1132        let def = IndexDefinition {
1133            id: IndexId::new(1),
1134            label: LabelId::new(2),
1135            property_key: PropertyKeyId::new(3),
1136            index_type: IndexType::Hash,
1137        };
1138
1139        // Should be able to debug print
1140        let debug_str = format!("{:?}", def);
1141        assert!(debug_str.contains("IndexDefinition"));
1142        assert!(debug_str.contains("Hash"));
1143    }
1144
1145    #[test]
1146    fn test_index_type_equality() {
1147        assert_eq!(IndexType::Hash, IndexType::Hash);
1148        assert_ne!(IndexType::Hash, IndexType::BTree);
1149        assert_ne!(IndexType::BTree, IndexType::FullText);
1150
1151        // Clone
1152        let t = IndexType::Hash;
1153        let t2 = t;
1154        assert_eq!(t, t2);
1155    }
1156
1157    #[test]
1158    fn test_catalog_error_equality() {
1159        assert_eq!(
1160            CatalogError::SchemaNotEnabled,
1161            CatalogError::SchemaNotEnabled
1162        );
1163        assert_eq!(
1164            CatalogError::ConstraintAlreadyExists,
1165            CatalogError::ConstraintAlreadyExists
1166        );
1167        assert_eq!(
1168            CatalogError::LabelNotFound("X".to_string()),
1169            CatalogError::LabelNotFound("X".to_string())
1170        );
1171        assert_ne!(
1172            CatalogError::LabelNotFound("X".to_string()),
1173            CatalogError::LabelNotFound("Y".to_string())
1174        );
1175    }
1176}