Skip to main content

radixdb_core/
schema.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Schema types for RadixDB - table and column definitions
16//!
17//! This module defines SchemaColumn and Schema types for table structure.
18
19use std::fmt;
20use std::sync::{Arc, OnceLock};
21
22use ahash::{AHashMap, AHashSet};
23use chrono::{DateTime, Utc};
24use sha2::{Digest, Sha256};
25
26use crate::{
27    CompactArc, DataType, Error, ExternalTypeRef, ForeignKeyAction, LogicalTypeRef, Result,
28    SchemaColumnId, Value,
29};
30
31type StringMap<V> = AHashMap<String, V>;
32type StringSet = AHashSet<String>;
33
34impl crate::RowSchema for Schema {
35    fn row_column_count(&self) -> usize {
36        self.columns.len()
37    }
38
39    fn row_column(&self, index: usize) -> Option<crate::RowColumnRef<'_>> {
40        self.columns.get(index).map(|column| {
41            crate::RowColumnRef::new(&column.name, column.data_type, column.nullable)
42                .with_logical_type(column.logical_type())
43        })
44    }
45}
46
47/// A column definition in a table schema
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SchemaColumn {
50    /// Unique identifier for the column (0-based index)
51    pub id: usize,
52
53    /// Column name
54    pub name: String,
55
56    /// Pre-computed lowercase column name for case-insensitive lookups
57    #[doc(hidden)]
58    pub name_lower: String,
59
60    /// Data type of the column
61    pub data_type: DataType,
62
63    /// Stable external type identity. Built-in columns keep this empty.
64    pub external_type: Option<ExternalTypeRef>,
65
66    /// Schema-qualified SQL type name used by introspection and dump output.
67    pub external_type_name: Option<String>,
68
69    /// Whether the column can contain NULL values
70    pub nullable: bool,
71
72    /// Whether this column is part of the primary key
73    pub primary_key: bool,
74
75    /// Whether this column auto-increments (generates sequential IDs for NULL values)
76    pub auto_increment: bool,
77
78    /// Default value expression as a string (to be parsed and evaluated during INSERT)
79    pub default_expr: Option<String>,
80
81    /// Pre-computed default value for schema evolution (used when adding column to existing rows)
82    pub default_value: Option<Value>,
83
84    /// CHECK constraint expression as a string (to be parsed and evaluated during INSERT)
85    pub check_expr: Option<String>,
86
87    /// Number of dimensions for VECTOR columns (0 = not a vector column)
88    pub vector_dimensions: u16,
89
90    /// Declared precision for DECIMAL columns (0 = unconstrained DECIMAL).
91    pub decimal_precision: u8,
92
93    /// Declared scale for DECIMAL columns. This is zero when precision is zero.
94    pub decimal_scale: u8,
95}
96
97impl SchemaColumn {
98    /// Create a new column definition
99    pub fn new(
100        id: usize,
101        name: impl Into<String>,
102        data_type: DataType,
103        nullable: bool,
104        primary_key: bool,
105    ) -> Self {
106        let name_str = name.into();
107        let name_lower = name_str.to_lowercase();
108        Self {
109            id,
110            name: name_str,
111            name_lower,
112            data_type,
113            external_type: None,
114            external_type_name: None,
115            nullable,
116            primary_key,
117            auto_increment: false,
118            default_expr: None,
119            default_value: None,
120            check_expr: None,
121            vector_dimensions: 0,
122            decimal_precision: 0,
123            decimal_scale: 0,
124        }
125    }
126
127    /// Set vector dimensions (for VECTOR columns)
128    pub fn with_vector_dimensions(mut self, dims: u16) -> Self {
129        self.vector_dimensions = dims;
130        self
131    }
132
133    /// Set exact DECIMAL(p,s) column parameters. A precision of zero denotes
134    /// the unparameterized DECIMAL domain and requires scale zero.
135    pub fn with_decimal_parameters(mut self, precision: u8, scale: u8) -> Self {
136        self.decimal_precision = precision;
137        self.decimal_scale = scale;
138        self
139    }
140
141    pub fn with_external_type(
142        mut self,
143        type_ref: ExternalTypeRef,
144        sql_name: impl Into<String>,
145    ) -> Self {
146        self.data_type = DataType::Null;
147        self.external_type = Some(type_ref);
148        self.external_type_name = Some(sql_name.into());
149        self
150    }
151
152    pub const fn logical_type(&self) -> LogicalTypeRef {
153        match self.external_type {
154            Some(type_ref) => LogicalTypeRef::External(type_ref),
155            None => LogicalTypeRef::Builtin(self.data_type),
156        }
157    }
158
159    /// Render the complete declared SQL type, including durable modifiers.
160    pub fn formatted_data_type(&self) -> String {
161        if let Some(name) = &self.external_type_name {
162            name.clone()
163        } else if self.data_type == DataType::Vector && self.vector_dimensions > 0 {
164            format!("VECTOR({})", self.vector_dimensions)
165        } else if self.data_type == DataType::Decimal && self.decimal_precision > 0 {
166            format!("DECIMAL({},{})", self.decimal_precision, self.decimal_scale)
167        } else {
168            self.data_type.to_string()
169        }
170    }
171
172    /// Exact schema-level type identity used by references and descriptors.
173    pub fn has_same_declared_type(&self, other: &Self) -> bool {
174        self.data_type == other.data_type
175            && self.external_type == other.external_type
176            && self.vector_dimensions == other.vector_dimensions
177            && self.decimal_precision == other.decimal_precision
178            && self.decimal_scale == other.decimal_scale
179    }
180
181    /// Validate value-level modifiers that are not represented by [`DataType`].
182    /// The value is not rewritten: exact decimal payload bytes remain intact.
183    pub fn validate_declared_value(&self, value: &Value) -> Result<()> {
184        if value.is_null() {
185            return Ok(());
186        }
187        if let Some(type_ref) = self.external_type {
188            if value.logical_type() != LogicalTypeRef::External(type_ref) {
189                return Err(Error::Type(format!(
190                    "value for column '{}' has the wrong external type identity",
191                    self.name
192                )));
193            }
194            return Ok(());
195        }
196        if self.data_type != DataType::Decimal || self.decimal_precision == 0 {
197            return Ok(());
198        }
199        let (unscaled, _, mut scale) = value.as_decimal_parts().ok_or_else(|| {
200            Error::Type(format!(
201                "value for column '{}' is not a valid DECIMAL payload",
202                self.name
203            ))
204        })?;
205        let mut magnitude = unscaled.unsigned_abs();
206        while scale > self.decimal_scale && magnitude % 10 == 0 {
207            magnitude /= 10;
208            scale -= 1;
209        }
210        if scale > self.decimal_scale {
211            return Err(Error::Type(format!(
212                "DECIMAL value for column '{}' has scale {}, exceeding declared scale {}",
213                self.name, scale, self.decimal_scale
214            )));
215        }
216        let coefficient_digits = if magnitude == 0 {
217            1
218        } else {
219            magnitude.ilog10() as usize + 1
220        };
221        let integer_digits = coefficient_digits.saturating_sub(usize::from(scale));
222        let allowed_integer_digits = usize::from(self.decimal_precision - self.decimal_scale);
223        if integer_digits > allowed_integer_digits {
224            return Err(Error::Type(format!(
225                "DECIMAL value for column '{}' exceeds declared precision {} and scale {}",
226                self.name, self.decimal_precision, self.decimal_scale
227            )));
228        }
229        Ok(())
230    }
231
232    /// Create a new column definition with all options
233    #[allow(clippy::too_many_arguments)]
234    pub fn with_constraints(
235        id: usize,
236        name: impl Into<String>,
237        data_type: DataType,
238        nullable: bool,
239        primary_key: bool,
240        auto_increment: bool,
241        default_expr: Option<String>,
242        check_expr: Option<String>,
243    ) -> Self {
244        let name_str = name.into();
245        let name_lower = name_str.to_lowercase();
246        Self {
247            id,
248            name: name_str,
249            name_lower,
250            data_type,
251            external_type: None,
252            external_type_name: None,
253            nullable,
254            primary_key,
255            auto_increment,
256            default_expr,
257            default_value: None,
258            check_expr,
259            vector_dimensions: 0,
260            decimal_precision: 0,
261            decimal_scale: 0,
262        }
263    }
264
265    /// Create a new column definition with pre-computed default value
266    #[allow(clippy::too_many_arguments)]
267    pub fn with_default_value(
268        id: usize,
269        name: impl Into<String>,
270        data_type: DataType,
271        nullable: bool,
272        primary_key: bool,
273        auto_increment: bool,
274        default_expr: Option<String>,
275        default_value: Option<Value>,
276        check_expr: Option<String>,
277    ) -> Self {
278        let name_str = name.into();
279        let name_lower = name_str.to_lowercase();
280        Self {
281            id,
282            name: name_str,
283            name_lower,
284            data_type,
285            external_type: None,
286            external_type_name: None,
287            nullable,
288            primary_key,
289            auto_increment,
290            default_expr,
291            default_value,
292            check_expr,
293            vector_dimensions: 0,
294            decimal_precision: 0,
295            decimal_scale: 0,
296        }
297    }
298
299    /// Create a simple non-nullable, non-primary-key column
300    pub fn simple(id: usize, name: impl Into<String>, data_type: DataType) -> Self {
301        Self::new(id, name, data_type, false, false)
302    }
303
304    /// Create a nullable column
305    pub fn nullable(id: usize, name: impl Into<String>, data_type: DataType) -> Self {
306        Self::new(id, name, data_type, true, false)
307    }
308
309    /// Create a primary key column
310    pub fn primary_key(id: usize, name: impl Into<String>, data_type: DataType) -> Self {
311        Self::new(id, name, data_type, false, true)
312    }
313}
314
315impl fmt::Display for SchemaColumn {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        write!(f, "{} {}", self.name, self.formatted_data_type())?;
318        if self.primary_key {
319            write!(f, " PRIMARY KEY")?;
320        }
321        if !self.nullable && !self.primary_key {
322            write!(f, " NOT NULL")?;
323        }
324        Ok(())
325    }
326}
327
328/// Foreign key constraint metadata
329#[derive(Debug, Clone, PartialEq, Eq)]
330pub struct ForeignKeyConstraint {
331    /// Index of the FK column in this table's schema
332    pub column_index: usize,
333    /// FK column name (for error messages)
334    pub column_name: String,
335    /// Referenced parent table name (lowercase)
336    pub referenced_table: String,
337    /// Referenced parent column name (lowercase)
338    pub referenced_column: String,
339    /// Action when parent row is deleted
340    pub on_delete: ForeignKeyAction,
341    /// Action when parent PK is updated
342    pub on_update: ForeignKeyAction,
343}
344
345/// Durable catalog identity for a SQL table constraint.
346///
347/// `id` and `name` are assigned once when the constraint is created and are
348/// persisted with the schema.  Renaming a table or column updates the bound
349/// columns in `kind`, but deliberately does not rename the public constraint.
350#[derive(Debug, Clone, PartialEq, Eq)]
351pub struct SchemaConstraint {
352    pub id: u64,
353    pub name: String,
354    pub kind: SchemaConstraintKind,
355}
356
357/// Complete, catalog-visible constraint metadata.
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub enum SchemaConstraintKind {
360    PrimaryKey {
361        columns: Vec<String>,
362    },
363    Unique {
364        columns: Vec<String>,
365        /// Physical index owned by this UNIQUE constraint.
366        index_name: String,
367    },
368    ForeignKey {
369        columns: Vec<String>,
370        referenced_table: String,
371        referenced_columns: Vec<String>,
372        on_delete: ForeignKeyAction,
373        on_update: ForeignKeyAction,
374    },
375    Check {
376        column_name: Option<String>,
377        expression: String,
378        /// Monotonic table-local CHECK ordinal used by the public name.
379        ordinal: u32,
380    },
381}
382
383impl SchemaConstraintKind {
384    pub fn columns(&self) -> &[String] {
385        match self {
386            Self::PrimaryKey { columns }
387            | Self::Unique { columns, .. }
388            | Self::ForeignKey { columns, .. } => columns,
389            Self::Check {
390                column_name: Some(column_name),
391                ..
392            } => std::slice::from_ref(column_name),
393            Self::Check {
394                column_name: None, ..
395            } => &[],
396        }
397    }
398}
399
400/// Public constraint identifiers use the same bounded representation in every
401/// parser/catalog/descriptor path.
402pub const MAX_CONSTRAINT_NAME_BYTES: usize = 128;
403const CONSTRAINT_HASH_SUFFIX_BYTES: usize = 10; // "__" + eight hex digits
404const CONSTRAINT_NAME_DESCRIPTOR_PREFIX: &str = "radixdb.constraint-name.v1";
405
406/// Proof used by navigation planning for one eligible reference target.
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408pub enum ReferenceTargetKey {
409    PrimaryKey,
410    UniqueNotNull,
411}
412
413/// Read-only schema capability for one source FK column.
414#[derive(Debug, Clone, PartialEq, Eq)]
415pub struct ReferenceDescriptor {
416    source: SchemaColumnId,
417    target: SchemaColumnId,
418    source_nullable: bool,
419    data_type: DataType,
420    target_key: ReferenceTargetKey,
421}
422
423impl ReferenceDescriptor {
424    #[doc(hidden)]
425    pub fn new(
426        source: SchemaColumnId,
427        target: SchemaColumnId,
428        source_nullable: bool,
429        data_type: DataType,
430        target_key: ReferenceTargetKey,
431    ) -> Self {
432        Self {
433            source,
434            target,
435            source_nullable,
436            data_type,
437            target_key,
438        }
439    }
440
441    pub fn source(&self) -> &SchemaColumnId {
442        &self.source
443    }
444
445    pub fn target(&self) -> &SchemaColumnId {
446        &self.target
447    }
448
449    pub fn source_nullable(&self) -> bool {
450        self.source_nullable
451    }
452
453    pub fn data_type(&self) -> DataType {
454        self.data_type
455    }
456
457    pub fn target_key(&self) -> ReferenceTargetKey {
458        self.target_key
459    }
460
461    pub fn schema_generation(&self) -> u64 {
462        self.source.table().schema_generation()
463    }
464}
465
466/// Table schema definition
467///
468
469#[derive(Debug)]
470pub struct Schema {
471    /// Durable table identity assigned once at engine admission. The all-zero
472    /// value is reserved for detached schemas that have not entered a catalog.
473    #[doc(hidden)]
474    pub catalog_id: [u8; 16],
475
476    /// Name of the table
477    #[doc(hidden)]
478    pub table_name: String,
479
480    /// Pre-computed lowercase table name for case-insensitive lookups
481    #[doc(hidden)]
482    pub table_name_lower: String,
483
484    /// Column definitions
485    #[doc(hidden)]
486    pub columns: Vec<SchemaColumn>,
487
488    /// Foreign key constraints
489    #[doc(hidden)]
490    pub foreign_keys: Vec<ForeignKeyConstraint>,
491
492    /// Table-level CHECK expressions evaluated against the complete row.
493    ///
494    /// These are distinct from `SchemaColumn::check_expr`: a table CHECK may
495    /// reference any number of columns and therefore cannot be evaluated with
496    /// a single-column row context.
497    #[doc(hidden)]
498    pub table_checks: Vec<String>,
499
500    /// Durable named constraint catalog. Enforcement remains in the existing
501    /// column/FK/CHECK/index owners; this list gives every owner a stable public
502    /// identity and records physical UNIQUE ownership.
503    #[doc(hidden)]
504    pub constraints: Vec<SchemaConstraint>,
505
506    /// Next stable constraint identity. IDs are never reused after DROP.
507    #[doc(hidden)]
508    pub next_constraint_id: u64,
509
510    /// Next table-local CHECK ordinal. Ordinals are never reused after DROP.
511    #[doc(hidden)]
512    pub next_check_ordinal: u32,
513
514    /// Creation timestamp
515    #[doc(hidden)]
516    pub created_at: DateTime<Utc>,
517
518    /// Last update timestamp
519    #[doc(hidden)]
520    pub updated_at: DateTime<Utc>,
521
522    /// Cached column names (computed lazily on first access, Arc for zero-copy sharing)
523    column_names_cache: OnceLock<CompactArc<Vec<String>>>,
524
525    /// Cached primary key column index (computed lazily on first access)
526    /// None means not computed yet, Some(None) means no PK, Some(Some(idx)) means PK at idx
527    pk_column_index_cache: OnceLock<Option<usize>>,
528
529    /// Cached column index map (lowercase name -> index) for O(1) column lookup
530    column_index_map_cache: OnceLock<StringMap<usize>>,
531
532    /// Cached primary key indices (computed lazily on first access)
533    pk_indices_cache: OnceLock<Arc<Vec<usize>>>,
534
535    /// Cached lowercase column names (computed lazily on first access)
536    column_names_lower_cache: OnceLock<CompactArc<Vec<String>>>,
537}
538
539impl Clone for Schema {
540    fn clone(&self) -> Self {
541        // Clone caches if already computed to avoid recomputation
542        let column_names_cache = OnceLock::new();
543        if let Some(names) = self.column_names_cache.get() {
544            // CompactArc clone is O(1) - just increments ref count
545            let _ = column_names_cache.set(CompactArc::clone(names));
546        }
547
548        let pk_column_index_cache = OnceLock::new();
549        if let Some(pk_idx) = self.pk_column_index_cache.get() {
550            let _ = pk_column_index_cache.set(*pk_idx);
551        }
552
553        let column_index_map_cache = OnceLock::new();
554        if let Some(map) = self.column_index_map_cache.get() {
555            let _ = column_index_map_cache.set(map.clone());
556        }
557
558        let pk_indices_cache = OnceLock::new();
559        if let Some(indices) = self.pk_indices_cache.get() {
560            let _ = pk_indices_cache.set(Arc::clone(indices));
561        }
562
563        let column_names_lower_cache = OnceLock::new();
564        if let Some(names) = self.column_names_lower_cache.get() {
565            let _ = column_names_lower_cache.set(CompactArc::clone(names));
566        }
567
568        Self {
569            catalog_id: self.catalog_id,
570            table_name: self.table_name.clone(),
571            table_name_lower: self.table_name_lower.clone(),
572            columns: self.columns.clone(),
573            foreign_keys: self.foreign_keys.clone(),
574            table_checks: self.table_checks.clone(),
575            constraints: self.constraints.clone(),
576            next_constraint_id: self.next_constraint_id,
577            next_check_ordinal: self.next_check_ordinal,
578            created_at: self.created_at,
579            updated_at: self.updated_at,
580            column_names_cache,
581            pk_column_index_cache,
582            column_index_map_cache,
583            pk_indices_cache,
584            column_names_lower_cache,
585        }
586    }
587}
588
589impl PartialEq for Schema {
590    fn eq(&self, other: &Self) -> bool {
591        self.catalog_id == other.catalog_id
592            && self.table_name == other.table_name
593            && self.columns == other.columns
594            && self.foreign_keys == other.foreign_keys
595            && self.table_checks == other.table_checks
596            && self.constraints == other.constraints
597            && self.next_constraint_id == other.next_constraint_id
598            && self.next_check_ordinal == other.next_check_ordinal
599            && self.created_at == other.created_at
600            && self.updated_at == other.updated_at
601    }
602}
603
604impl Eq for Schema {}
605
606impl Schema {
607    /// Create a new schema with the given table name and columns
608    pub fn new(table_name: impl Into<String>, columns: Vec<SchemaColumn>) -> Self {
609        Self::with_foreign_keys(table_name, columns, Vec::new())
610    }
611
612    /// Create a new schema with columns and foreign key constraints
613    pub fn with_foreign_keys(
614        table_name: impl Into<String>,
615        columns: Vec<SchemaColumn>,
616        foreign_keys: Vec<ForeignKeyConstraint>,
617    ) -> Self {
618        Self::with_constraints(table_name, columns, foreign_keys, Vec::new())
619    }
620
621    /// Create a new schema with all table-level constraints.
622    pub fn with_constraints(
623        table_name: impl Into<String>,
624        mut columns: Vec<SchemaColumn>,
625        foreign_keys: Vec<ForeignKeyConstraint>,
626        table_checks: Vec<String>,
627    ) -> Self {
628        let now = Utc::now();
629        let name = table_name.into();
630        let name_lower = name.to_lowercase();
631        normalize_columns(&mut columns);
632
633        // Eagerly compute caches to avoid recomputation on clone
634        let column_names_cache = OnceLock::new();
635        let _ = column_names_cache.set(CompactArc::new(
636            columns.iter().map(|c| c.name.clone()).collect(),
637        ));
638
639        let pk_column_index_cache = OnceLock::new();
640        let pk_idx = columns
641            .iter()
642            .enumerate()
643            .find(|(_, col)| col.primary_key && col.data_type == DataType::Integer)
644            .map(|(i, _)| i);
645        let _ = pk_column_index_cache.set(pk_idx);
646
647        let column_index_map_cache = OnceLock::new();
648        let _ = column_index_map_cache.set(
649            columns
650                .iter()
651                .enumerate()
652                .map(|(i, c)| (c.name_lower.clone(), i))
653                .collect(),
654        );
655
656        let pk_indices_cache = OnceLock::new();
657        let _ = pk_indices_cache.set(Arc::new(
658            columns
659                .iter()
660                .enumerate()
661                .filter(|(_, c)| c.primary_key)
662                .map(|(i, _)| i)
663                .collect(),
664        ));
665
666        let column_names_lower_cache = OnceLock::new();
667        let _ = column_names_lower_cache.set(CompactArc::new(
668            columns.iter().map(|c| c.name_lower.clone()).collect(),
669        ));
670
671        Self {
672            catalog_id: [0; 16],
673            table_name: name,
674            table_name_lower: name_lower,
675            columns,
676            foreign_keys,
677            table_checks,
678            constraints: Vec::new(),
679            next_constraint_id: 1,
680            next_check_ordinal: 1,
681            created_at: now,
682            updated_at: now,
683            column_names_cache,
684            pk_column_index_cache,
685            column_index_map_cache,
686            pk_indices_cache,
687            column_names_lower_cache,
688        }
689    }
690
691    /// Create a new schema with explicit timestamps
692    pub fn with_timestamps(
693        table_name: impl Into<String>,
694        columns: Vec<SchemaColumn>,
695        created_at: DateTime<Utc>,
696        updated_at: DateTime<Utc>,
697    ) -> Self {
698        Self::with_timestamps_and_foreign_keys(
699            table_name,
700            columns,
701            Vec::new(),
702            created_at,
703            updated_at,
704        )
705    }
706
707    /// Create a new schema with explicit timestamps and foreign key constraints
708    pub fn with_timestamps_and_foreign_keys(
709        table_name: impl Into<String>,
710        columns: Vec<SchemaColumn>,
711        foreign_keys: Vec<ForeignKeyConstraint>,
712        created_at: DateTime<Utc>,
713        updated_at: DateTime<Utc>,
714    ) -> Self {
715        Self::with_timestamps_and_constraints(
716            table_name,
717            columns,
718            foreign_keys,
719            Vec::new(),
720            created_at,
721            updated_at,
722        )
723    }
724
725    /// Create a schema with explicit timestamps and all table constraints.
726    pub fn with_timestamps_and_constraints(
727        table_name: impl Into<String>,
728        mut columns: Vec<SchemaColumn>,
729        foreign_keys: Vec<ForeignKeyConstraint>,
730        table_checks: Vec<String>,
731        created_at: DateTime<Utc>,
732        updated_at: DateTime<Utc>,
733    ) -> Self {
734        let name = table_name.into();
735        let name_lower = name.to_lowercase();
736        normalize_columns(&mut columns);
737
738        // Eagerly compute caches to avoid recomputation on clone
739        let column_names_cache = OnceLock::new();
740        let _ = column_names_cache.set(CompactArc::new(
741            columns.iter().map(|c| c.name.clone()).collect(),
742        ));
743
744        let pk_column_index_cache = OnceLock::new();
745        let pk_idx = columns
746            .iter()
747            .enumerate()
748            .find(|(_, col)| col.primary_key && col.data_type == DataType::Integer)
749            .map(|(i, _)| i);
750        let _ = pk_column_index_cache.set(pk_idx);
751
752        let column_index_map_cache = OnceLock::new();
753        let _ = column_index_map_cache.set(
754            columns
755                .iter()
756                .enumerate()
757                .map(|(i, c)| (c.name_lower.clone(), i))
758                .collect(),
759        );
760
761        let pk_indices_cache = OnceLock::new();
762        let _ = pk_indices_cache.set(Arc::new(
763            columns
764                .iter()
765                .enumerate()
766                .filter(|(_, c)| c.primary_key)
767                .map(|(i, _)| i)
768                .collect(),
769        ));
770
771        let column_names_lower_cache = OnceLock::new();
772        let _ = column_names_lower_cache.set(CompactArc::new(
773            columns.iter().map(|c| c.name_lower.clone()).collect(),
774        ));
775
776        Self {
777            catalog_id: [0; 16],
778            table_name: name,
779            table_name_lower: name_lower,
780            columns,
781            foreign_keys,
782            table_checks,
783            constraints: Vec::new(),
784            next_constraint_id: 1,
785            next_check_ordinal: 1,
786            created_at,
787            updated_at,
788            column_names_cache,
789            pk_column_index_cache,
790            column_index_map_cache,
791            pk_indices_cache,
792            column_names_lower_cache,
793        }
794    }
795
796    /// Get the number of columns
797    pub fn column_count(&self) -> usize {
798        self.columns.len()
799    }
800
801    /// Check if the schema has any columns
802    pub fn is_empty(&self) -> bool {
803        self.columns.is_empty()
804    }
805
806    /// Return the authoritative table name.
807    pub fn table_name(&self) -> &str {
808        &self.table_name
809    }
810
811    /// Return the durable 128-bit catalog identity. Detached schemas expose
812    /// the reserved all-zero value until an engine admits them.
813    pub fn catalog_id(&self) -> [u8; 16] {
814        self.catalog_id
815    }
816
817    #[doc(hidden)]
818    pub fn ensure_catalog_identity(&mut self) {
819        if self.catalog_id == [0; 16] {
820            self.catalog_id = *uuid::Uuid::now_v7().as_bytes();
821        }
822    }
823
824    #[doc(hidden)]
825    pub fn install_catalog_identity(&mut self, catalog_id: [u8; 16]) -> Result<()> {
826        if catalog_id == [0; 16] {
827            return Err(Error::InvalidArgument(
828                "persisted schema has the reserved zero catalog identity".to_string(),
829            ));
830        }
831        self.catalog_id = catalog_id;
832        Ok(())
833    }
834
835    #[doc(hidden)]
836    pub fn canonicalize_for_persistence(&self) -> Result<Self> {
837        let mut schema = self.clone();
838        schema.ensure_catalog_identity();
839        schema.ensure_constraint_catalog()?;
840        Ok(schema)
841    }
842
843    /// Return the authoritative column definitions.
844    pub fn columns(&self) -> &[SchemaColumn] {
845        &self.columns
846    }
847
848    /// Return the table foreign-key constraints.
849    pub fn foreign_keys(&self) -> &[ForeignKeyConstraint] {
850        &self.foreign_keys
851    }
852
853    /// Return the table-level CHECK expressions.
854    pub fn table_checks(&self) -> &[String] {
855        &self.table_checks
856    }
857
858    /// Return durable named SQL constraints in creation order.
859    pub fn constraints(&self) -> &[SchemaConstraint] {
860        &self.constraints
861    }
862
863    /// Return a named constraint using catalog case-insensitive identity.
864    pub fn find_constraint(&self, name: &str) -> Option<&SchemaConstraint> {
865        let name_lower = name.to_lowercase();
866        self.constraints
867            .iter()
868            .find(|constraint| constraint.name.to_lowercase() == name_lower)
869    }
870
871    /// Complete automatic names for a schema constructed through the public
872    /// Rust API. SQL DDL normally arrives with the same entries already
873    /// installed; engine admission and persistence call this method so a
874    /// current-format schema never publishes unnamed enforcement owners.
875    #[doc(hidden)]
876    pub fn ensure_constraint_catalog(&mut self) -> Result<()> {
877        if !self.constraints.is_empty() {
878            return self.validate_constraint_catalog_complete();
879        }
880
881        if let Some(primary_key) = self.primary_key_columns().first() {
882            self.register_primary_key_constraint(vec![primary_key.name.clone()])?;
883        }
884        for column in self.columns.clone() {
885            if let Some(expression) = column.check_expr {
886                self.register_check_constraint(Some(column.name), expression)?;
887            }
888        }
889        for foreign_key in self.foreign_keys.clone() {
890            self.register_foreign_key_constraint(&foreign_key)?;
891        }
892        for expression in self.table_checks.clone() {
893            self.register_check_constraint(None, expression)?;
894        }
895        self.validate_constraint_catalog_complete()
896    }
897
898    /// Install a constraint catalog decoded from the current persistence
899    /// format. This is intentionally unavailable as an implicit migration path:
900    /// older schema markers fail before reaching this method.
901    #[doc(hidden)]
902    pub fn install_constraint_catalog(
903        &mut self,
904        constraints: Vec<SchemaConstraint>,
905        next_constraint_id: u64,
906        next_check_ordinal: u32,
907    ) -> Result<()> {
908        let mut names = StringSet::default();
909        let mut ids = std::collections::HashSet::new();
910        let mut max_id = 0u64;
911        let mut max_check_ordinal = 0u32;
912        for constraint in &constraints {
913            if constraint.id == 0 || !ids.insert(constraint.id) {
914                return Err(Error::InvalidArgument(
915                    "constraint catalog contains a zero or duplicate stable identity".to_string(),
916                ));
917            }
918            if constraint.name.is_empty()
919                || constraint.name.len() > MAX_CONSTRAINT_NAME_BYTES
920                || !names.insert(constraint.name.to_lowercase())
921            {
922                return Err(Error::InvalidArgument(
923                    "constraint catalog contains an empty, oversized, or duplicate name"
924                        .to_string(),
925                ));
926            }
927            max_id = max_id.max(constraint.id);
928            if let SchemaConstraintKind::Check { ordinal, .. } = constraint.kind {
929                if ordinal == 0 {
930                    return Err(Error::InvalidArgument(
931                        "CHECK constraint ordinal must be non-zero".to_string(),
932                    ));
933                }
934                max_check_ordinal = max_check_ordinal.max(ordinal);
935            }
936        }
937        if next_constraint_id <= max_id || next_check_ordinal <= max_check_ordinal {
938            return Err(Error::InvalidArgument(
939                "constraint catalog next identity/ordinal would reuse an existing value"
940                    .to_string(),
941            ));
942        }
943        self.constraints = constraints;
944        self.next_constraint_id = next_constraint_id;
945        self.next_check_ordinal = next_check_ordinal;
946        self.validate_constraint_catalog_complete()?;
947        Ok(())
948    }
949
950    #[doc(hidden)]
951    pub fn register_primary_key_constraint(&mut self, columns: Vec<String>) -> Result<String> {
952        let canonical_columns = self.resolve_constraint_columns(columns)?;
953        let base = format!("pk_{}", self.table_name_lower);
954        self.register_constraint(
955            base,
956            "primary_key",
957            &canonical_columns,
958            &[],
959            SchemaConstraintKind::PrimaryKey {
960                columns: canonical_columns.clone(),
961            },
962        )
963    }
964
965    #[doc(hidden)]
966    pub fn register_unique_constraint(&mut self, columns: Vec<String>) -> Result<String> {
967        let canonical_columns = self.resolve_constraint_columns(columns)?;
968        let base = format!(
969            "uq_{}_{}",
970            self.table_name_lower,
971            canonical_columns
972                .iter()
973                .map(|column| column.to_lowercase())
974                .collect::<Vec<_>>()
975                .join("_")
976        );
977        let placeholder = SchemaConstraintKind::Unique {
978            columns: canonical_columns.clone(),
979            index_name: String::new(),
980        };
981        let name =
982            self.register_constraint(base, "unique", &canonical_columns, &[], placeholder)?;
983        let constraint = self
984            .constraints
985            .last_mut()
986            .expect("registered UNIQUE constraint exists");
987        let SchemaConstraintKind::Unique { index_name, .. } = &mut constraint.kind else {
988            unreachable!("registered UNIQUE kind changed")
989        };
990        *index_name = name.clone();
991        Ok(name)
992    }
993
994    #[doc(hidden)]
995    pub fn register_foreign_key_constraint(
996        &mut self,
997        foreign_key: &ForeignKeyConstraint,
998    ) -> Result<String> {
999        let columns = self.resolve_constraint_columns(vec![foreign_key.column_name.clone()])?;
1000        let referenced_table = foreign_key.referenced_table.to_lowercase();
1001        let referenced_columns = vec![foreign_key.referenced_column.to_lowercase()];
1002        let base = format!(
1003            "fk_{}_{}___{}",
1004            self.table_name_lower,
1005            columns[0].to_lowercase(),
1006            referenced_table
1007        );
1008        let mut extra = vec![referenced_table.clone()];
1009        extra.extend(referenced_columns.iter().cloned());
1010        self.register_constraint(
1011            base,
1012            "foreign_key",
1013            &columns,
1014            &extra,
1015            SchemaConstraintKind::ForeignKey {
1016                columns: columns.clone(),
1017                referenced_table,
1018                referenced_columns,
1019                on_delete: foreign_key.on_delete,
1020                on_update: foreign_key.on_update,
1021            },
1022        )
1023    }
1024
1025    #[doc(hidden)]
1026    pub fn register_check_constraint(
1027        &mut self,
1028        column_name: Option<String>,
1029        expression: String,
1030    ) -> Result<String> {
1031        let column_name = match column_name {
1032            Some(column_name) => Some(
1033                self.resolve_constraint_columns(vec![column_name])?
1034                    .remove(0),
1035            ),
1036            None => None,
1037        };
1038        let ordinal = self.next_check_ordinal;
1039        self.next_check_ordinal = self
1040            .next_check_ordinal
1041            .checked_add(1)
1042            .ok_or_else(|| Error::InvalidArgument("CHECK ordinal space exhausted".to_string()))?;
1043        let base = format!("chk_{}_{}", self.table_name_lower, ordinal);
1044        let result = self.register_constraint(
1045            base,
1046            "check",
1047            &[],
1048            &[ordinal.to_string()],
1049            SchemaConstraintKind::Check {
1050                column_name,
1051                expression,
1052                ordinal,
1053            },
1054        );
1055        if result.is_err() {
1056            self.next_check_ordinal = ordinal;
1057        }
1058        result
1059    }
1060
1061    /// Remove only the durable catalog entry. The DDL owner first validates
1062    /// dependencies and updates the matching enforcement owner atomically.
1063    #[doc(hidden)]
1064    pub fn take_constraint(&mut self, name: &str) -> Option<SchemaConstraint> {
1065        let index = self
1066            .constraints
1067            .iter()
1068            .position(|constraint| constraint.name.eq_ignore_ascii_case(name))?;
1069        Some(self.constraints.remove(index))
1070    }
1071
1072    fn resolve_constraint_columns(&self, columns: Vec<String>) -> Result<Vec<String>> {
1073        if columns.is_empty() {
1074            return Err(Error::InvalidArgument(
1075                "constraint must reference at least one column".to_string(),
1076            ));
1077        }
1078        columns
1079            .into_iter()
1080            .map(|column| {
1081                self.get_column_by_name(&column)
1082                    .map(|resolved| resolved.name.clone())
1083                    .ok_or(Error::ColumnNotFound(column))
1084            })
1085            .collect()
1086    }
1087
1088    fn register_constraint(
1089        &mut self,
1090        base: String,
1091        kind_name: &str,
1092        columns: &[String],
1093        extra_fields: &[String],
1094        kind: SchemaConstraintKind,
1095    ) -> Result<String> {
1096        let base_conflicts = self
1097            .constraints
1098            .iter()
1099            .any(|constraint| constraint.name.eq_ignore_ascii_case(&base));
1100        let name = generated_constraint_name(
1101            &base,
1102            kind_name,
1103            &self.table_name_lower,
1104            columns,
1105            extra_fields,
1106            base_conflicts,
1107        );
1108        if self
1109            .constraints
1110            .iter()
1111            .any(|constraint| constraint.name.eq_ignore_ascii_case(&name))
1112        {
1113            return Err(Error::InvalidArgument(format!(
1114                "generated constraint name '{}' collides with an existing constraint",
1115                name
1116            )));
1117        }
1118        let id = self.next_constraint_id;
1119        self.next_constraint_id = self.next_constraint_id.checked_add(1).ok_or_else(|| {
1120            Error::InvalidArgument("constraint identity space exhausted".to_string())
1121        })?;
1122        self.constraints.push(SchemaConstraint {
1123            id,
1124            name: name.clone(),
1125            kind,
1126        });
1127        Ok(name)
1128    }
1129
1130    /// Return the schema creation timestamp.
1131    pub fn created_at(&self) -> DateTime<Utc> {
1132        self.created_at
1133    }
1134
1135    /// Return the schema update timestamp.
1136    pub fn updated_at(&self) -> DateTime<Utc> {
1137        self.updated_at
1138    }
1139
1140    /// Install timestamps selected by the authoritative durable catalog.
1141    #[doc(hidden)]
1142    pub fn install_catalog_timestamps(
1143        &mut self,
1144        created_at: DateTime<Utc>,
1145        updated_at: DateTime<Utc>,
1146    ) -> Result<()> {
1147        if updated_at < created_at {
1148            return Err(Error::invalid_argument(
1149                "schema update timestamp precedes its creation timestamp",
1150            ));
1151        }
1152        self.created_at = created_at;
1153        self.updated_at = updated_at;
1154        Ok(())
1155    }
1156
1157    /// Find a column by name (case-insensitive)
1158    /// Returns the column index and reference
1159    /// OPTIMIZATION: Uses cached column_index_map for O(1) lookup
1160    pub fn find_column(&self, name: &str) -> Option<(usize, &SchemaColumn)> {
1161        let name_lower = name.to_lowercase();
1162        self.column_index_map()
1163            .get(&name_lower)
1164            .map(|&idx| (idx, &self.columns[idx]))
1165    }
1166
1167    /// Get a column by index
1168    pub fn get_column(&self, index: usize) -> Option<&SchemaColumn> {
1169        self.columns.get(index)
1170    }
1171
1172    /// Get a column by name (case-insensitive)
1173    pub fn get_column_by_name(&self, name: &str) -> Option<&SchemaColumn> {
1174        self.find_column(name).map(|(_, col)| col)
1175    }
1176
1177    /// Get the column index by name (case-insensitive)
1178    pub fn get_column_index(&self, name: &str) -> Option<usize> {
1179        self.find_column(name).map(|(idx, _)| idx)
1180    }
1181
1182    /// Get the data type of a column by name
1183    pub fn get_column_type(&self, name: &str) -> Option<DataType> {
1184        self.get_column_by_name(name).map(|col| col.data_type)
1185    }
1186
1187    /// Check if a column exists by name
1188    pub fn has_column(&self, name: &str) -> bool {
1189        self.find_column(name).is_some()
1190    }
1191
1192    /// Get all column names as borrowed strings (allocates Vec but not strings)
1193    pub fn column_names(&self) -> Vec<&str> {
1194        self.columns.iter().map(|c| c.name.as_str()).collect()
1195    }
1196
1197    /// Get all column names as owned strings (cached - only clones once)
1198    ///
1199    /// This is more efficient than calling `.columns.iter().map(|c| c.name.clone()).collect()`
1200    /// repeatedly, as the result is computed once and cached.
1201    #[inline]
1202    pub fn column_names_owned(&self) -> &[String] {
1203        self.column_names_cache
1204            .get_or_init(|| CompactArc::new(self.columns.iter().map(|c| c.name.clone()).collect()))
1205    }
1206
1207    /// Get column names as Arc for zero-copy sharing with results
1208    ///
1209    /// This is the most efficient way to pass column names to `ExecutorResult::with_arc_columns`
1210    /// as it avoids any string cloning after the first call.
1211    #[inline]
1212    pub fn column_names_arc(&self) -> CompactArc<Vec<String>> {
1213        CompactArc::clone(
1214            self.column_names_cache.get_or_init(|| {
1215                CompactArc::new(self.columns.iter().map(|c| c.name.clone()).collect())
1216            }),
1217        )
1218    }
1219
1220    /// Get lowercase column names as Arc for zero-copy sharing
1221    ///
1222    /// Uses the pre-computed `name_lower` from each SchemaColumn, avoiding
1223    /// per-query `to_lowercase()` calls.
1224    #[inline]
1225    pub fn column_names_lower_arc(&self) -> CompactArc<Vec<String>> {
1226        CompactArc::clone(self.column_names_lower_cache.get_or_init(|| {
1227            CompactArc::new(self.columns.iter().map(|c| c.name_lower.clone()).collect())
1228        }))
1229    }
1230
1231    /// Get a cached map of lowercase column names to their indices
1232    /// OPTIMIZATION: Cached to avoid creating this map on every query
1233    #[inline]
1234    pub fn column_index_map(&self) -> &StringMap<usize> {
1235        self.column_index_map_cache.get_or_init(|| {
1236            self.columns
1237                .iter()
1238                .enumerate()
1239                .map(|(i, c)| (c.name_lower.clone(), i))
1240                .collect()
1241        })
1242    }
1243
1244    /// Get the primary key columns
1245    pub fn primary_key_columns(&self) -> Vec<&SchemaColumn> {
1246        self.columns.iter().filter(|c| c.primary_key).collect()
1247    }
1248
1249    /// Check if the schema has a primary key
1250    pub fn has_primary_key(&self) -> bool {
1251        self.columns.iter().any(|c| c.primary_key)
1252    }
1253
1254    /// Get the primary key column indices (cached for performance)
1255    /// OPTIMIZATION: Cached to avoid allocating Vec on every call
1256    #[inline]
1257    pub fn primary_key_indices(&self) -> &[usize] {
1258        self.pk_indices_cache.get_or_init(|| {
1259            Arc::new(
1260                self.columns
1261                    .iter()
1262                    .enumerate()
1263                    .filter(|(_, c)| c.primary_key)
1264                    .map(|(i, _)| i)
1265                    .collect(),
1266            )
1267        })
1268    }
1269
1270    /// Get the single primary key column index (cached for performance)
1271    /// Returns None if there's no PK or if PK is not an integer type
1272    /// OPTIMIZATION: Cached to avoid iteration on every INSERT
1273    #[inline]
1274    pub fn pk_column_index(&self) -> Option<usize> {
1275        *self.pk_column_index_cache.get_or_init(|| {
1276            for (i, col) in self.columns.iter().enumerate() {
1277                if col.primary_key && col.data_type == DataType::Integer {
1278                    return Some(i);
1279                }
1280            }
1281            None
1282        })
1283    }
1284
1285    /// Validate column count matches expected value
1286    pub fn validate_column_count(&self, expected: usize) -> Result<()> {
1287        if self.columns.len() != expected {
1288            return Err(Error::table_columns_not_match(expected, self.columns.len()));
1289        }
1290        Ok(())
1291    }
1292
1293    /// Validate that every foreign-key ordinal still names the same local column.
1294    ///
1295    /// Foreign keys retain both an ordinal (used by row consumers) and a name
1296    /// (used by diagnostics and persistence).  Schema mutation and artifact
1297    /// decode must never publish a schema in which those two identities drift.
1298    #[doc(hidden)]
1299    pub fn validate_foreign_key_invariants(&self) -> Result<()> {
1300        for fk in &self.foreign_keys {
1301            let column = self.columns.get(fk.column_index).ok_or_else(|| {
1302                Error::InvalidArgument(format!(
1303                    "foreign key column index {} is out of bounds for table '{}' with {} columns",
1304                    fk.column_index,
1305                    self.table_name,
1306                    self.columns.len()
1307                ))
1308            })?;
1309
1310            if !column.name.eq_ignore_ascii_case(&fk.column_name) {
1311                return Err(Error::InvalidArgument(format!(
1312                    "foreign key column identity mismatch in table '{}': index {} names '{}', metadata names '{}'",
1313                    self.table_name, fk.column_index, column.name, fk.column_name
1314                )));
1315            }
1316            if fk.referenced_table.is_empty() || fk.referenced_column.is_empty() {
1317                return Err(Error::InvalidArgument(format!(
1318                    "foreign key on '{}.{}' has an empty referenced table or column",
1319                    self.table_name, column.name
1320                )));
1321            }
1322            if (matches!(fk.on_delete, ForeignKeyAction::SetNull)
1323                || matches!(fk.on_update, ForeignKeyAction::SetNull))
1324                && !column.nullable
1325            {
1326                return Err(Error::InvalidArgument(format!(
1327                    "foreign key on non-nullable column '{}.{}' uses SET NULL",
1328                    self.table_name, column.name
1329                )));
1330            }
1331        }
1332
1333        Ok(())
1334    }
1335
1336    #[doc(hidden)]
1337    pub fn validate_constraint_catalog(&self) -> Result<()> {
1338        let mut names = StringSet::default();
1339        let mut ids = std::collections::HashSet::new();
1340        for constraint in &self.constraints {
1341            if constraint.id == 0 || !ids.insert(constraint.id) {
1342                return Err(Error::InvalidArgument(format!(
1343                    "constraint '{}' has a zero or duplicate stable identity",
1344                    constraint.name
1345                )));
1346            }
1347            if constraint.name.is_empty()
1348                || constraint.name.len() > MAX_CONSTRAINT_NAME_BYTES
1349                || !names.insert(constraint.name.to_lowercase())
1350            {
1351                return Err(Error::InvalidArgument(format!(
1352                    "constraint '{}' has an invalid or duplicate public name",
1353                    constraint.name
1354                )));
1355            }
1356            for column in constraint.kind.columns() {
1357                if self.find_column(column).is_none() {
1358                    return Err(Error::InvalidArgument(format!(
1359                        "constraint '{}' references missing column '{}'",
1360                        constraint.name, column
1361                    )));
1362                }
1363            }
1364            match &constraint.kind {
1365                SchemaConstraintKind::PrimaryKey { columns } => {
1366                    if columns.len() != 1
1367                        || self
1368                            .get_column_by_name(&columns[0])
1369                            .is_none_or(|column| !column.primary_key)
1370                    {
1371                        return Err(Error::InvalidArgument(format!(
1372                            "constraint '{}' does not match the PRIMARY KEY owner",
1373                            constraint.name
1374                        )));
1375                    }
1376                }
1377                SchemaConstraintKind::Unique { index_name, .. } => {
1378                    if index_name.is_empty() {
1379                        return Err(Error::InvalidArgument(format!(
1380                            "UNIQUE constraint '{}' has no owned physical index",
1381                            constraint.name
1382                        )));
1383                    }
1384                }
1385                SchemaConstraintKind::ForeignKey {
1386                    columns,
1387                    referenced_table,
1388                    referenced_columns,
1389                    on_delete,
1390                    on_update,
1391                } => {
1392                    if columns.len() != 1
1393                        || referenced_columns.len() != 1
1394                        || !self.foreign_keys.iter().any(|foreign_key| {
1395                            foreign_key.column_name.eq_ignore_ascii_case(&columns[0])
1396                                && foreign_key
1397                                    .referenced_table
1398                                    .eq_ignore_ascii_case(referenced_table)
1399                                && foreign_key
1400                                    .referenced_column
1401                                    .eq_ignore_ascii_case(&referenced_columns[0])
1402                                && foreign_key.on_delete == *on_delete
1403                                && foreign_key.on_update == *on_update
1404                        })
1405                    {
1406                        return Err(Error::InvalidArgument(format!(
1407                            "constraint '{}' does not match a FOREIGN KEY owner",
1408                            constraint.name
1409                        )));
1410                    }
1411                }
1412                SchemaConstraintKind::Check {
1413                    column_name,
1414                    expression,
1415                    ordinal,
1416                } => {
1417                    if *ordinal == 0 {
1418                        return Err(Error::InvalidArgument(format!(
1419                            "CHECK constraint '{}' has ordinal zero",
1420                            constraint.name
1421                        )));
1422                    }
1423                    let matches_owner = if let Some(column_name) = column_name {
1424                        self.get_column_by_name(column_name)
1425                            .and_then(|column| column.check_expr.as_ref())
1426                            == Some(expression)
1427                    } else {
1428                        self.table_checks.iter().any(|check| check == expression)
1429                    };
1430                    if !matches_owner {
1431                        return Err(Error::InvalidArgument(format!(
1432                            "constraint '{}' does not match a CHECK owner",
1433                            constraint.name
1434                        )));
1435                    }
1436                }
1437            }
1438        }
1439        let max_id = self
1440            .constraints
1441            .iter()
1442            .map(|constraint| constraint.id)
1443            .max();
1444        if max_id.is_some_and(|max_id| self.next_constraint_id <= max_id) {
1445            return Err(Error::InvalidArgument(
1446                "next constraint identity would reuse an existing identity".to_string(),
1447            ));
1448        }
1449        let max_ordinal = self
1450            .constraints
1451            .iter()
1452            .filter_map(|constraint| match constraint.kind {
1453                SchemaConstraintKind::Check { ordinal, .. } => Some(ordinal),
1454                _ => None,
1455            })
1456            .max();
1457        if max_ordinal.is_some_and(|max_ordinal| self.next_check_ordinal <= max_ordinal) {
1458            return Err(Error::InvalidArgument(
1459                "next CHECK ordinal would reuse an existing ordinal".to_string(),
1460            ));
1461        }
1462        Ok(())
1463    }
1464
1465    #[doc(hidden)]
1466    pub fn validate_constraint_catalog_complete(&self) -> Result<()> {
1467        self.validate_constraint_catalog()?;
1468        let primary_key_owner_count = usize::from(self.has_primary_key());
1469        let primary_key_catalog_count = self
1470            .constraints
1471            .iter()
1472            .filter(|constraint| matches!(constraint.kind, SchemaConstraintKind::PrimaryKey { .. }))
1473            .count();
1474        if primary_key_owner_count != primary_key_catalog_count {
1475            return Err(Error::InvalidArgument(
1476                "PRIMARY KEY owner and named constraint catalog are incomplete".to_string(),
1477            ));
1478        }
1479
1480        for foreign_key in &self.foreign_keys {
1481            let count = self
1482                .constraints
1483                .iter()
1484                .filter(|constraint| match &constraint.kind {
1485                    SchemaConstraintKind::ForeignKey {
1486                        columns,
1487                        referenced_table,
1488                        referenced_columns,
1489                        on_delete,
1490                        on_update,
1491                    } => {
1492                        columns.len() == 1
1493                            && referenced_columns.len() == 1
1494                            && columns[0].eq_ignore_ascii_case(&foreign_key.column_name)
1495                            && referenced_table.eq_ignore_ascii_case(&foreign_key.referenced_table)
1496                            && referenced_columns[0]
1497                                .eq_ignore_ascii_case(&foreign_key.referenced_column)
1498                            && *on_delete == foreign_key.on_delete
1499                            && *on_update == foreign_key.on_update
1500                    }
1501                    _ => false,
1502                })
1503                .count();
1504            if count != 1 {
1505                return Err(Error::InvalidArgument(format!(
1506                    "FOREIGN KEY owner on column '{}' has {count} named catalog entries",
1507                    foreign_key.column_name
1508                )));
1509            }
1510        }
1511
1512        for column in &self.columns {
1513            if let Some(expression) = &column.check_expr {
1514                let count = self
1515                    .constraints
1516                    .iter()
1517                    .filter(|constraint| {
1518                        matches!(
1519                            &constraint.kind,
1520                            SchemaConstraintKind::Check {
1521                                column_name: Some(column_name),
1522                                expression: candidate,
1523                                ..
1524                            } if column_name.eq_ignore_ascii_case(&column.name)
1525                                && candidate == expression
1526                        )
1527                    })
1528                    .count();
1529                if count != 1 {
1530                    return Err(Error::InvalidArgument(format!(
1531                        "column CHECK owner on '{}' has {count} named catalog entries",
1532                        column.name
1533                    )));
1534                }
1535            }
1536        }
1537
1538        let mut table_check_owners = std::collections::HashMap::<&str, usize>::new();
1539        for expression in &self.table_checks {
1540            *table_check_owners.entry(expression.as_str()).or_default() += 1;
1541        }
1542        let mut table_check_catalog = std::collections::HashMap::<&str, usize>::new();
1543        for constraint in &self.constraints {
1544            if let SchemaConstraintKind::Check {
1545                column_name: None,
1546                expression,
1547                ..
1548            } = &constraint.kind
1549            {
1550                *table_check_catalog.entry(expression.as_str()).or_default() += 1;
1551            }
1552        }
1553        if table_check_owners != table_check_catalog {
1554            return Err(Error::InvalidArgument(
1555                "table CHECK owners and named constraint catalog are incomplete".to_string(),
1556            ));
1557        }
1558        Ok(())
1559    }
1560
1561    /// Validate the single authoritative schema snapshot used by all caches.
1562    #[doc(hidden)]
1563    pub fn validate_structural_invariants(&self) -> Result<()> {
1564        if self.table_name.is_empty() || self.table_name_lower != self.table_name.to_lowercase() {
1565            return Err(Error::InvalidArgument(
1566                "schema table identity is empty or not normalized".to_string(),
1567            ));
1568        }
1569        let mut seen = StringSet::default();
1570        let mut primary_keys = 0usize;
1571        for (index, column) in self.columns.iter().enumerate() {
1572            if column.id != index
1573                || column.name.is_empty()
1574                || column.name_lower != column.name.to_lowercase()
1575            {
1576                return Err(Error::InvalidArgument(format!(
1577                    "column {} has a stale ordinal or normalized identity",
1578                    column.name
1579                )));
1580            }
1581            if !seen.insert(column.name_lower.clone()) {
1582                return Err(Error::DuplicateColumn);
1583            }
1584            if column.primary_key && column.nullable {
1585                return Err(Error::InvalidArgument(format!(
1586                    "PRIMARY KEY column '{}' cannot be nullable",
1587                    column.name
1588                )));
1589            }
1590            if column.auto_increment
1591                && !matches!(column.data_type, DataType::Integer | DataType::Uuid)
1592            {
1593                return Err(Error::InvalidArgument(format!(
1594                    "AUTO_INCREMENT column '{}' must be INTEGER or UUID",
1595                    column.name
1596                )));
1597            }
1598            if column.external_type.is_some() {
1599                if column.data_type != DataType::Null
1600                    || column
1601                        .external_type_name
1602                        .as_deref()
1603                        .is_none_or(str::is_empty)
1604                    || column.vector_dimensions != 0
1605                    || column.decimal_precision != 0
1606                    || column.decimal_scale != 0
1607                {
1608                    return Err(Error::InvalidArgument(format!(
1609                        "external column '{}' carries inconsistent built-in metadata",
1610                        column.name
1611                    )));
1612                }
1613            } else if column.data_type == DataType::Null {
1614                return Err(Error::InvalidArgument(format!(
1615                    "stored column '{}' cannot use NULL as a built-in type",
1616                    column.name
1617                )));
1618            }
1619            if column.data_type != DataType::Vector && column.vector_dimensions != 0 {
1620                return Err(Error::InvalidArgument(format!(
1621                    "non-VECTOR column '{}' carries VECTOR dimensions",
1622                    column.name
1623                )));
1624            }
1625            if column.data_type == DataType::Decimal {
1626                if column.decimal_precision > 38
1627                    || (column.decimal_precision == 0 && column.decimal_scale != 0)
1628                    || column.decimal_scale > column.decimal_precision
1629                {
1630                    return Err(Error::InvalidArgument(format!(
1631                        "column '{}' has invalid DECIMAL({},{}) parameters",
1632                        column.name, column.decimal_precision, column.decimal_scale
1633                    )));
1634                }
1635            } else if column.decimal_precision != 0 || column.decimal_scale != 0 {
1636                return Err(Error::InvalidArgument(format!(
1637                    "non-DECIMAL column '{}' carries DECIMAL parameters",
1638                    column.name
1639                )));
1640            }
1641            if let Some(default_value) = &column.default_value {
1642                column.validate_declared_value(default_value)?;
1643            }
1644            primary_keys += usize::from(column.primary_key);
1645        }
1646        if primary_keys > 1 {
1647            return Err(Error::NotSupported(
1648                "schemas support exactly one PRIMARY KEY column".to_string(),
1649            ));
1650        }
1651        self.validate_foreign_key_invariants()?;
1652        self.validate_constraint_catalog()
1653    }
1654
1655    /// Mark the schema as updated (sets updated_at to now)
1656    pub fn mark_updated(&mut self) {
1657        self.updated_at = Utc::now();
1658    }
1659
1660    /// Complete an in-place schema mutation without reconstructing the schema.
1661    /// This preserves stable constraint IDs and public names.
1662    #[doc(hidden)]
1663    pub fn finish_catalog_mutation(&mut self) -> Result<()> {
1664        self.mark_updated();
1665        self.rebuild_caches();
1666        self.validate_structural_invariants()
1667    }
1668
1669    /// Rename the table while keeping its normalized identity synchronized.
1670    pub fn rename_table(&mut self, name: impl Into<String>) {
1671        let name = name.into();
1672        self.table_name_lower = name.to_lowercase();
1673        self.table_name = name;
1674    }
1675
1676    /// Rebuild all caches after schema mutation
1677    /// This replaces the OnceLock fields with fresh ones containing updated values
1678    fn rebuild_caches(&mut self) {
1679        // Rebuild column names cache
1680        self.column_names_cache = OnceLock::new();
1681        let _ = self.column_names_cache.set(CompactArc::new(
1682            self.columns.iter().map(|c| c.name.clone()).collect(),
1683        ));
1684
1685        // Rebuild PK cache
1686        self.pk_column_index_cache = OnceLock::new();
1687        let pk_idx = self
1688            .columns
1689            .iter()
1690            .enumerate()
1691            .find(|(_, col)| col.primary_key && col.data_type == DataType::Integer)
1692            .map(|(i, _)| i);
1693        let _ = self.pk_column_index_cache.set(pk_idx);
1694
1695        // Rebuild column index map cache
1696        self.column_index_map_cache = OnceLock::new();
1697        let _ = self.column_index_map_cache.set(
1698            self.columns
1699                .iter()
1700                .enumerate()
1701                .map(|(i, c)| (c.name_lower.clone(), i))
1702                .collect(),
1703        );
1704
1705        // Rebuild primary key indices cache
1706        self.pk_indices_cache = OnceLock::new();
1707        let _ = self.pk_indices_cache.set(Arc::new(
1708            self.columns
1709                .iter()
1710                .enumerate()
1711                .filter(|(_, c)| c.primary_key)
1712                .map(|(i, _)| i)
1713                .collect(),
1714        ));
1715
1716        // Rebuild lowercase column names cache
1717        self.column_names_lower_cache = OnceLock::new();
1718        let _ = self.column_names_lower_cache.set(CompactArc::new(
1719            self.columns.iter().map(|c| c.name_lower.clone()).collect(),
1720        ));
1721    }
1722
1723    /// Add a column to the schema
1724    pub fn add_column(&mut self, column: SchemaColumn) -> Result<()> {
1725        // Check for duplicate column name
1726        if self.has_column(&column.name) {
1727            return Err(Error::DuplicateColumn);
1728        }
1729        self.columns.push(column);
1730        self.mark_updated();
1731        self.rebuild_caches();
1732        Ok(())
1733    }
1734
1735    /// Remove a column by name
1736    pub fn remove_column(&mut self, name: &str) -> Result<SchemaColumn> {
1737        let idx = self
1738            .get_column_index(name)
1739            .ok_or_else(|| Error::ColumnNotFound(name.to_string()))?;
1740
1741        if let Some(constraint) = self.constraints.iter().find(|constraint| {
1742            constraint
1743                .kind
1744                .columns()
1745                .iter()
1746                .any(|column| column.eq_ignore_ascii_case(name))
1747        }) {
1748            return Err(Error::InvalidArgument(format!(
1749                "cannot drop column '{}' because constraint '{}' depends on it",
1750                self.columns[idx].name, constraint.name
1751            )));
1752        }
1753
1754        if self.foreign_keys.iter().any(|fk| fk.column_index == idx) {
1755            return Err(Error::InvalidArgument(format!(
1756                "cannot drop column '{}' because it owns a foreign key constraint",
1757                self.columns[idx].name
1758            )));
1759        }
1760
1761        let column = self.columns.remove(idx);
1762
1763        // Re-index remaining columns
1764        for (i, col) in self.columns.iter_mut().enumerate() {
1765            col.id = i;
1766        }
1767
1768        // Keep every surviving FK ordinal aligned with its local column.
1769        for fk in &mut self.foreign_keys {
1770            if fk.column_index > idx {
1771                fk.column_index -= 1;
1772            }
1773            fk.column_name = self.columns[fk.column_index].name.clone();
1774        }
1775
1776        self.mark_updated();
1777        self.rebuild_caches();
1778        self.validate_foreign_key_invariants()?;
1779        Ok(column)
1780    }
1781
1782    /// Rename a column
1783    pub fn rename_column(&mut self, old_name: &str, new_name: impl Into<String>) -> Result<()> {
1784        let new_name = new_name.into();
1785
1786        // Check new name doesn't exist
1787        if self.has_column(&new_name) {
1788            return Err(Error::DuplicateColumn);
1789        }
1790
1791        let idx = self
1792            .get_column_index(old_name)
1793            .ok_or_else(|| Error::ColumnNotFound(old_name.to_string()))?;
1794
1795        self.columns[idx].name_lower = new_name.to_lowercase();
1796        self.columns[idx].name = new_name;
1797        for fk in &mut self.foreign_keys {
1798            if fk.column_index == idx {
1799                fk.column_name = self.columns[idx].name.clone();
1800            }
1801        }
1802        for constraint in &mut self.constraints {
1803            match &mut constraint.kind {
1804                SchemaConstraintKind::PrimaryKey { columns }
1805                | SchemaConstraintKind::Unique { columns, .. }
1806                | SchemaConstraintKind::ForeignKey { columns, .. } => {
1807                    for column in columns {
1808                        if column.eq_ignore_ascii_case(old_name) {
1809                            *column = self.columns[idx].name.clone();
1810                        }
1811                    }
1812                }
1813                SchemaConstraintKind::Check {
1814                    column_name: Some(column_name),
1815                    ..
1816                } if column_name.eq_ignore_ascii_case(old_name) => {
1817                    *column_name = self.columns[idx].name.clone();
1818                }
1819                SchemaConstraintKind::Check { .. } => {}
1820            }
1821        }
1822        self.mark_updated();
1823        self.rebuild_caches();
1824        self.validate_foreign_key_invariants()?;
1825        Ok(())
1826    }
1827
1828    /// Modify a column's properties (except name)
1829    pub fn modify_column(
1830        &mut self,
1831        name: &str,
1832        data_type: Option<DataType>,
1833        nullable: Option<bool>,
1834    ) -> Result<()> {
1835        let idx = self
1836            .get_column_index(name)
1837            .ok_or_else(|| Error::ColumnNotFound(name.to_string()))?;
1838
1839        if let Some(dt) = data_type {
1840            self.columns[idx].data_type = dt;
1841        }
1842        if let Some(n) = nullable {
1843            self.columns[idx].nullable = n;
1844        }
1845
1846        self.mark_updated();
1847        self.rebuild_caches();
1848        Ok(())
1849    }
1850
1851    /// Set or replace a column default expression and its pre-computed value.
1852    pub fn set_column_default(
1853        &mut self,
1854        name: &str,
1855        default_expr: Option<String>,
1856        default_value: Option<Value>,
1857    ) -> Result<()> {
1858        let idx = self
1859            .get_column_index(name)
1860            .ok_or_else(|| Error::ColumnNotFound(name.to_string()))?;
1861
1862        self.columns[idx].default_expr = default_expr;
1863        self.columns[idx].default_value = default_value;
1864
1865        self.mark_updated();
1866        self.rebuild_caches();
1867        Ok(())
1868    }
1869
1870    /// Set or replace a column CHECK expression.
1871    pub fn set_column_check(&mut self, name: &str, check_expr: Option<String>) -> Result<()> {
1872        let idx = self
1873            .get_column_index(name)
1874            .ok_or_else(|| Error::ColumnNotFound(name.to_string()))?;
1875
1876        self.columns[idx].check_expr = check_expr;
1877        self.mark_updated();
1878        self.rebuild_caches();
1879        Ok(())
1880    }
1881}
1882
1883impl Default for Schema {
1884    fn default() -> Self {
1885        Self::new("", Vec::new())
1886    }
1887}
1888
1889fn normalize_columns(columns: &mut [SchemaColumn]) {
1890    for (index, column) in columns.iter_mut().enumerate() {
1891        column.id = index;
1892        column.name_lower = column.name.to_lowercase();
1893    }
1894}
1895
1896/// Produce the one canonical automatic constraint name used by both the
1897/// runtime schema and the durable catalog binder.
1898///
1899/// Keeping this algorithm in the core contract prevents the two owners from
1900/// assigning different public names when a readable base collides or exceeds
1901/// the identifier ceiling.
1902#[doc(hidden)]
1903pub fn generated_constraint_name(
1904    base: &str,
1905    kind: &str,
1906    table: &str,
1907    columns: &[String],
1908    extra_fields: &[String],
1909    base_conflicts: bool,
1910) -> String {
1911    if base.len() <= MAX_CONSTRAINT_NAME_BYTES && !base_conflicts {
1912        return base.to_owned();
1913    }
1914
1915    fn append_field(output: &mut Vec<u8>, value: &str) {
1916        let bytes = value.as_bytes();
1917        let length = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
1918        output.extend_from_slice(&length.to_le_bytes());
1919        output.extend_from_slice(bytes);
1920    }
1921
1922    let mut descriptor = CONSTRAINT_NAME_DESCRIPTOR_PREFIX.as_bytes().to_vec();
1923    append_field(&mut descriptor, kind);
1924    append_field(&mut descriptor, &table.to_lowercase());
1925    for column in columns {
1926        append_field(&mut descriptor, &column.to_lowercase());
1927    }
1928    for field in extra_fields {
1929        append_field(&mut descriptor, &field.to_lowercase());
1930    }
1931    let digest = Sha256::digest(&descriptor);
1932    let suffix = format!(
1933        "__{:02x}{:02x}{:02x}{:02x}",
1934        digest[0], digest[1], digest[2], digest[3]
1935    );
1936    let max_base = MAX_CONSTRAINT_NAME_BYTES - CONSTRAINT_HASH_SUFFIX_BYTES;
1937    let mut end = base.len().min(max_base);
1938    while !base.is_char_boundary(end) {
1939        end -= 1;
1940    }
1941    format!("{}{}", &base[..end], suffix)
1942}
1943
1944impl fmt::Display for Schema {
1945    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1946        write!(f, "CREATE TABLE {} (", self.table_name)?;
1947        for (i, col) in self.columns.iter().enumerate() {
1948            if i > 0 {
1949                write!(f, ", ")?;
1950            }
1951            write!(f, "{}", col)?;
1952        }
1953        for (i, check) in self.table_checks.iter().enumerate() {
1954            if !self.columns.is_empty() || i > 0 {
1955                write!(f, ", ")?;
1956            }
1957            write!(f, "CHECK ({})", check)?;
1958        }
1959        write!(f, ")")
1960    }
1961}
1962
1963/// Builder for creating schemas more ergonomically
1964pub struct SchemaBuilder {
1965    table_name: String,
1966    columns: Vec<SchemaColumn>,
1967    foreign_keys: Vec<ForeignKeyConstraint>,
1968    table_checks: Vec<String>,
1969}
1970
1971impl SchemaBuilder {
1972    /// Create a new schema builder
1973    pub fn new(table_name: impl Into<String>) -> Self {
1974        Self {
1975            table_name: table_name.into(),
1976            columns: Vec::new(),
1977            foreign_keys: Vec::new(),
1978            table_checks: Vec::new(),
1979        }
1980    }
1981
1982    /// Continue binding constraints against an existing schema.
1983    pub fn from_schema(schema: &Schema) -> Self {
1984        Self {
1985            table_name: schema.table_name.clone(),
1986            columns: schema.columns.clone(),
1987            foreign_keys: schema.foreign_keys.clone(),
1988            table_checks: schema.table_checks.clone(),
1989        }
1990    }
1991
1992    /// Add a column
1993    pub fn column(
1994        mut self,
1995        name: impl Into<String>,
1996        data_type: DataType,
1997        nullable: bool,
1998        primary_key: bool,
1999    ) -> Self {
2000        let id = self.columns.len();
2001        self.columns.push(SchemaColumn::new(
2002            id,
2003            name,
2004            data_type,
2005            nullable,
2006            primary_key,
2007        ));
2008        self
2009    }
2010
2011    /// Add a simple non-nullable column
2012    pub fn add(self, name: impl Into<String>, data_type: DataType) -> Self {
2013        self.column(name, data_type, false, false)
2014    }
2015
2016    /// Add a nullable column
2017    pub fn add_nullable(self, name: impl Into<String>, data_type: DataType) -> Self {
2018        self.column(name, data_type, true, false)
2019    }
2020
2021    /// Add a primary key column
2022    pub fn add_primary_key(self, name: impl Into<String>, data_type: DataType) -> Self {
2023        self.column(name, data_type, false, true)
2024    }
2025
2026    /// Add a column with full constraints (default, check)
2027    #[allow(clippy::too_many_arguments)]
2028    pub fn add_with_constraints(
2029        mut self,
2030        name: impl Into<String>,
2031        data_type: DataType,
2032        nullable: bool,
2033        primary_key: bool,
2034        auto_increment: bool,
2035        default_expr: Option<String>,
2036        check_expr: Option<String>,
2037    ) -> Self {
2038        let id = self.columns.len();
2039        self.columns.push(SchemaColumn::with_constraints(
2040            id,
2041            name,
2042            data_type,
2043            nullable,
2044            primary_key,
2045            auto_increment,
2046            default_expr,
2047            check_expr,
2048        ));
2049        self
2050    }
2051
2052    /// Set vector dimensions on the last added column
2053    pub fn set_last_vector_dimensions(mut self, dims: u16) -> Self {
2054        if let Some(col) = self.columns.last_mut() {
2055            col.vector_dimensions = dims;
2056        }
2057        self
2058    }
2059
2060    /// Set DECIMAL precision/scale on the last added column.
2061    pub fn set_last_decimal_parameters(mut self, precision: u8, scale: u8) -> Self {
2062        if let Some(col) = self.columns.last_mut() {
2063            col.decimal_precision = precision;
2064            col.decimal_scale = scale;
2065        }
2066        self
2067    }
2068
2069    pub fn set_last_external_type(
2070        mut self,
2071        type_ref: ExternalTypeRef,
2072        sql_name: impl Into<String>,
2073    ) -> Self {
2074        if let Some(column) = self.columns.last_mut() {
2075            column.data_type = DataType::Null;
2076            column.external_type = Some(type_ref);
2077            column.external_type_name = Some(sql_name.into());
2078        }
2079        self
2080    }
2081
2082    /// Set pre-computed default value on the last added column.
2083    pub fn set_last_default_value(mut self, default_value: Option<Value>) -> Self {
2084        if let Some(col) = self.columns.last_mut() {
2085            col.default_value = default_value;
2086        }
2087        self
2088    }
2089
2090    /// Find column index by name (case-insensitive)
2091    pub fn column_index(&self, name: &str) -> Option<usize> {
2092        let lower = name.to_lowercase();
2093        self.columns
2094            .iter()
2095            .position(|c| c.name.to_lowercase() == lower)
2096    }
2097
2098    /// Check if a column is nullable by index
2099    pub fn is_column_nullable(&self, idx: usize) -> bool {
2100        self.columns.get(idx).is_some_and(|c| c.nullable)
2101    }
2102
2103    /// Return the declared type of a column already admitted by the builder.
2104    pub fn column_data_type(&self, idx: usize) -> Option<DataType> {
2105        self.columns.get(idx).map(|column| column.data_type)
2106    }
2107
2108    pub fn column_logical_type(&self, idx: usize) -> Option<LogicalTypeRef> {
2109        self.columns.get(idx).map(SchemaColumn::logical_type)
2110    }
2111
2112    /// Inspect a column already admitted by the builder.
2113    pub fn column_definition(&self, idx: usize) -> Option<&SchemaColumn> {
2114        self.columns.get(idx)
2115    }
2116
2117    /// Return the single declared primary-key column, if present.
2118    pub fn primary_key_column(&self) -> Option<(usize, &SchemaColumn)> {
2119        let mut primary_keys = self
2120            .columns
2121            .iter()
2122            .enumerate()
2123            .filter(|(_, column)| column.primary_key);
2124        let primary_key = primary_keys.next()?;
2125        if primary_keys.next().is_some() {
2126            None
2127        } else {
2128            Some(primary_key)
2129        }
2130    }
2131
2132    /// Add a foreign key constraint
2133    pub fn add_foreign_key(mut self, fk: ForeignKeyConstraint) -> Self {
2134        self.foreign_keys.push(fk);
2135        self
2136    }
2137
2138    /// Add a CHECK constraint evaluated against the complete row.
2139    pub fn add_table_check(mut self, expression: impl Into<String>) -> Self {
2140        self.table_checks.push(expression.into());
2141        self
2142    }
2143
2144    /// Build the schema
2145    pub fn build(self) -> Schema {
2146        Schema::with_constraints(
2147            self.table_name,
2148            self.columns,
2149            self.foreign_keys,
2150            self.table_checks,
2151        )
2152    }
2153}
2154
2155#[cfg(test)]
2156mod tests {
2157    use super::*;
2158
2159    fn create_test_schema() -> Schema {
2160        SchemaBuilder::new("users")
2161            .add_primary_key("id", DataType::Integer)
2162            .add("name", DataType::Text)
2163            .add_nullable("email", DataType::Text)
2164            .add("active", DataType::Boolean)
2165            .build()
2166    }
2167
2168    #[test]
2169    fn test_schema_column_creation() {
2170        let col = SchemaColumn::new(0, "id", DataType::Integer, false, true);
2171        assert_eq!(col.id, 0);
2172        assert_eq!(col.name, "id");
2173        assert_eq!(col.data_type, DataType::Integer);
2174        assert!(!col.nullable);
2175        assert!(col.primary_key);
2176    }
2177
2178    #[test]
2179    fn test_schema_column_helpers() {
2180        let simple = SchemaColumn::simple(0, "name", DataType::Text);
2181        assert!(!simple.nullable);
2182        assert!(!simple.primary_key);
2183
2184        let nullable = SchemaColumn::nullable(1, "email", DataType::Text);
2185        assert!(nullable.nullable);
2186        assert!(!nullable.primary_key);
2187
2188        let pk = SchemaColumn::primary_key(2, "id", DataType::Integer);
2189        assert!(!pk.nullable);
2190        assert!(pk.primary_key);
2191    }
2192
2193    #[test]
2194    fn test_schema_creation() {
2195        let schema = create_test_schema();
2196        assert_eq!(schema.table_name, "users");
2197        assert_eq!(schema.column_count(), 4);
2198        assert!(!schema.is_empty());
2199    }
2200
2201    #[test]
2202    fn test_schema_find_column() {
2203        let schema = create_test_schema();
2204
2205        // Find by exact name
2206        let (idx, col) = schema.find_column("name").unwrap();
2207        assert_eq!(idx, 1);
2208        assert_eq!(col.name, "name");
2209
2210        // Case-insensitive
2211        let (idx, _) = schema.find_column("NAME").unwrap();
2212        assert_eq!(idx, 1);
2213
2214        // Not found
2215        assert!(schema.find_column("nonexistent").is_none());
2216    }
2217
2218    #[test]
2219    fn test_schema_get_column() {
2220        let schema = create_test_schema();
2221
2222        let col = schema.get_column(0).unwrap();
2223        assert_eq!(col.name, "id");
2224
2225        let col = schema.get_column_by_name("email").unwrap();
2226        assert_eq!(col.data_type, DataType::Text);
2227        assert!(col.nullable);
2228
2229        assert!(schema.get_column(100).is_none());
2230    }
2231
2232    #[test]
2233    fn test_schema_column_names() {
2234        let schema = create_test_schema();
2235        let names = schema.column_names();
2236        assert_eq!(names, vec!["id", "name", "email", "active"]);
2237    }
2238
2239    #[test]
2240    fn test_schema_primary_key() {
2241        let schema = create_test_schema();
2242
2243        assert!(schema.has_primary_key());
2244
2245        let pk_cols = schema.primary_key_columns();
2246        assert_eq!(pk_cols.len(), 1);
2247        assert_eq!(pk_cols[0].name, "id");
2248
2249        let pk_indices = schema.primary_key_indices();
2250        assert_eq!(pk_indices, vec![0]);
2251    }
2252
2253    #[test]
2254    fn orm_01_constraint_names_ids_collisions_and_ordinals_are_stable() {
2255        let mut schema = SchemaBuilder::new("constraint_collision")
2256            .add_primary_key("id", DataType::Integer)
2257            .add("a_b", DataType::Text)
2258            .add("c", DataType::Text)
2259            .add("a", DataType::Text)
2260            .add("b_c", DataType::Text)
2261            .build();
2262
2263        assert_eq!(
2264            schema
2265                .register_primary_key_constraint(vec!["id".to_string()])
2266                .unwrap(),
2267            "pk_constraint_collision"
2268        );
2269        let first = schema
2270            .register_unique_constraint(vec!["a_b".to_string(), "c".to_string()])
2271            .unwrap();
2272        let second = schema
2273            .register_unique_constraint(vec!["a".to_string(), "b_c".to_string()])
2274            .unwrap();
2275        assert_eq!(first, "uq_constraint_collision_a_b_c");
2276        assert!(second.starts_with("uq_constraint_collision_a_b_c__"));
2277        assert_eq!(second.len(), first.len() + 10);
2278
2279        let first_check = schema
2280            .register_check_constraint(None, "id >= 0".to_string())
2281            .unwrap();
2282        schema.table_checks.push("id >= 0".to_string());
2283        // Registration precedes enforcement only in this narrow unit setup;
2284        // install the owner before validating/removing the entry.
2285        assert_eq!(first_check, "chk_constraint_collision_1");
2286        let first_check_id = schema.find_constraint(&first_check).unwrap().id;
2287        schema.take_constraint(&first_check);
2288        schema.table_checks.clear();
2289        schema.table_checks.push("id <= 100".to_string());
2290        let second_check = schema
2291            .register_check_constraint(None, "id <= 100".to_string())
2292            .unwrap();
2293        assert_eq!(second_check, "chk_constraint_collision_2");
2294        assert!(schema.find_constraint(&second_check).unwrap().id > first_check_id);
2295
2296        let stable = schema
2297            .constraints()
2298            .iter()
2299            .map(|constraint| (constraint.id, constraint.name.clone()))
2300            .collect::<Vec<_>>();
2301        schema.rename_column("a_b", "renamed").unwrap();
2302        schema.rename_table("renamed_table");
2303        assert_eq!(
2304            schema
2305                .constraints()
2306                .iter()
2307                .map(|constraint| (constraint.id, constraint.name.clone()))
2308                .collect::<Vec<_>>(),
2309            stable
2310        );
2311        schema.validate_constraint_catalog().unwrap();
2312    }
2313
2314    #[test]
2315    fn orm_01_long_constraint_name_is_utf8_bounded_and_deterministic() {
2316        let table = "таблица_".repeat(20);
2317        let column = "колонка_".repeat(20);
2318        let mut first = SchemaBuilder::new(&table)
2319            .add(&column, DataType::Text)
2320            .build();
2321        let mut second = first.clone();
2322        let first_name = first
2323            .register_unique_constraint(vec![column.clone()])
2324            .unwrap();
2325        let second_name = second.register_unique_constraint(vec![column]).unwrap();
2326        assert_eq!(first_name, second_name);
2327        assert!(first_name.len() <= MAX_CONSTRAINT_NAME_BYTES);
2328        assert!(first_name.is_char_boundary(first_name.len()));
2329        assert_eq!(first_name.rsplit_once("__").unwrap().1.len(), 8);
2330    }
2331
2332    #[test]
2333    fn test_schema_validate_column_count() {
2334        let schema = create_test_schema();
2335
2336        assert!(schema.validate_column_count(4).is_ok());
2337
2338        let err = schema.validate_column_count(3).unwrap_err();
2339        assert!(matches!(
2340            err,
2341            Error::TableColumnsNotMatch {
2342                expected: 3,
2343                got: 4
2344            }
2345        ));
2346    }
2347
2348    #[test]
2349    fn test_schema_add_column() {
2350        let mut schema = create_test_schema();
2351        let original_count = schema.column_count();
2352
2353        schema
2354            .add_column(SchemaColumn::simple(
2355                original_count,
2356                "age",
2357                DataType::Integer,
2358            ))
2359            .unwrap();
2360
2361        assert_eq!(schema.column_count(), original_count + 1);
2362        assert!(schema.has_column("age"));
2363
2364        // Duplicate column should fail
2365        let err = schema
2366            .add_column(SchemaColumn::simple(0, "age", DataType::Integer))
2367            .unwrap_err();
2368        assert!(matches!(err, Error::DuplicateColumn));
2369    }
2370
2371    #[test]
2372    fn test_schema_remove_column() {
2373        let mut schema = create_test_schema();
2374
2375        let removed = schema.remove_column("email").unwrap();
2376        assert_eq!(removed.name, "email");
2377        assert_eq!(schema.column_count(), 3);
2378        assert!(!schema.has_column("email"));
2379
2380        // Column IDs should be re-indexed
2381        assert_eq!(schema.columns[2].id, 2);
2382
2383        // Removing non-existent column should fail
2384        assert!(schema.remove_column("nonexistent").is_err());
2385    }
2386
2387    #[test]
2388    fn test_schema_rename_column() {
2389        let mut schema = create_test_schema();
2390
2391        schema.rename_column("name", "full_name").unwrap();
2392        assert!(schema.has_column("full_name"));
2393        assert!(!schema.has_column("name"));
2394
2395        // Renaming to existing name should fail
2396        let err = schema.rename_column("full_name", "id").unwrap_err();
2397        assert!(matches!(err, Error::DuplicateColumn));
2398
2399        // Renaming non-existent column should fail
2400        assert!(schema.rename_column("nonexistent", "new_name").is_err());
2401    }
2402
2403    fn schema_with_foreign_key() -> Schema {
2404        SchemaBuilder::new("children")
2405            .add_primary_key("id", DataType::Integer)
2406            .add("prefix", DataType::Text)
2407            .add("parent_id", DataType::Integer)
2408            .add_foreign_key(ForeignKeyConstraint {
2409                column_index: 2,
2410                column_name: "parent_id".to_string(),
2411                referenced_table: "parents".to_string(),
2412                referenced_column: "id".to_string(),
2413                on_delete: ForeignKeyAction::Restrict,
2414                on_update: ForeignKeyAction::Restrict,
2415            })
2416            .build()
2417    }
2418
2419    #[test]
2420    fn test_schema_mutation_preserves_foreign_key_identity() {
2421        let mut schema = schema_with_foreign_key();
2422
2423        schema.remove_column("prefix").unwrap();
2424        assert_eq!(schema.foreign_keys[0].column_index, 1);
2425        assert_eq!(schema.foreign_keys[0].column_name, "parent_id");
2426        schema.validate_foreign_key_invariants().unwrap();
2427
2428        schema.rename_column("parent_id", "owner_id").unwrap();
2429        assert_eq!(schema.foreign_keys[0].column_index, 1);
2430        assert_eq!(schema.foreign_keys[0].column_name, "owner_id");
2431        schema.validate_foreign_key_invariants().unwrap();
2432    }
2433
2434    #[test]
2435    fn test_schema_rejects_dropping_or_decoding_invalid_foreign_key_identity() {
2436        let mut schema = schema_with_foreign_key();
2437        let error = schema.remove_column("parent_id").unwrap_err();
2438        assert!(error.to_string().contains("owns a foreign key constraint"));
2439
2440        schema.foreign_keys[0].column_index = 99;
2441        let error = schema.validate_foreign_key_invariants().unwrap_err();
2442        assert!(error.to_string().contains("out of bounds"));
2443
2444        schema.foreign_keys[0].column_index = 2;
2445        schema.foreign_keys[0].column_name = "wrong_column".to_string();
2446        let error = schema.validate_foreign_key_invariants().unwrap_err();
2447        assert!(error.to_string().contains("identity mismatch"));
2448    }
2449
2450    #[test]
2451    fn test_schema_modify_column() {
2452        let mut schema = create_test_schema();
2453
2454        schema
2455            .modify_column("name", Some(DataType::Json), Some(true))
2456            .unwrap();
2457
2458        let col = schema.get_column_by_name("name").unwrap();
2459        assert_eq!(col.data_type, DataType::Json);
2460        assert!(col.nullable);
2461
2462        // Modifying non-existent column should fail
2463        assert!(schema
2464            .modify_column("nonexistent", None, Some(true))
2465            .is_err());
2466    }
2467
2468    #[test]
2469    fn test_schema_column_display() {
2470        let col = SchemaColumn::new(0, "id", DataType::Integer, false, true);
2471        assert_eq!(col.to_string(), "id INTEGER PRIMARY KEY");
2472
2473        let col = SchemaColumn::new(1, "name", DataType::Text, false, false);
2474        assert_eq!(col.to_string(), "name TEXT NOT NULL");
2475
2476        let col = SchemaColumn::new(2, "email", DataType::Text, true, false);
2477        assert_eq!(col.to_string(), "email TEXT");
2478    }
2479
2480    #[test]
2481    fn test_schema_display() {
2482        let schema = SchemaBuilder::new("users")
2483            .add_primary_key("id", DataType::Integer)
2484            .add("name", DataType::Text)
2485            .build();
2486
2487        let expected = "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)";
2488        assert_eq!(schema.to_string(), expected);
2489    }
2490
2491    #[test]
2492    fn test_schema_builder() {
2493        let schema = SchemaBuilder::new("products")
2494            .add_primary_key("id", DataType::Integer)
2495            .add("name", DataType::Text)
2496            .add_nullable("description", DataType::Text)
2497            .add("price", DataType::Float)
2498            .build();
2499
2500        assert_eq!(schema.table_name, "products");
2501        assert_eq!(schema.column_count(), 4);
2502        assert!(schema.get_column_by_name("id").unwrap().primary_key);
2503        assert!(schema.get_column_by_name("description").unwrap().nullable);
2504    }
2505
2506    #[test]
2507    fn test_schema_timestamps() {
2508        let schema1 = Schema::new("test", vec![]);
2509        std::thread::sleep(std::time::Duration::from_millis(10));
2510        let schema2 = Schema::new("test", vec![]);
2511
2512        // Different creation times
2513        assert!(schema2.created_at >= schema1.created_at);
2514    }
2515
2516    #[test]
2517    fn test_schema_get_column_type() {
2518        let schema = create_test_schema();
2519
2520        assert_eq!(schema.get_column_type("id"), Some(DataType::Integer));
2521        assert_eq!(schema.get_column_type("name"), Some(DataType::Text));
2522        assert_eq!(schema.get_column_type("active"), Some(DataType::Boolean));
2523        assert_eq!(schema.get_column_type("nonexistent"), None);
2524    }
2525
2526    #[test]
2527    fn schema_constructor_rebuilds_authoritative_column_identity() {
2528        let mut column = SchemaColumn::new(99, "old", DataType::Integer, false, true);
2529        column.name = "Renamed".to_string();
2530        let mut schema = Schema::new("MixedCase", vec![column]);
2531
2532        assert_eq!(schema.table_name(), "MixedCase");
2533        assert_eq!(schema.columns()[0].id, 0);
2534        assert_eq!(schema.get_column_index("renamed"), Some(0));
2535        assert_eq!(schema.column_names_owned(), &["Renamed".to_string()]);
2536        assert!(schema.validate_structural_invariants().is_ok());
2537
2538        schema.rename_table("OtherName");
2539        assert_eq!(schema.table_name(), "OtherName");
2540        assert_eq!(schema.table_name_lower, "othername");
2541    }
2542
2543    #[test]
2544    fn structural_validation_rejects_impossible_key_and_vector_metadata() {
2545        let mut nullable_pk = SchemaBuilder::new("nullable_pk")
2546            .add_primary_key("id", DataType::Integer)
2547            .build();
2548        nullable_pk.columns[0].nullable = true;
2549        assert!(nullable_pk
2550            .validate_structural_invariants()
2551            .unwrap_err()
2552            .to_string()
2553            .contains("cannot be nullable"));
2554
2555        let mut wrong_auto = SchemaBuilder::new("wrong_auto")
2556            .add("id", DataType::Text)
2557            .build();
2558        wrong_auto.columns[0].auto_increment = true;
2559        assert!(wrong_auto
2560            .validate_structural_invariants()
2561            .unwrap_err()
2562            .to_string()
2563            .contains("AUTO_INCREMENT"));
2564
2565        let mut stale_vector = SchemaBuilder::new("stale_vector")
2566            .add("value", DataType::Text)
2567            .build();
2568        stale_vector.columns[0].vector_dimensions = 3;
2569        assert!(stale_vector
2570            .validate_structural_invariants()
2571            .unwrap_err()
2572            .to_string()
2573            .contains("VECTOR dimensions"));
2574    }
2575}