1use schemars::JsonSchema;
2
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5
6use crate::schema::{
7 StrOrBoolOrArray, column::ColumnDef, constraint::TableConstraint, index::IndexDef,
8 names::TableName,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum TableValidationError {
13 DuplicateIndexColumn {
14 index_name: String,
15 column_name: String,
16 },
17}
18
19impl std::fmt::Display for TableValidationError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 TableValidationError::DuplicateIndexColumn {
23 index_name,
24 column_name,
25 } => {
26 write!(
27 f,
28 "Duplicate index '{}' on column '{}': the same index name cannot be applied to the same column multiple times",
29 index_name, column_name
30 )
31 }
32 }
33 }
34}
35
36impl std::error::Error for TableValidationError {}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
39#[serde(rename_all = "snake_case")]
40pub struct TableDef {
41 pub name: TableName,
42 pub columns: Vec<ColumnDef>,
43 pub constraints: Vec<TableConstraint>,
44 pub indexes: Vec<IndexDef>,
45}
46
47impl TableDef {
48 pub fn normalize(&self) -> Result<Self, TableValidationError> {
56 let mut constraints = self.constraints.clone();
57 let mut indexes = self.indexes.clone();
58
59 let pk_columns: Vec<String> = self
61 .columns
62 .iter()
63 .filter(|c| c.primary_key == Some(true))
64 .map(|c| c.name.clone())
65 .collect();
66
67 if !pk_columns.is_empty() {
69 let has_pk = constraints
70 .iter()
71 .any(|c| matches!(c, TableConstraint::PrimaryKey { .. }));
72 if !has_pk {
73 constraints.push(TableConstraint::PrimaryKey {
74 columns: pk_columns,
75 });
76 }
77 }
78
79 for col in &self.columns {
81 if let Some(ref unique_val) = col.unique {
83 match unique_val {
84 StrOrBoolOrArray::Str(name) => {
85 let constraint_name = Some(name.clone());
86
87 let exists = constraints.iter().any(|c| {
89 if let TableConstraint::Unique {
90 name: c_name,
91 columns,
92 } = c
93 {
94 c_name.as_ref() == Some(name)
95 && columns.len() == 1
96 && columns[0] == col.name
97 } else {
98 false
99 }
100 });
101
102 if !exists {
103 constraints.push(TableConstraint::Unique {
104 name: constraint_name,
105 columns: vec![col.name.clone()],
106 });
107 }
108 }
109 StrOrBoolOrArray::Bool(true) => {
110 let exists = constraints.iter().any(|c| {
111 if let TableConstraint::Unique {
112 name: None,
113 columns,
114 } = c
115 {
116 columns.len() == 1 && columns[0] == col.name
117 } else {
118 false
119 }
120 });
121
122 if !exists {
123 constraints.push(TableConstraint::Unique {
124 name: None,
125 columns: vec![col.name.clone()],
126 });
127 }
128 }
129 StrOrBoolOrArray::Bool(false) => continue,
130 StrOrBoolOrArray::Array(names) => {
131 for constraint_name in names {
134 if let Some(existing) = constraints.iter_mut().find(|c| {
136 if let TableConstraint::Unique { name: Some(n), .. } = c {
137 n == constraint_name
138 } else {
139 false
140 }
141 }) {
142 if let TableConstraint::Unique { columns, .. } = existing
144 && !columns.contains(&col.name)
145 {
146 columns.push(col.name.clone());
147 }
148 } else {
149 constraints.push(TableConstraint::Unique {
151 name: Some(constraint_name.clone()),
152 columns: vec![col.name.clone()],
153 });
154 }
155 }
156 }
157 }
158 }
159
160 if let Some(ref fk) = col.foreign_key {
162 let exists = constraints.iter().any(|c| {
164 if let TableConstraint::ForeignKey { columns, .. } = c {
165 columns.len() == 1 && columns[0] == col.name
166 } else {
167 false
168 }
169 });
170
171 if !exists {
172 constraints.push(TableConstraint::ForeignKey {
173 name: None,
174 columns: vec![col.name.clone()],
175 ref_table: fk.ref_table.clone(),
176 ref_columns: fk.ref_columns.clone(),
177 on_delete: fk.on_delete.clone(),
178 on_update: fk.on_update.clone(),
179 });
180 }
181 }
182 }
183
184 let mut index_groups: HashMap<String, Vec<String>> = HashMap::new();
187 let mut index_order: Vec<String> = Vec::new(); let mut inline_index_column_tracker: HashMap<String, HashSet<String>> = HashMap::new();
191
192 for col in &self.columns {
193 if let Some(ref index_val) = col.index {
194 match index_val {
195 StrOrBoolOrArray::Str(name) => {
196 let index_name = name.clone();
198
199 if let Some(columns) = inline_index_column_tracker.get(name.as_str())
201 && columns.contains(col.name.as_str())
202 {
203 return Err(TableValidationError::DuplicateIndexColumn {
204 index_name: name.clone(),
205 column_name: col.name.clone(),
206 });
207 }
208
209 if !index_groups.contains_key(&index_name) {
210 index_order.push(index_name.clone());
211 }
212
213 index_groups
214 .entry(index_name.clone())
215 .or_default()
216 .push(col.name.clone());
217
218 inline_index_column_tracker
219 .entry(index_name)
220 .or_default()
221 .insert(col.name.clone());
222 }
223 StrOrBoolOrArray::Bool(true) => {
224 let index_name = format!("idx_{}_{}", self.name, col.name);
226
227 if let Some(columns) = inline_index_column_tracker.get(index_name.as_str())
230 && columns.contains(col.name.as_str())
231 {
232 return Err(TableValidationError::DuplicateIndexColumn {
233 index_name: index_name.clone(),
234 column_name: col.name.clone(),
235 });
236 }
237
238 if !index_groups.contains_key(&index_name) {
239 index_order.push(index_name.clone());
240 }
241
242 index_groups
243 .entry(index_name.clone())
244 .or_default()
245 .push(col.name.clone());
246
247 inline_index_column_tracker
248 .entry(index_name)
249 .or_default()
250 .insert(col.name.clone());
251 }
252 StrOrBoolOrArray::Bool(false) => continue,
253 StrOrBoolOrArray::Array(names) => {
254 let mut seen_in_array = HashSet::new();
258 for index_name in names {
259 if seen_in_array.contains(index_name.as_str()) {
261 return Err(TableValidationError::DuplicateIndexColumn {
262 index_name: index_name.clone(),
263 column_name: col.name.clone(),
264 });
265 }
266 seen_in_array.insert(index_name.clone());
267
268 if let Some(columns) =
271 inline_index_column_tracker.get(index_name.as_str())
272 && columns.contains(col.name.as_str())
273 {
274 return Err(TableValidationError::DuplicateIndexColumn {
275 index_name: index_name.clone(),
276 column_name: col.name.clone(),
277 });
278 }
279
280 if !index_groups.contains_key(index_name.as_str()) {
281 index_order.push(index_name.clone());
282 }
283
284 index_groups
285 .entry(index_name.clone())
286 .or_default()
287 .push(col.name.clone());
288
289 inline_index_column_tracker
290 .entry(index_name.clone())
291 .or_default()
292 .insert(col.name.clone());
293 }
294 }
295 }
296 }
297 }
298
299 for index_name in index_order {
301 let columns = index_groups.get(&index_name).unwrap().clone();
302
303 let exists = indexes.iter().any(|i| i.name == index_name);
306
307 if !exists {
308 indexes.push(IndexDef {
309 name: index_name,
310 columns,
311 unique: false,
312 });
313 }
314 }
315
316 Ok(TableDef {
317 name: self.name.clone(),
318 columns: self.columns.clone(),
319 constraints,
320 indexes,
321 })
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::schema::column::{ColumnType, SimpleColumnType};
329 use crate::schema::foreign_key::ForeignKeyDef;
330 use crate::schema::reference::ReferenceAction;
331 use crate::schema::str_or_bool::StrOrBoolOrArray;
332
333 fn col(name: &str, ty: ColumnType) -> ColumnDef {
334 ColumnDef {
335 name: name.to_string(),
336 r#type: ty,
337 nullable: true,
338 default: None,
339 comment: None,
340 primary_key: None,
341 unique: None,
342 index: None,
343 foreign_key: None,
344 }
345 }
346
347 #[test]
348 fn normalize_inline_primary_key() {
349 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
350 id_col.primary_key = Some(true);
351
352 let table = TableDef {
353 name: "users".into(),
354 columns: vec![
355 id_col,
356 col("name", ColumnType::Simple(SimpleColumnType::Text)),
357 ],
358 constraints: vec![],
359 indexes: vec![],
360 };
361
362 let normalized = table.normalize().unwrap();
363 assert_eq!(normalized.constraints.len(), 1);
364 assert!(matches!(
365 &normalized.constraints[0],
366 TableConstraint::PrimaryKey { columns } if columns == &["id".to_string()]
367 ));
368 }
369
370 #[test]
371 fn normalize_multiple_inline_primary_keys() {
372 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
373 id_col.primary_key = Some(true);
374
375 let mut tenant_col = col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer));
376 tenant_col.primary_key = Some(true);
377
378 let table = TableDef {
379 name: "users".into(),
380 columns: vec![id_col, tenant_col],
381 constraints: vec![],
382 indexes: vec![],
383 };
384
385 let normalized = table.normalize().unwrap();
386 assert_eq!(normalized.constraints.len(), 1);
387 assert!(matches!(
388 &normalized.constraints[0],
389 TableConstraint::PrimaryKey { columns } if columns == &["id".to_string(), "tenant_id".to_string()]
390 ));
391 }
392
393 #[test]
394 fn normalize_does_not_duplicate_existing_pk() {
395 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
396 id_col.primary_key = Some(true);
397
398 let table = TableDef {
399 name: "users".into(),
400 columns: vec![id_col],
401 constraints: vec![TableConstraint::PrimaryKey {
402 columns: vec!["id".into()],
403 }],
404 indexes: vec![],
405 };
406
407 let normalized = table.normalize().unwrap();
408 assert_eq!(normalized.constraints.len(), 1);
409 }
410
411 #[test]
412 fn normalize_inline_unique_bool() {
413 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
414 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
415
416 let table = TableDef {
417 name: "users".into(),
418 columns: vec![
419 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
420 email_col,
421 ],
422 constraints: vec![],
423 indexes: vec![],
424 };
425
426 let normalized = table.normalize().unwrap();
427 assert_eq!(normalized.constraints.len(), 1);
428 assert!(matches!(
429 &normalized.constraints[0],
430 TableConstraint::Unique { name: None, columns } if columns == &["email".to_string()]
431 ));
432 }
433
434 #[test]
435 fn normalize_inline_unique_with_name() {
436 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
437 email_col.unique = Some(StrOrBoolOrArray::Str("uq_users_email".into()));
438
439 let table = TableDef {
440 name: "users".into(),
441 columns: vec![
442 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
443 email_col,
444 ],
445 constraints: vec![],
446 indexes: vec![],
447 };
448
449 let normalized = table.normalize().unwrap();
450 assert_eq!(normalized.constraints.len(), 1);
451 assert!(matches!(
452 &normalized.constraints[0],
453 TableConstraint::Unique { name: Some(n), columns }
454 if n == "uq_users_email" && columns == &["email".to_string()]
455 ));
456 }
457
458 #[test]
459 fn normalize_inline_index_bool() {
460 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
461 name_col.index = Some(StrOrBoolOrArray::Bool(true));
462
463 let table = TableDef {
464 name: "users".into(),
465 columns: vec![
466 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
467 name_col,
468 ],
469 constraints: vec![],
470 indexes: vec![],
471 };
472
473 let normalized = table.normalize().unwrap();
474 assert_eq!(normalized.indexes.len(), 1);
475 assert_eq!(normalized.indexes[0].name, "idx_users_name");
476 assert_eq!(normalized.indexes[0].columns, vec!["name".to_string()]);
477 assert!(!normalized.indexes[0].unique);
478 }
479
480 #[test]
481 fn normalize_inline_index_with_name() {
482 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
483 name_col.index = Some(StrOrBoolOrArray::Str("custom_idx_name".into()));
484
485 let table = TableDef {
486 name: "users".into(),
487 columns: vec![
488 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
489 name_col,
490 ],
491 constraints: vec![],
492 indexes: vec![],
493 };
494
495 let normalized = table.normalize().unwrap();
496 assert_eq!(normalized.indexes.len(), 1);
497 assert_eq!(normalized.indexes[0].name, "custom_idx_name");
498 }
499
500 #[test]
501 fn normalize_inline_foreign_key() {
502 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
503 user_id_col.foreign_key = Some(ForeignKeyDef {
504 ref_table: "users".into(),
505 ref_columns: vec!["id".into()],
506 on_delete: Some(ReferenceAction::Cascade),
507 on_update: None,
508 });
509
510 let table = TableDef {
511 name: "posts".into(),
512 columns: vec![
513 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
514 user_id_col,
515 ],
516 constraints: vec![],
517 indexes: vec![],
518 };
519
520 let normalized = table.normalize().unwrap();
521 assert_eq!(normalized.constraints.len(), 1);
522 assert!(matches!(
523 &normalized.constraints[0],
524 TableConstraint::ForeignKey {
525 name: None,
526 columns,
527 ref_table,
528 ref_columns,
529 on_delete: Some(ReferenceAction::Cascade),
530 on_update: None,
531 } if columns == &["user_id".to_string()]
532 && ref_table == "users"
533 && ref_columns == &["id".to_string()]
534 ));
535 }
536
537 #[test]
538 fn normalize_all_inline_constraints() {
539 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
540 id_col.primary_key = Some(true);
541
542 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
543 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
544
545 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
546 name_col.index = Some(StrOrBoolOrArray::Bool(true));
547
548 let mut user_id_col = col("org_id", ColumnType::Simple(SimpleColumnType::Integer));
549 user_id_col.foreign_key = Some(ForeignKeyDef {
550 ref_table: "orgs".into(),
551 ref_columns: vec!["id".into()],
552 on_delete: None,
553 on_update: None,
554 });
555
556 let table = TableDef {
557 name: "users".into(),
558 columns: vec![id_col, email_col, name_col, user_id_col],
559 constraints: vec![],
560 indexes: vec![],
561 };
562
563 let normalized = table.normalize().unwrap();
564 assert_eq!(normalized.constraints.len(), 3);
566 assert_eq!(normalized.indexes.len(), 1);
568 }
569
570 #[test]
571 fn normalize_composite_index_from_string_name() {
572 let mut updated_at_col = col(
573 "updated_at",
574 ColumnType::Simple(SimpleColumnType::Timestamp),
575 );
576 updated_at_col.index = Some(StrOrBoolOrArray::Str("tuple".into()));
577
578 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
579 user_id_col.index = Some(StrOrBoolOrArray::Str("tuple".into()));
580
581 let table = TableDef {
582 name: "post".into(),
583 columns: vec![
584 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
585 updated_at_col,
586 user_id_col,
587 ],
588 constraints: vec![],
589 indexes: vec![],
590 };
591
592 let normalized = table.normalize().unwrap();
593 assert_eq!(normalized.indexes.len(), 1);
594 assert_eq!(normalized.indexes[0].name, "tuple");
595 assert_eq!(
596 normalized.indexes[0].columns,
597 vec!["updated_at".to_string(), "user_id".to_string()]
598 );
599 assert!(!normalized.indexes[0].unique);
600 }
601
602 #[test]
603 fn normalize_multiple_different_indexes() {
604 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
605 col1.index = Some(StrOrBoolOrArray::Str("idx_a".into()));
606
607 let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text));
608 col2.index = Some(StrOrBoolOrArray::Str("idx_a".into()));
609
610 let mut col3 = col("col3", ColumnType::Simple(SimpleColumnType::Text));
611 col3.index = Some(StrOrBoolOrArray::Str("idx_b".into()));
612
613 let mut col4 = col("col4", ColumnType::Simple(SimpleColumnType::Text));
614 col4.index = Some(StrOrBoolOrArray::Bool(true));
615
616 let table = TableDef {
617 name: "test".into(),
618 columns: vec![
619 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
620 col1,
621 col2,
622 col3,
623 col4,
624 ],
625 constraints: vec![],
626 indexes: vec![],
627 };
628
629 let normalized = table.normalize().unwrap();
630 assert_eq!(normalized.indexes.len(), 3);
631
632 let idx_a = normalized
634 .indexes
635 .iter()
636 .find(|i| i.name == "idx_a")
637 .unwrap();
638 assert_eq!(idx_a.columns, vec!["col1".to_string(), "col2".to_string()]);
639
640 let idx_b = normalized
642 .indexes
643 .iter()
644 .find(|i| i.name == "idx_b")
645 .unwrap();
646 assert_eq!(idx_b.columns, vec!["col3".to_string()]);
647
648 let idx_col4 = normalized
650 .indexes
651 .iter()
652 .find(|i| i.name == "idx_test_col4")
653 .unwrap();
654 assert_eq!(idx_col4.columns, vec!["col4".to_string()]);
655 }
656
657 #[test]
658 fn normalize_false_values_are_ignored() {
659 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
660 email_col.unique = Some(StrOrBoolOrArray::Bool(false));
661 email_col.index = Some(StrOrBoolOrArray::Bool(false));
662
663 let table = TableDef {
664 name: "users".into(),
665 columns: vec![
666 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
667 email_col,
668 ],
669 constraints: vec![],
670 indexes: vec![],
671 };
672
673 let normalized = table.normalize().unwrap();
674 assert_eq!(normalized.constraints.len(), 0);
675 assert_eq!(normalized.indexes.len(), 0);
676 }
677
678 #[test]
679 fn normalize_multiple_indexes_from_same_array() {
680 let mut updated_at_col = col(
682 "updated_at",
683 ColumnType::Simple(SimpleColumnType::Timestamp),
684 );
685 updated_at_col.index = Some(StrOrBoolOrArray::Array(vec![
686 "tuple".into(),
687 "tuple2".into(),
688 ]));
689
690 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
691 user_id_col.index = Some(StrOrBoolOrArray::Array(vec![
692 "tuple".into(),
693 "tuple2".into(),
694 ]));
695
696 let table = TableDef {
697 name: "post".into(),
698 columns: vec![
699 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
700 updated_at_col,
701 user_id_col,
702 ],
703 constraints: vec![],
704 indexes: vec![],
705 };
706
707 let normalized = table.normalize().unwrap();
708 assert_eq!(normalized.indexes.len(), 2);
710
711 let tuple_idx = normalized
712 .indexes
713 .iter()
714 .find(|i| i.name == "tuple")
715 .unwrap();
716 let mut sorted_cols = tuple_idx.columns.clone();
717 sorted_cols.sort();
718 assert_eq!(
719 sorted_cols,
720 vec!["updated_at".to_string(), "user_id".to_string()]
721 );
722
723 let tuple2_idx = normalized
724 .indexes
725 .iter()
726 .find(|i| i.name == "tuple2")
727 .unwrap();
728 let mut sorted_cols2 = tuple2_idx.columns.clone();
729 sorted_cols2.sort();
730 assert_eq!(
731 sorted_cols2,
732 vec!["updated_at".to_string(), "user_id".to_string()]
733 );
734 }
735
736 #[test]
737 fn normalize_inline_unique_with_array_existing_constraint() {
738 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
740 col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
741
742 let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text));
743 col2.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
744
745 let table = TableDef {
746 name: "test".into(),
747 columns: vec![
748 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
749 col1,
750 col2,
751 ],
752 constraints: vec![],
753 indexes: vec![],
754 };
755
756 let normalized = table.normalize().unwrap();
757 assert_eq!(normalized.constraints.len(), 1);
758 let unique_constraint = &normalized.constraints[0];
759 assert!(matches!(
760 unique_constraint,
761 TableConstraint::Unique { name: Some(n), columns: _ }
762 if n == "uq_group"
763 ));
764 if let TableConstraint::Unique { columns, .. } = unique_constraint {
765 let mut sorted_cols = columns.clone();
766 sorted_cols.sort();
767 assert_eq!(sorted_cols, vec!["col1".to_string(), "col2".to_string()]);
768 }
769 }
770
771 #[test]
772 fn normalize_inline_unique_with_array_column_already_in_constraint() {
773 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
775 col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
776
777 let table = TableDef {
778 name: "test".into(),
779 columns: vec![
780 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
781 col1.clone(),
782 ],
783 constraints: vec![],
784 indexes: vec![],
785 };
786
787 let normalized1 = table.normalize().unwrap();
788 assert_eq!(normalized1.constraints.len(), 1);
789
790 let table2 = TableDef {
792 name: "test".into(),
793 columns: vec![
794 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
795 col1,
796 ],
797 constraints: normalized1.constraints.clone(),
798 indexes: vec![],
799 };
800
801 let normalized2 = table2.normalize().unwrap();
802 assert_eq!(normalized2.constraints.len(), 1);
803 if let TableConstraint::Unique { columns, .. } = &normalized2.constraints[0] {
804 assert_eq!(columns.len(), 1);
805 assert_eq!(columns[0], "col1");
806 }
807 }
808
809 #[test]
810 fn normalize_inline_unique_str_already_exists() {
811 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
813 email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
814
815 let table = TableDef {
816 name: "users".into(),
817 columns: vec![
818 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
819 email_col,
820 ],
821 constraints: vec![TableConstraint::Unique {
822 name: Some("uq_email".into()),
823 columns: vec!["email".into()],
824 }],
825 indexes: vec![],
826 };
827
828 let normalized = table.normalize().unwrap();
829 let unique_constraints: Vec<_> = normalized
831 .constraints
832 .iter()
833 .filter(|c| matches!(c, TableConstraint::Unique { .. }))
834 .collect();
835 assert_eq!(unique_constraints.len(), 1);
836 }
837
838 #[test]
839 fn normalize_inline_unique_bool_already_exists() {
840 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
842 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
843
844 let table = TableDef {
845 name: "users".into(),
846 columns: vec![
847 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
848 email_col,
849 ],
850 constraints: vec![TableConstraint::Unique {
851 name: None,
852 columns: vec!["email".into()],
853 }],
854 indexes: vec![],
855 };
856
857 let normalized = table.normalize().unwrap();
858 let unique_constraints: Vec<_> = normalized
860 .constraints
861 .iter()
862 .filter(|c| matches!(c, TableConstraint::Unique { .. }))
863 .collect();
864 assert_eq!(unique_constraints.len(), 1);
865 }
866
867 #[test]
868 fn normalize_inline_foreign_key_already_exists() {
869 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
871 user_id_col.foreign_key = Some(ForeignKeyDef {
872 ref_table: "users".into(),
873 ref_columns: vec!["id".into()],
874 on_delete: None,
875 on_update: None,
876 });
877
878 let table = TableDef {
879 name: "posts".into(),
880 columns: vec![
881 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
882 user_id_col,
883 ],
884 constraints: vec![TableConstraint::ForeignKey {
885 name: None,
886 columns: vec!["user_id".into()],
887 ref_table: "users".into(),
888 ref_columns: vec!["id".into()],
889 on_delete: None,
890 on_update: None,
891 }],
892 indexes: vec![],
893 };
894
895 let normalized = table.normalize().unwrap();
896 let fk_constraints: Vec<_> = normalized
898 .constraints
899 .iter()
900 .filter(|c| matches!(c, TableConstraint::ForeignKey { .. }))
901 .collect();
902 assert_eq!(fk_constraints.len(), 1);
903 }
904
905 #[test]
906 fn normalize_duplicate_index_same_column_str() {
907 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
910 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
911
912 let table = TableDef {
913 name: "test".into(),
914 columns: vec![
915 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
916 col1.clone(),
917 {
918 let mut c = col1.clone();
920 c.index = Some(StrOrBoolOrArray::Str("idx1".into()));
921 c
922 },
923 ],
924 constraints: vec![],
925 indexes: vec![],
926 };
927
928 let result = table.normalize();
929 assert!(result.is_err());
930 if let Err(TableValidationError::DuplicateIndexColumn {
931 index_name,
932 column_name,
933 }) = result
934 {
935 assert_eq!(index_name, "idx1");
936 assert_eq!(column_name, "col1");
937 } else {
938 panic!("Expected DuplicateIndexColumn error");
939 }
940 }
941
942 #[test]
943 fn normalize_duplicate_index_same_column_array() {
944 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
946 col1.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into(), "idx1".into()]));
947
948 let table = TableDef {
949 name: "test".into(),
950 columns: vec![
951 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
952 col1,
953 ],
954 constraints: vec![],
955 indexes: vec![],
956 };
957
958 let result = table.normalize();
959 assert!(result.is_err());
960 if let Err(TableValidationError::DuplicateIndexColumn {
961 index_name,
962 column_name,
963 }) = result
964 {
965 assert_eq!(index_name, "idx1");
966 assert_eq!(column_name, "col1");
967 } else {
968 panic!("Expected DuplicateIndexColumn error");
969 }
970 }
971
972 #[test]
973 fn normalize_duplicate_index_same_column_multiple_definitions() {
974 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
976 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
977
978 let table = TableDef {
979 name: "test".into(),
980 columns: vec![
981 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
982 col1.clone(),
983 {
984 let mut c = col1.clone();
985 c.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into()]));
986 c
987 },
988 ],
989 constraints: vec![],
990 indexes: vec![],
991 };
992
993 let result = table.normalize();
994 assert!(result.is_err());
995 if let Err(TableValidationError::DuplicateIndexColumn {
996 index_name,
997 column_name,
998 }) = result
999 {
1000 assert_eq!(index_name, "idx1");
1001 assert_eq!(column_name, "col1");
1002 } else {
1003 panic!("Expected DuplicateIndexColumn error");
1004 }
1005 }
1006
1007 #[test]
1008 fn test_table_validation_error_display() {
1009 let error = TableValidationError::DuplicateIndexColumn {
1010 index_name: "idx_test".into(),
1011 column_name: "col1".into(),
1012 };
1013 let error_msg = format!("{}", error);
1014 assert!(error_msg.contains("idx_test"));
1015 assert!(error_msg.contains("col1"));
1016 assert!(error_msg.contains("Duplicate index"));
1017 }
1018
1019 #[test]
1020 fn normalize_inline_unique_str_with_different_constraint_type() {
1021 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1023 email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
1024
1025 let table = TableDef {
1026 name: "users".into(),
1027 columns: vec![
1028 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1029 email_col,
1030 ],
1031 constraints: vec![
1032 TableConstraint::PrimaryKey {
1034 columns: vec!["id".into()],
1035 },
1036 ],
1037 indexes: vec![],
1038 };
1039
1040 let normalized = table.normalize().unwrap();
1041 assert_eq!(normalized.constraints.len(), 2);
1043 }
1044
1045 #[test]
1046 fn normalize_inline_unique_array_with_different_constraint_type() {
1047 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1049 col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
1050
1051 let table = TableDef {
1052 name: "test".into(),
1053 columns: vec![
1054 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1055 col1,
1056 ],
1057 constraints: vec![
1058 TableConstraint::PrimaryKey {
1060 columns: vec!["id".into()],
1061 },
1062 ],
1063 indexes: vec![],
1064 };
1065
1066 let normalized = table.normalize().unwrap();
1067 assert_eq!(normalized.constraints.len(), 2);
1069 }
1070
1071 #[test]
1072 fn normalize_duplicate_index_bool_true_same_column() {
1073 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1075 col1.index = Some(StrOrBoolOrArray::Bool(true));
1076
1077 let table = TableDef {
1078 name: "test".into(),
1079 columns: vec![
1080 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1081 col1.clone(),
1082 {
1083 let mut c = col1.clone();
1085 c.index = Some(StrOrBoolOrArray::Bool(true));
1086 c
1087 },
1088 ],
1089 constraints: vec![],
1090 indexes: vec![],
1091 };
1092
1093 let result = table.normalize();
1094 assert!(result.is_err());
1095 if let Err(TableValidationError::DuplicateIndexColumn {
1096 index_name,
1097 column_name,
1098 }) = result
1099 {
1100 assert!(index_name.contains("idx_test"));
1101 assert!(index_name.contains("col1"));
1102 assert_eq!(column_name, "col1");
1103 } else {
1104 panic!("Expected DuplicateIndexColumn error");
1105 }
1106 }
1107}