1use schemars::JsonSchema;
2
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5
6use crate::schema::{
7 StrOrBoolOrArray, column::ColumnDef, constraint::TableConstraint,
8 foreign_key::ForeignKeySyntax, names::TableName, primary_key::PrimaryKeySyntax,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum TableValidationError {
13 DuplicateIndexColumn {
14 index_name: String,
15 column_name: String,
16 },
17 InvalidForeignKeyFormat {
18 column_name: String,
19 value: String,
20 },
21}
22
23impl std::fmt::Display for TableValidationError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 TableValidationError::DuplicateIndexColumn {
27 index_name,
28 column_name,
29 } => {
30 write!(
31 f,
32 "Duplicate index '{}' on column '{}': the same index name cannot be applied to the same column multiple times",
33 index_name, column_name
34 )
35 }
36 TableValidationError::InvalidForeignKeyFormat { column_name, value } => {
37 write!(
38 f,
39 "Invalid foreign key format '{}' on column '{}': expected 'table.column' format",
40 value, column_name
41 )
42 }
43 }
44 }
45}
46
47impl std::error::Error for TableValidationError {}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
50#[serde(rename_all = "snake_case")]
51pub struct TableDef {
52 pub name: TableName,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub description: Option<String>,
55 pub columns: Vec<ColumnDef>,
56 pub constraints: Vec<TableConstraint>,
57}
58
59impl TableDef {
60 pub fn normalize(&self) -> Result<Self, TableValidationError> {
68 let mut constraints = self.constraints.clone();
69
70 let mut pk_columns: Vec<String> = Vec::new();
72 let mut pk_auto_increment = false;
73
74 for col in &self.columns {
75 if let Some(ref pk) = col.primary_key {
76 match pk {
77 PrimaryKeySyntax::Bool(true) => {
78 pk_columns.push(col.name.clone());
79 }
80 PrimaryKeySyntax::Bool(false) => {}
81 PrimaryKeySyntax::Object(pk_def) => {
82 pk_columns.push(col.name.clone());
83 if pk_def.auto_increment {
84 pk_auto_increment = true;
85 }
86 }
87 }
88 }
89 }
90
91 if !pk_columns.is_empty() {
93 let has_pk_constraint = constraints
94 .iter()
95 .any(|c| matches!(c, TableConstraint::PrimaryKey { .. }));
96
97 if !has_pk_constraint {
98 constraints.push(TableConstraint::PrimaryKey {
99 auto_increment: pk_auto_increment,
100 columns: pk_columns,
101 });
102 }
103 }
104
105 let mut unique_groups: HashMap<String, Vec<String>> = HashMap::new();
108 let mut unique_order: Vec<String> = Vec::new(); for col in &self.columns {
111 if let Some(ref unique_val) = col.unique {
112 match unique_val {
113 StrOrBoolOrArray::Str(name) => {
114 let unique_name = name.clone();
116
117 if !unique_groups.contains_key(&unique_name) {
118 unique_order.push(unique_name.clone());
119 }
120
121 unique_groups
122 .entry(unique_name)
123 .or_default()
124 .push(col.name.clone());
125 }
126 StrOrBoolOrArray::Bool(true) => {
127 let group_key = format!("__auto_{}", col.name);
129
130 if !unique_groups.contains_key(&group_key) {
131 unique_order.push(group_key.clone());
132 }
133
134 unique_groups
135 .entry(group_key)
136 .or_default()
137 .push(col.name.clone());
138 }
139 StrOrBoolOrArray::Bool(false) => continue,
140 StrOrBoolOrArray::Array(names) => {
141 for unique_name in names {
144 if !unique_groups.contains_key(unique_name.as_str()) {
145 unique_order.push(unique_name.clone());
146 }
147
148 unique_groups
149 .entry(unique_name.clone())
150 .or_default()
151 .push(col.name.clone());
152 }
153 }
154 }
155 }
156 }
157
158 for unique_name in unique_order {
160 let columns = unique_groups.get(&unique_name).unwrap().clone();
161
162 let constraint_name = if unique_name.starts_with("__auto_") {
165 None
167 } else {
168 Some(unique_name.clone())
170 };
171
172 let exists = constraints.iter().any(|c| {
174 if let TableConstraint::Unique {
175 name,
176 columns: cols,
177 } = c
178 {
179 match (&constraint_name, name) {
181 (Some(n1), Some(n2)) => n1 == n2,
182 (None, None) => cols == &columns,
183 _ => false,
184 }
185 } else {
186 false
187 }
188 });
189
190 if !exists {
191 constraints.push(TableConstraint::Unique {
192 name: constraint_name,
193 columns,
194 });
195 }
196 }
197
198 for col in &self.columns {
200 if let Some(ref fk_syntax) = col.foreign_key {
202 let (ref_table, ref_columns, on_delete, on_update) = match fk_syntax {
204 ForeignKeySyntax::String(s) => {
205 let parts: Vec<&str> = s.split('.').collect();
207 if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
208 return Err(TableValidationError::InvalidForeignKeyFormat {
209 column_name: col.name.clone(),
210 value: s.clone(),
211 });
212 }
213 (parts[0].to_string(), vec![parts[1].to_string()], None, None)
214 }
215 ForeignKeySyntax::Object(fk_def) => (
216 fk_def.ref_table.clone(),
217 fk_def.ref_columns.clone(),
218 fk_def.on_delete.clone(),
219 fk_def.on_update.clone(),
220 ),
221 };
222
223 let exists = constraints.iter().any(|c| {
225 if let TableConstraint::ForeignKey { columns, .. } = c {
226 columns.len() == 1 && columns[0] == col.name
227 } else {
228 false
229 }
230 });
231
232 if !exists {
233 constraints.push(TableConstraint::ForeignKey {
234 name: None,
235 columns: vec![col.name.clone()],
236 ref_table,
237 ref_columns,
238 on_delete,
239 on_update,
240 });
241 }
242 }
243 }
244
245 let mut index_groups: HashMap<String, Vec<String>> = HashMap::new();
248 let mut index_order: Vec<String> = Vec::new(); let mut inline_index_column_tracker: HashMap<String, HashSet<String>> = HashMap::new();
252
253 for col in &self.columns {
254 if let Some(ref index_val) = col.index {
255 match index_val {
256 StrOrBoolOrArray::Str(name) => {
257 let index_name = name.clone();
259
260 if let Some(columns) = inline_index_column_tracker.get(name.as_str())
262 && columns.contains(col.name.as_str())
263 {
264 return Err(TableValidationError::DuplicateIndexColumn {
265 index_name: name.clone(),
266 column_name: col.name.clone(),
267 });
268 }
269
270 if !index_groups.contains_key(&index_name) {
271 index_order.push(index_name.clone());
272 }
273
274 index_groups
275 .entry(index_name.clone())
276 .or_default()
277 .push(col.name.clone());
278
279 inline_index_column_tracker
280 .entry(index_name)
281 .or_default()
282 .insert(col.name.clone());
283 }
284 StrOrBoolOrArray::Bool(true) => {
285 let group_key = format!("__auto_{}", col.name);
289
290 if let Some(columns) = inline_index_column_tracker.get(group_key.as_str())
292 && columns.contains(col.name.as_str())
293 {
294 return Err(TableValidationError::DuplicateIndexColumn {
295 index_name: group_key.clone(),
296 column_name: col.name.clone(),
297 });
298 }
299
300 if !index_groups.contains_key(&group_key) {
301 index_order.push(group_key.clone());
302 }
303
304 index_groups
305 .entry(group_key.clone())
306 .or_default()
307 .push(col.name.clone());
308
309 inline_index_column_tracker
310 .entry(group_key)
311 .or_default()
312 .insert(col.name.clone());
313 }
314 StrOrBoolOrArray::Bool(false) => continue,
315 StrOrBoolOrArray::Array(names) => {
316 let mut seen_in_array = HashSet::new();
320 for index_name in names {
321 if seen_in_array.contains(index_name.as_str()) {
323 return Err(TableValidationError::DuplicateIndexColumn {
324 index_name: index_name.clone(),
325 column_name: col.name.clone(),
326 });
327 }
328 seen_in_array.insert(index_name.clone());
329
330 if let Some(columns) =
333 inline_index_column_tracker.get(index_name.as_str())
334 && columns.contains(col.name.as_str())
335 {
336 return Err(TableValidationError::DuplicateIndexColumn {
337 index_name: index_name.clone(),
338 column_name: col.name.clone(),
339 });
340 }
341
342 if !index_groups.contains_key(index_name.as_str()) {
343 index_order.push(index_name.clone());
344 }
345
346 index_groups
347 .entry(index_name.clone())
348 .or_default()
349 .push(col.name.clone());
350
351 inline_index_column_tracker
352 .entry(index_name.clone())
353 .or_default()
354 .insert(col.name.clone());
355 }
356 }
357 }
358 }
359 }
360
361 for index_name in index_order {
363 let columns = index_groups.get(&index_name).unwrap().clone();
364
365 let constraint_name = if index_name.starts_with("__auto_") {
368 None
370 } else {
371 Some(index_name.clone())
373 };
374
375 let exists = constraints.iter().any(|c| {
377 if let TableConstraint::Index {
378 name,
379 columns: cols,
380 } = c
381 {
382 match (&constraint_name, name) {
384 (Some(n1), Some(n2)) => n1 == n2,
385 (None, None) => cols == &columns,
386 _ => false,
387 }
388 } else {
389 false
390 }
391 });
392
393 if !exists {
394 constraints.push(TableConstraint::Index {
395 name: constraint_name,
396 columns,
397 });
398 }
399 }
400
401 Ok(TableDef {
402 name: self.name.clone(),
403 description: self.description.clone(),
404 columns: self.columns.clone(),
405 constraints,
406 })
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use crate::schema::column::{ColumnType, SimpleColumnType};
414 use crate::schema::foreign_key::{ForeignKeyDef, ForeignKeySyntax};
415 use crate::schema::primary_key::PrimaryKeySyntax;
416 use crate::schema::reference::ReferenceAction;
417 use crate::schema::str_or_bool::StrOrBoolOrArray;
418
419 fn col(name: &str, ty: ColumnType) -> ColumnDef {
420 ColumnDef {
421 name: name.to_string(),
422 r#type: ty,
423 nullable: true,
424 default: None,
425 comment: None,
426 primary_key: None,
427 unique: None,
428 index: None,
429 foreign_key: None,
430 }
431 }
432
433 #[test]
434 fn normalize_inline_primary_key() {
435 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
436 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
437
438 let table = TableDef {
439 name: "users".into(),
440 description: None,
441 columns: vec![
442 id_col,
443 col("name", ColumnType::Simple(SimpleColumnType::Text)),
444 ],
445 constraints: vec![],
446 };
447
448 let normalized = table.normalize().unwrap();
449 assert_eq!(normalized.constraints.len(), 1);
450 assert!(matches!(
451 &normalized.constraints[0],
452 TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string()]
453 ));
454 }
455
456 #[test]
457 fn normalize_multiple_inline_primary_keys() {
458 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
459 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
460
461 let mut tenant_col = col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer));
462 tenant_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
463
464 let table = TableDef {
465 name: "users".into(),
466 description: None,
467 columns: vec![id_col, tenant_col],
468 constraints: vec![],
469 };
470
471 let normalized = table.normalize().unwrap();
472 assert_eq!(normalized.constraints.len(), 1);
473 assert!(matches!(
474 &normalized.constraints[0],
475 TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string(), "tenant_id".to_string()]
476 ));
477 }
478
479 #[test]
480 fn normalize_does_not_duplicate_existing_pk() {
481 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
482 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
483
484 let table = TableDef {
485 name: "users".into(),
486 description: None,
487 columns: vec![id_col],
488 constraints: vec![TableConstraint::PrimaryKey {
489 auto_increment: false,
490 columns: vec!["id".into()],
491 }],
492 };
493
494 let normalized = table.normalize().unwrap();
495 assert_eq!(normalized.constraints.len(), 1);
496 }
497
498 #[test]
499 fn normalize_ignores_primary_key_false() {
500 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
501 id_col.primary_key = Some(PrimaryKeySyntax::Bool(false));
502
503 let table = TableDef {
504 name: "users".into(),
505 description: None,
506 columns: vec![
507 id_col,
508 col("name", ColumnType::Simple(SimpleColumnType::Text)),
509 ],
510 constraints: vec![],
511 };
512
513 let normalized = table.normalize().unwrap();
514 assert_eq!(normalized.constraints.len(), 0);
516 }
517
518 #[test]
519 fn normalize_inline_unique_bool() {
520 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
521 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
522
523 let table = TableDef {
524 name: "users".into(),
525 description: None,
526 columns: vec![
527 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
528 email_col,
529 ],
530 constraints: vec![],
531 };
532
533 let normalized = table.normalize().unwrap();
534 assert_eq!(normalized.constraints.len(), 1);
535 assert!(matches!(
536 &normalized.constraints[0],
537 TableConstraint::Unique { name: None, columns } if columns == &["email".to_string()]
538 ));
539 }
540
541 #[test]
542 fn normalize_inline_unique_with_name() {
543 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
544 email_col.unique = Some(StrOrBoolOrArray::Str("uq_users_email".into()));
545
546 let table = TableDef {
547 name: "users".into(),
548 description: None,
549 columns: vec![
550 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
551 email_col,
552 ],
553 constraints: vec![],
554 };
555
556 let normalized = table.normalize().unwrap();
557 assert_eq!(normalized.constraints.len(), 1);
558 assert!(matches!(
559 &normalized.constraints[0],
560 TableConstraint::Unique { name: Some(n), columns }
561 if n == "uq_users_email" && columns == &["email".to_string()]
562 ));
563 }
564
565 #[test]
566 fn normalize_composite_unique_from_string_name() {
567 let mut route_col = col("join_route", ColumnType::Simple(SimpleColumnType::Text));
570 route_col.unique = Some(StrOrBoolOrArray::Str("route_provider_id".into()));
571
572 let mut provider_col = col("provider_id", ColumnType::Simple(SimpleColumnType::Text));
573 provider_col.unique = Some(StrOrBoolOrArray::Str("route_provider_id".into()));
574
575 let table = TableDef {
576 name: "user".into(),
577 description: None,
578 columns: vec![
579 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
580 route_col,
581 provider_col,
582 ],
583 constraints: vec![],
584 };
585
586 let normalized = table.normalize().unwrap();
587 assert_eq!(normalized.constraints.len(), 1);
588 assert!(matches!(
589 &normalized.constraints[0],
590 TableConstraint::Unique { name: Some(n), columns }
591 if n == "route_provider_id"
592 && columns == &["join_route".to_string(), "provider_id".to_string()]
593 ));
594 }
595
596 #[test]
597 fn normalize_unique_name_mismatch_creates_both_constraints() {
598 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
601 email_col.unique = Some(StrOrBoolOrArray::Str("named_unique".into()));
602
603 let table = TableDef {
604 name: "user".into(),
605 description: None,
606 columns: vec![
607 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
608 email_col,
609 ],
610 constraints: vec![
611 TableConstraint::Unique {
613 name: None,
614 columns: vec!["email".into()],
615 },
616 ],
617 };
618
619 let normalized = table.normalize().unwrap();
620
621 let unique_constraints: Vec<_> = normalized
623 .constraints
624 .iter()
625 .filter(|c| matches!(c, TableConstraint::Unique { .. }))
626 .collect();
627 assert_eq!(
628 unique_constraints.len(),
629 2,
630 "Should keep both named and unnamed unique constraints as they don't match"
631 );
632
633 let has_named = unique_constraints.iter().any(
635 |c| matches!(c, TableConstraint::Unique { name: Some(n), .. } if n == "named_unique"),
636 );
637 let has_unnamed = unique_constraints
638 .iter()
639 .any(|c| matches!(c, TableConstraint::Unique { name: None, .. }));
640
641 assert!(has_named, "Should have named unique constraint");
642 assert!(has_unnamed, "Should have unnamed unique constraint");
643 }
644
645 #[test]
646 fn normalize_inline_index_bool() {
647 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
648 name_col.index = Some(StrOrBoolOrArray::Bool(true));
649
650 let table = TableDef {
651 name: "users".into(),
652 description: None,
653 columns: vec![
654 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
655 name_col,
656 ],
657 constraints: vec![],
658 };
659
660 let normalized = table.normalize().unwrap();
661 let indexes: Vec<_> = normalized
663 .constraints
664 .iter()
665 .filter(|c| matches!(c, TableConstraint::Index { .. }))
666 .collect();
667 assert_eq!(indexes.len(), 1);
668 assert!(matches!(
671 indexes[0],
672 TableConstraint::Index { name: None, columns }
673 if columns == &["name".to_string()]
674 ));
675 }
676
677 #[test]
678 fn normalize_inline_index_with_name() {
679 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
680 name_col.index = Some(StrOrBoolOrArray::Str("custom_idx_name".into()));
681
682 let table = TableDef {
683 name: "users".into(),
684 description: None,
685 columns: vec![
686 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
687 name_col,
688 ],
689 constraints: vec![],
690 };
691
692 let normalized = table.normalize().unwrap();
693 let indexes: Vec<_> = normalized
694 .constraints
695 .iter()
696 .filter(|c| matches!(c, TableConstraint::Index { .. }))
697 .collect();
698 assert_eq!(indexes.len(), 1);
699 assert!(matches!(
700 indexes[0],
701 TableConstraint::Index { name: Some(n), .. }
702 if n == "custom_idx_name"
703 ));
704 }
705
706 #[test]
707 fn normalize_inline_foreign_key() {
708 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
709 user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
710 ref_table: "users".into(),
711 ref_columns: vec!["id".into()],
712 on_delete: Some(ReferenceAction::Cascade),
713 on_update: None,
714 }));
715
716 let table = TableDef {
717 name: "posts".into(),
718 description: None,
719 columns: vec![
720 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
721 user_id_col,
722 ],
723 constraints: vec![],
724 };
725
726 let normalized = table.normalize().unwrap();
727 assert_eq!(normalized.constraints.len(), 1);
728 assert!(matches!(
729 &normalized.constraints[0],
730 TableConstraint::ForeignKey {
731 name: None,
732 columns,
733 ref_table,
734 ref_columns,
735 on_delete: Some(ReferenceAction::Cascade),
736 on_update: None,
737 } if columns == &["user_id".to_string()]
738 && ref_table == "users"
739 && ref_columns == &["id".to_string()]
740 ));
741 }
742
743 #[test]
744 fn normalize_all_inline_constraints() {
745 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
746 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
747
748 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
749 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
750
751 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
752 name_col.index = Some(StrOrBoolOrArray::Bool(true));
753
754 let mut user_id_col = col("org_id", ColumnType::Simple(SimpleColumnType::Integer));
755 user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
756 ref_table: "orgs".into(),
757 ref_columns: vec!["id".into()],
758 on_delete: None,
759 on_update: None,
760 }));
761
762 let table = TableDef {
763 name: "users".into(),
764 description: None,
765 columns: vec![id_col, email_col, name_col, user_id_col],
766 constraints: vec![],
767 };
768
769 let normalized = table.normalize().unwrap();
770 let non_index_constraints: Vec<_> = normalized
773 .constraints
774 .iter()
775 .filter(|c| !matches!(c, TableConstraint::Index { .. }))
776 .collect();
777 assert_eq!(non_index_constraints.len(), 3);
778 let indexes: Vec<_> = normalized
780 .constraints
781 .iter()
782 .filter(|c| matches!(c, TableConstraint::Index { .. }))
783 .collect();
784 assert_eq!(indexes.len(), 1);
785 }
786
787 #[test]
788 fn normalize_composite_index_from_string_name() {
789 let mut updated_at_col = col(
790 "updated_at",
791 ColumnType::Simple(SimpleColumnType::Timestamp),
792 );
793 updated_at_col.index = Some(StrOrBoolOrArray::Str("tuple".into()));
794
795 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
796 user_id_col.index = Some(StrOrBoolOrArray::Str("tuple".into()));
797
798 let table = TableDef {
799 name: "post".into(),
800 description: None,
801 columns: vec![
802 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
803 updated_at_col,
804 user_id_col,
805 ],
806 constraints: vec![],
807 };
808
809 let normalized = table.normalize().unwrap();
810 let indexes: Vec<_> = normalized
811 .constraints
812 .iter()
813 .filter_map(|c| {
814 if let TableConstraint::Index { name, columns } = c {
815 Some((name.clone(), columns.clone()))
816 } else {
817 None
818 }
819 })
820 .collect();
821 assert_eq!(indexes.len(), 1);
822 assert_eq!(indexes[0].0, Some("tuple".to_string()));
823 assert_eq!(
824 indexes[0].1,
825 vec!["updated_at".to_string(), "user_id".to_string()]
826 );
827 }
828
829 #[test]
830 fn normalize_multiple_different_indexes() {
831 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
832 col1.index = Some(StrOrBoolOrArray::Str("idx_a".into()));
833
834 let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text));
835 col2.index = Some(StrOrBoolOrArray::Str("idx_a".into()));
836
837 let mut col3 = col("col3", ColumnType::Simple(SimpleColumnType::Text));
838 col3.index = Some(StrOrBoolOrArray::Str("idx_b".into()));
839
840 let mut col4 = col("col4", ColumnType::Simple(SimpleColumnType::Text));
841 col4.index = Some(StrOrBoolOrArray::Bool(true));
842
843 let table = TableDef {
844 name: "test".into(),
845 description: None,
846 columns: vec![
847 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
848 col1,
849 col2,
850 col3,
851 col4,
852 ],
853 constraints: vec![],
854 };
855
856 let normalized = table.normalize().unwrap();
857 let indexes: Vec<_> = normalized
858 .constraints
859 .iter()
860 .filter_map(|c| {
861 if let TableConstraint::Index { name, columns } = c {
862 Some((name.clone(), columns.clone()))
863 } else {
864 None
865 }
866 })
867 .collect();
868 assert_eq!(indexes.len(), 3);
869
870 let idx_a = indexes
872 .iter()
873 .find(|(n, _)| n == &Some("idx_a".to_string()))
874 .unwrap();
875 assert_eq!(idx_a.1, vec!["col1".to_string(), "col2".to_string()]);
876
877 let idx_b = indexes
879 .iter()
880 .find(|(n, _)| n == &Some("idx_b".to_string()))
881 .unwrap();
882 assert_eq!(idx_b.1, vec!["col3".to_string()]);
883
884 let idx_col4 = indexes.iter().find(|(n, _)| n.is_none()).unwrap();
886 assert_eq!(idx_col4.1, vec!["col4".to_string()]);
887 }
888
889 #[test]
890 fn normalize_false_values_are_ignored() {
891 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
892 email_col.unique = Some(StrOrBoolOrArray::Bool(false));
893 email_col.index = Some(StrOrBoolOrArray::Bool(false));
894
895 let table = TableDef {
896 name: "users".into(),
897 description: None,
898 columns: vec![
899 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
900 email_col,
901 ],
902 constraints: vec![],
903 };
904
905 let normalized = table.normalize().unwrap();
906 assert_eq!(normalized.constraints.len(), 0);
907 }
908
909 #[test]
910 fn normalize_table_without_primary_key() {
911 let table = TableDef {
914 name: "users".into(),
915 description: None,
916 columns: vec![
917 col("name", ColumnType::Simple(SimpleColumnType::Text)),
918 col("email", ColumnType::Simple(SimpleColumnType::Text)),
919 ],
920 constraints: vec![],
921 };
922
923 let normalized = table.normalize().unwrap();
924 assert_eq!(normalized.constraints.len(), 0);
926 }
927
928 #[test]
929 fn normalize_multiple_indexes_from_same_array() {
930 let mut updated_at_col = col(
932 "updated_at",
933 ColumnType::Simple(SimpleColumnType::Timestamp),
934 );
935 updated_at_col.index = Some(StrOrBoolOrArray::Array(vec![
936 "tuple".into(),
937 "tuple2".into(),
938 ]));
939
940 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
941 user_id_col.index = Some(StrOrBoolOrArray::Array(vec![
942 "tuple".into(),
943 "tuple2".into(),
944 ]));
945
946 let table = TableDef {
947 name: "post".into(),
948 description: None,
949 columns: vec![
950 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
951 updated_at_col,
952 user_id_col,
953 ],
954 constraints: vec![],
955 };
956
957 let normalized = table.normalize().unwrap();
958 let indexes: Vec<_> = normalized
960 .constraints
961 .iter()
962 .filter_map(|c| {
963 if let TableConstraint::Index { name, columns } = c {
964 Some((name.clone(), columns.clone()))
965 } else {
966 None
967 }
968 })
969 .collect();
970 assert_eq!(indexes.len(), 2);
971
972 let tuple_idx = indexes
973 .iter()
974 .find(|(n, _)| n == &Some("tuple".to_string()))
975 .unwrap();
976 let mut sorted_cols = tuple_idx.1.clone();
977 sorted_cols.sort();
978 assert_eq!(
979 sorted_cols,
980 vec!["updated_at".to_string(), "user_id".to_string()]
981 );
982
983 let tuple2_idx = indexes
984 .iter()
985 .find(|(n, _)| n == &Some("tuple2".to_string()))
986 .unwrap();
987 let mut sorted_cols2 = tuple2_idx.1.clone();
988 sorted_cols2.sort();
989 assert_eq!(
990 sorted_cols2,
991 vec!["updated_at".to_string(), "user_id".to_string()]
992 );
993 }
994
995 #[test]
996 fn normalize_inline_unique_with_array_existing_constraint() {
997 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
999 col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
1000
1001 let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text));
1002 col2.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
1003
1004 let table = TableDef {
1005 name: "test".into(),
1006 description: None,
1007 columns: vec![
1008 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1009 col1,
1010 col2,
1011 ],
1012 constraints: vec![],
1013 };
1014
1015 let normalized = table.normalize().unwrap();
1016 assert_eq!(normalized.constraints.len(), 1);
1017 let unique_constraint = &normalized.constraints[0];
1018 assert!(matches!(
1019 unique_constraint,
1020 TableConstraint::Unique { name: Some(n), columns: _ }
1021 if n == "uq_group"
1022 ));
1023 if let TableConstraint::Unique { columns, .. } = unique_constraint {
1024 let mut sorted_cols = columns.clone();
1025 sorted_cols.sort();
1026 assert_eq!(sorted_cols, vec!["col1".to_string(), "col2".to_string()]);
1027 }
1028 }
1029
1030 #[test]
1031 fn normalize_inline_unique_with_array_column_already_in_constraint() {
1032 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1034 col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
1035
1036 let table = TableDef {
1037 name: "test".into(),
1038 description: None,
1039 columns: vec![
1040 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1041 col1.clone(),
1042 ],
1043 constraints: vec![],
1044 };
1045
1046 let normalized1 = table.normalize().unwrap();
1047 assert_eq!(normalized1.constraints.len(), 1);
1048
1049 let table2 = TableDef {
1051 name: "test".into(),
1052 description: None,
1053 columns: vec![
1054 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1055 col1,
1056 ],
1057 constraints: normalized1.constraints.clone(),
1058 };
1059
1060 let normalized2 = table2.normalize().unwrap();
1061 assert_eq!(normalized2.constraints.len(), 1);
1062 if let TableConstraint::Unique { columns, .. } = &normalized2.constraints[0] {
1063 assert_eq!(columns.len(), 1);
1064 assert_eq!(columns[0], "col1");
1065 }
1066 }
1067
1068 #[test]
1069 fn normalize_inline_unique_str_already_exists() {
1070 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1072 email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
1073
1074 let table = TableDef {
1075 name: "users".into(),
1076 description: None,
1077 columns: vec![
1078 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1079 email_col,
1080 ],
1081 constraints: vec![TableConstraint::Unique {
1082 name: Some("uq_email".into()),
1083 columns: vec!["email".into()],
1084 }],
1085 };
1086
1087 let normalized = table.normalize().unwrap();
1088 let unique_constraints: Vec<_> = normalized
1090 .constraints
1091 .iter()
1092 .filter(|c| matches!(c, TableConstraint::Unique { .. }))
1093 .collect();
1094 assert_eq!(unique_constraints.len(), 1);
1095 }
1096
1097 #[test]
1098 fn normalize_inline_unique_bool_already_exists() {
1099 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1101 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
1102
1103 let table = TableDef {
1104 name: "users".into(),
1105 description: None,
1106 columns: vec![
1107 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1108 email_col,
1109 ],
1110 constraints: vec![TableConstraint::Unique {
1111 name: None,
1112 columns: vec!["email".into()],
1113 }],
1114 };
1115
1116 let normalized = table.normalize().unwrap();
1117 let unique_constraints: Vec<_> = normalized
1119 .constraints
1120 .iter()
1121 .filter(|c| matches!(c, TableConstraint::Unique { .. }))
1122 .collect();
1123 assert_eq!(unique_constraints.len(), 1);
1124 }
1125
1126 #[test]
1127 fn normalize_inline_foreign_key_already_exists() {
1128 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1130 user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
1131 ref_table: "users".into(),
1132 ref_columns: vec!["id".into()],
1133 on_delete: None,
1134 on_update: None,
1135 }));
1136
1137 let table = TableDef {
1138 name: "posts".into(),
1139 description: None,
1140 columns: vec![
1141 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1142 user_id_col,
1143 ],
1144 constraints: vec![TableConstraint::ForeignKey {
1145 name: None,
1146 columns: vec!["user_id".into()],
1147 ref_table: "users".into(),
1148 ref_columns: vec!["id".into()],
1149 on_delete: None,
1150 on_update: None,
1151 }],
1152 };
1153
1154 let normalized = table.normalize().unwrap();
1155 let fk_constraints: Vec<_> = normalized
1157 .constraints
1158 .iter()
1159 .filter(|c| matches!(c, TableConstraint::ForeignKey { .. }))
1160 .collect();
1161 assert_eq!(fk_constraints.len(), 1);
1162 }
1163
1164 #[test]
1165 fn normalize_duplicate_index_same_column_str() {
1166 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1169 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1170
1171 let table = TableDef {
1172 name: "test".into(),
1173 description: None,
1174 columns: vec![
1175 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1176 col1.clone(),
1177 {
1178 let mut c = col1.clone();
1180 c.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1181 c
1182 },
1183 ],
1184 constraints: vec![],
1185 };
1186
1187 let result = table.normalize();
1188 assert!(result.is_err());
1189 if let Err(TableValidationError::DuplicateIndexColumn {
1190 index_name,
1191 column_name,
1192 }) = result
1193 {
1194 assert_eq!(index_name, "idx1");
1195 assert_eq!(column_name, "col1");
1196 } else {
1197 panic!("Expected DuplicateIndexColumn error");
1198 }
1199 }
1200
1201 #[test]
1202 fn normalize_duplicate_index_same_column_array() {
1203 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1205 col1.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into(), "idx1".into()]));
1206
1207 let table = TableDef {
1208 name: "test".into(),
1209 description: None,
1210 columns: vec![
1211 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1212 col1,
1213 ],
1214 constraints: vec![],
1215 };
1216
1217 let result = table.normalize();
1218 assert!(result.is_err());
1219 if let Err(TableValidationError::DuplicateIndexColumn {
1220 index_name,
1221 column_name,
1222 }) = result
1223 {
1224 assert_eq!(index_name, "idx1");
1225 assert_eq!(column_name, "col1");
1226 } else {
1227 panic!("Expected DuplicateIndexColumn error");
1228 }
1229 }
1230
1231 #[test]
1232 fn normalize_duplicate_index_same_column_multiple_definitions() {
1233 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1235 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1236
1237 let table = TableDef {
1238 name: "test".into(),
1239 description: None,
1240 columns: vec![
1241 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1242 col1.clone(),
1243 {
1244 let mut c = col1.clone();
1245 c.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into()]));
1246 c
1247 },
1248 ],
1249 constraints: vec![],
1250 };
1251
1252 let result = table.normalize();
1253 assert!(result.is_err());
1254 if let Err(TableValidationError::DuplicateIndexColumn {
1255 index_name,
1256 column_name,
1257 }) = result
1258 {
1259 assert_eq!(index_name, "idx1");
1260 assert_eq!(column_name, "col1");
1261 } else {
1262 panic!("Expected DuplicateIndexColumn error");
1263 }
1264 }
1265
1266 #[test]
1267 fn test_table_validation_error_display() {
1268 let error = TableValidationError::DuplicateIndexColumn {
1269 index_name: "idx_test".into(),
1270 column_name: "col1".into(),
1271 };
1272 let error_msg = format!("{}", error);
1273 assert!(error_msg.contains("idx_test"));
1274 assert!(error_msg.contains("col1"));
1275 assert!(error_msg.contains("Duplicate index"));
1276 }
1277
1278 #[test]
1279 fn normalize_inline_unique_str_with_different_constraint_type() {
1280 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1282 email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
1283
1284 let table = TableDef {
1285 name: "users".into(),
1286 description: None,
1287 columns: vec![
1288 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1289 email_col,
1290 ],
1291 constraints: vec![
1292 TableConstraint::PrimaryKey {
1294 auto_increment: false,
1295 columns: vec!["id".into()],
1296 },
1297 ],
1298 };
1299
1300 let normalized = table.normalize().unwrap();
1301 assert_eq!(normalized.constraints.len(), 2);
1303 }
1304
1305 #[test]
1306 fn normalize_inline_unique_array_with_different_constraint_type() {
1307 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1309 col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
1310
1311 let table = TableDef {
1312 name: "test".into(),
1313 description: None,
1314 columns: vec![
1315 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1316 col1,
1317 ],
1318 constraints: vec![
1319 TableConstraint::PrimaryKey {
1321 auto_increment: false,
1322 columns: vec!["id".into()],
1323 },
1324 ],
1325 };
1326
1327 let normalized = table.normalize().unwrap();
1328 assert_eq!(normalized.constraints.len(), 2);
1330 }
1331
1332 #[test]
1333 fn normalize_duplicate_index_bool_true_same_column() {
1334 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1336 col1.index = Some(StrOrBoolOrArray::Bool(true));
1337
1338 let table = TableDef {
1339 name: "test".into(),
1340 description: None,
1341 columns: vec![
1342 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1343 col1.clone(),
1344 {
1345 let mut c = col1.clone();
1347 c.index = Some(StrOrBoolOrArray::Bool(true));
1348 c
1349 },
1350 ],
1351 constraints: vec![],
1352 };
1353
1354 let result = table.normalize();
1355 assert!(result.is_err());
1356 if let Err(TableValidationError::DuplicateIndexColumn {
1357 index_name,
1358 column_name,
1359 }) = result
1360 {
1361 assert!(index_name.contains("__auto_"));
1363 assert!(index_name.contains("col1"));
1364 assert_eq!(column_name, "col1");
1365 } else {
1366 panic!("Expected DuplicateIndexColumn error");
1367 }
1368 }
1369
1370 #[test]
1371 fn normalize_inline_foreign_key_string_syntax() {
1372 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1374 user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.id".into()));
1375
1376 let table = TableDef {
1377 name: "posts".into(),
1378 description: None,
1379 columns: vec![
1380 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1381 user_id_col,
1382 ],
1383 constraints: vec![],
1384 };
1385
1386 let normalized = table.normalize().unwrap();
1387 assert_eq!(normalized.constraints.len(), 1);
1388 assert!(matches!(
1389 &normalized.constraints[0],
1390 TableConstraint::ForeignKey {
1391 name: None,
1392 columns,
1393 ref_table,
1394 ref_columns,
1395 on_delete: None,
1396 on_update: None,
1397 } if columns == &["user_id".to_string()]
1398 && ref_table == "users"
1399 && ref_columns == &["id".to_string()]
1400 ));
1401 }
1402
1403 #[test]
1404 fn normalize_inline_foreign_key_invalid_format_no_dot() {
1405 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1407 user_id_col.foreign_key = Some(ForeignKeySyntax::String("usersid".into()));
1408
1409 let table = TableDef {
1410 name: "posts".into(),
1411 description: None,
1412 columns: vec![
1413 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1414 user_id_col,
1415 ],
1416 constraints: vec![],
1417 };
1418
1419 let result = table.normalize();
1420 assert!(result.is_err());
1421 if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1422 assert_eq!(column_name, "user_id");
1423 assert_eq!(value, "usersid");
1424 } else {
1425 panic!("Expected InvalidForeignKeyFormat error");
1426 }
1427 }
1428
1429 #[test]
1430 fn normalize_inline_foreign_key_invalid_format_empty_table() {
1431 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1433 user_id_col.foreign_key = Some(ForeignKeySyntax::String(".id".into()));
1434
1435 let table = TableDef {
1436 name: "posts".into(),
1437 description: None,
1438 columns: vec![
1439 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1440 user_id_col,
1441 ],
1442 constraints: vec![],
1443 };
1444
1445 let result = table.normalize();
1446 assert!(result.is_err());
1447 if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1448 assert_eq!(column_name, "user_id");
1449 assert_eq!(value, ".id");
1450 } else {
1451 panic!("Expected InvalidForeignKeyFormat error");
1452 }
1453 }
1454
1455 #[test]
1456 fn normalize_inline_foreign_key_invalid_format_empty_column() {
1457 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1459 user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.".into()));
1460
1461 let table = TableDef {
1462 name: "posts".into(),
1463 description: None,
1464 columns: vec![
1465 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1466 user_id_col,
1467 ],
1468 constraints: vec![],
1469 };
1470
1471 let result = table.normalize();
1472 assert!(result.is_err());
1473 if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1474 assert_eq!(column_name, "user_id");
1475 assert_eq!(value, "users.");
1476 } else {
1477 panic!("Expected InvalidForeignKeyFormat error");
1478 }
1479 }
1480
1481 #[test]
1482 fn normalize_inline_foreign_key_invalid_format_too_many_parts() {
1483 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1485 user_id_col.foreign_key = Some(ForeignKeySyntax::String("schema.users.id".into()));
1486
1487 let table = TableDef {
1488 name: "posts".into(),
1489 description: None,
1490 columns: vec![
1491 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1492 user_id_col,
1493 ],
1494 constraints: vec![],
1495 };
1496
1497 let result = table.normalize();
1498 assert!(result.is_err());
1499 if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1500 assert_eq!(column_name, "user_id");
1501 assert_eq!(value, "schema.users.id");
1502 } else {
1503 panic!("Expected InvalidForeignKeyFormat error");
1504 }
1505 }
1506
1507 #[test]
1508 fn normalize_inline_primary_key_with_auto_increment() {
1509 use crate::schema::primary_key::PrimaryKeyDef;
1510
1511 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
1512 id_col.primary_key = Some(PrimaryKeySyntax::Object(PrimaryKeyDef {
1513 auto_increment: true,
1514 }));
1515
1516 let table = TableDef {
1517 name: "users".into(),
1518 description: None,
1519 columns: vec![
1520 id_col,
1521 col("name", ColumnType::Simple(SimpleColumnType::Text)),
1522 ],
1523 constraints: vec![],
1524 };
1525
1526 let normalized = table.normalize().unwrap();
1527 assert_eq!(normalized.constraints.len(), 1);
1528 assert!(matches!(
1529 &normalized.constraints[0],
1530 TableConstraint::PrimaryKey { auto_increment: true, columns } if columns == &["id".to_string()]
1531 ));
1532 }
1533
1534 #[test]
1535 fn normalize_duplicate_inline_index_on_same_column() {
1536 use crate::schema::str_or_bool::StrOrBoolOrArray;
1539
1540 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1542 email_col.index = Some(StrOrBoolOrArray::Array(vec![
1543 "idx_email".into(),
1544 "idx_email".into(), ]));
1546
1547 let table = TableDef {
1548 name: "users".into(),
1549 description: None,
1550 columns: vec![
1551 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1552 email_col,
1553 ],
1554 constraints: vec![],
1555 };
1556
1557 let result = table.normalize();
1558 assert!(result.is_err());
1559 if let Err(TableValidationError::DuplicateIndexColumn {
1560 index_name,
1561 column_name,
1562 }) = result
1563 {
1564 assert_eq!(index_name, "idx_email");
1565 assert_eq!(column_name, "email");
1566 } else {
1567 panic!("Expected DuplicateIndexColumn error, got: {:?}", result);
1568 }
1569 }
1570
1571 #[test]
1572 fn test_invalid_foreign_key_format_error_display() {
1573 let error = TableValidationError::InvalidForeignKeyFormat {
1574 column_name: "user_id".into(),
1575 value: "invalid".into(),
1576 };
1577 let error_msg = format!("{}", error);
1578 assert!(error_msg.contains("user_id"));
1579 assert!(error_msg.contains("invalid"));
1580 assert!(error_msg.contains("table.column"));
1581 }
1582}