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