1use 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#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SchemaColumn {
50 pub id: usize,
52
53 pub name: String,
55
56 #[doc(hidden)]
58 pub name_lower: String,
59
60 pub data_type: DataType,
62
63 pub external_type: Option<ExternalTypeRef>,
65
66 pub external_type_name: Option<String>,
68
69 pub nullable: bool,
71
72 pub primary_key: bool,
74
75 pub auto_increment: bool,
77
78 pub default_expr: Option<String>,
80
81 pub default_value: Option<Value>,
83
84 pub check_expr: Option<String>,
86
87 pub vector_dimensions: u16,
89
90 pub decimal_precision: u8,
92
93 pub decimal_scale: u8,
95
96 pub double_precision: bool,
99
100 pub text_max_chars: u32,
103}
104
105impl SchemaColumn {
106 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 pub fn with_vector_dimensions(mut self, dims: u16) -> Self {
139 self.vector_dimensions = dims;
140 self
141 }
142
143 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 pub fn with_text_max_chars(mut self, max_chars: u32) -> Self {
153 self.text_max_chars = max_chars;
154 self
155 }
156
157 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 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 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 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 #[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 #[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 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
377pub struct ForeignKeyConstraint {
378 pub column_index: usize,
380 pub column_name: String,
382 pub referenced_table: String,
384 pub referenced_column: String,
386 pub on_delete: ForeignKeyAction,
388 pub on_update: ForeignKeyAction,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq)]
398pub struct SchemaConstraint {
399 pub id: u64,
400 pub name: String,
401 pub kind: SchemaConstraintKind,
402}
403
404#[derive(Debug, Clone, PartialEq, Eq)]
406pub enum SchemaConstraintKind {
407 PrimaryKey {
408 columns: Vec<String>,
409 },
410 Unique {
411 columns: Vec<String>,
412 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 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
447pub const MAX_CONSTRAINT_NAME_BYTES: usize = 128;
450const CONSTRAINT_HASH_SUFFIX_BYTES: usize = 10; const CONSTRAINT_NAME_DESCRIPTOR_PREFIX: &str = "radixdb.constraint-name.v1";
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455pub enum ReferenceTargetKey {
456 PrimaryKey,
457 UniqueNotNull,
458}
459
460#[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#[derive(Debug)]
517pub struct Schema {
518 #[doc(hidden)]
521 pub catalog_id: [u8; 16],
522
523 #[doc(hidden)]
525 pub table_name: String,
526
527 #[doc(hidden)]
529 pub table_name_lower: String,
530
531 #[doc(hidden)]
533 pub columns: Vec<SchemaColumn>,
534
535 #[doc(hidden)]
537 pub foreign_keys: Vec<ForeignKeyConstraint>,
538
539 #[doc(hidden)]
545 pub table_checks: Vec<String>,
546
547 #[doc(hidden)]
551 pub constraints: Vec<SchemaConstraint>,
552
553 #[doc(hidden)]
555 pub next_constraint_id: u64,
556
557 #[doc(hidden)]
559 pub next_check_ordinal: u32,
560
561 #[doc(hidden)]
563 pub created_at: DateTime<Utc>,
564
565 #[doc(hidden)]
567 pub updated_at: DateTime<Utc>,
568
569 column_names_cache: OnceLock<CompactArc<Vec<String>>>,
571
572 pk_column_index_cache: OnceLock<Option<usize>>,
575
576 column_index_map_cache: OnceLock<StringMap<usize>>,
578
579 pk_indices_cache: OnceLock<Arc<Vec<usize>>>,
581
582 column_names_lower_cache: OnceLock<CompactArc<Vec<String>>>,
584}
585
586impl Clone for Schema {
587 fn clone(&self) -> Self {
588 let column_names_cache = OnceLock::new();
590 if let Some(names) = self.column_names_cache.get() {
591 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 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 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 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 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 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 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 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 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 pub fn column_count(&self) -> usize {
825 self.columns.len()
826 }
827
828 pub fn is_empty(&self) -> bool {
830 self.columns.is_empty()
831 }
832
833 pub fn table_name(&self) -> &str {
835 &self.table_name
836 }
837
838 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 pub fn columns(&self) -> &[SchemaColumn] {
872 &self.columns
873 }
874
875 pub fn foreign_keys(&self) -> &[ForeignKeyConstraint] {
877 &self.foreign_keys
878 }
879
880 pub fn table_checks(&self) -> &[String] {
882 &self.table_checks
883 }
884
885 pub fn constraints(&self) -> &[SchemaConstraint] {
887 &self.constraints
888 }
889
890 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 #[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 #[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 #[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 pub fn created_at(&self) -> DateTime<Utc> {
1176 self.created_at
1177 }
1178
1179 pub fn updated_at(&self) -> DateTime<Utc> {
1181 self.updated_at
1182 }
1183
1184 #[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 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 pub fn get_column(&self, index: usize) -> Option<&SchemaColumn> {
1213 self.columns.get(index)
1214 }
1215
1216 pub fn get_column_by_name(&self, name: &str) -> Option<&SchemaColumn> {
1218 self.find_column(name).map(|(_, col)| col)
1219 }
1220
1221 pub fn get_column_index(&self, name: &str) -> Option<usize> {
1223 self.find_column(name).map(|(idx, _)| idx)
1224 }
1225
1226 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 pub fn has_column(&self, name: &str) -> bool {
1233 self.find_column(name).is_some()
1234 }
1235
1236 pub fn column_names(&self) -> Vec<&str> {
1238 self.columns.iter().map(|c| c.name.as_str()).collect()
1239 }
1240
1241 #[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 #[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 #[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 #[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 pub fn primary_key_columns(&self) -> Vec<&SchemaColumn> {
1290 self.columns.iter().filter(|c| c.primary_key).collect()
1291 }
1292
1293 pub fn has_primary_key(&self) -> bool {
1295 self.columns.iter().any(|c| c.primary_key)
1296 }
1297
1298 #[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 #[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 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 #[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 #[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 pub fn mark_updated(&mut self) {
1709 self.updated_at = Utc::now();
1710 }
1711
1712 #[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 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 fn rebuild_caches(&mut self) {
1731 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 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 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 self.pk_indices_cache = OnceLock::new();
1755 let _ = self.pk_indices_cache.set(Arc::new(primary_key_indices));
1756
1757 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 pub fn add_column(&mut self, column: SchemaColumn) -> Result<()> {
1766 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 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 for (i, col) in self.columns.iter_mut().enumerate() {
1806 col.id = i;
1807 }
1808
1809 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 pub fn rename_column(&mut self, old_name: &str, new_name: impl Into<String>) -> Result<()> {
1825 let new_name = new_name.into();
1826
1827 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 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 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 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#[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
2063pub 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 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 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 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 pub fn add(self, name: impl Into<String>, data_type: DataType) -> Self {
2113 self.column(name, data_type, false, false)
2114 }
2115
2116 pub fn add_nullable(self, name: impl Into<String>, data_type: DataType) -> Self {
2118 self.column(name, data_type, true, false)
2119 }
2120
2121 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 #[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 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 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 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 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 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 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 pub fn is_column_nullable(&self, idx: usize) -> bool {
2216 self.columns.get(idx).is_some_and(|c| c.nullable)
2217 }
2218
2219 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 pub fn column_definition(&self, idx: usize) -> Option<&SchemaColumn> {
2230 self.columns.get(idx)
2231 }
2232
2233 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 pub fn add_foreign_key(mut self, fk: ForeignKeyConstraint) -> Self {
2250 self.foreign_keys.push(fk);
2251 self
2252 }
2253
2254 pub fn add_table_check(mut self, expression: impl Into<String>) -> Self {
2256 self.table_checks.push(expression.into());
2257 self
2258 }
2259
2260 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 let (idx, col) = schema.find_column("name").unwrap();
2323 assert_eq!(idx, 1);
2324 assert_eq!(col.name, "name");
2325
2326 let (idx, _) = schema.find_column("NAME").unwrap();
2328 assert_eq!(idx, 1);
2329
2330 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 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 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 assert_eq!(schema.columns[2].id, 2);
2524
2525 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 let err = schema.rename_column("full_name", "id").unwrap_err();
2539 assert!(matches!(err, Error::DuplicateColumn));
2540
2541 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 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 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}