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