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