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, index::IndexDef, names::TableName,
9 primary_key::PrimaryKeySyntax,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum TableValidationError {
14 DuplicateIndexColumn {
15 index_name: String,
16 column_name: String,
17 },
18 InvalidForeignKeyFormat {
19 column_name: String,
20 value: String,
21 },
22}
23
24impl std::fmt::Display for TableValidationError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 TableValidationError::DuplicateIndexColumn {
28 index_name,
29 column_name,
30 } => {
31 write!(
32 f,
33 "Duplicate index '{}' on column '{}': the same index name cannot be applied to the same column multiple times",
34 index_name, column_name
35 )
36 }
37 TableValidationError::InvalidForeignKeyFormat { column_name, value } => {
38 write!(
39 f,
40 "Invalid foreign key format '{}' on column '{}': expected 'table.column' format",
41 value, column_name
42 )
43 }
44 }
45 }
46}
47
48impl std::error::Error for TableValidationError {}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51#[serde(rename_all = "snake_case")]
52pub struct TableDef {
53 pub name: TableName,
54 pub columns: Vec<ColumnDef>,
55 pub constraints: Vec<TableConstraint>,
56 pub indexes: Vec<IndexDef>,
57}
58
59impl TableDef {
60 pub fn normalize(&self) -> Result<Self, TableValidationError> {
68 let mut constraints = self.constraints.clone();
69 let mut indexes = self.indexes.clone();
70
71 let mut pk_columns: Vec<String> = Vec::new();
73 let mut pk_auto_increment = false;
74
75 for col in &self.columns {
76 if let Some(ref pk) = col.primary_key {
77 match pk {
78 PrimaryKeySyntax::Bool(true) => {
79 pk_columns.push(col.name.clone());
80 }
81 PrimaryKeySyntax::Bool(false) => {}
82 PrimaryKeySyntax::Object(pk_def) => {
83 pk_columns.push(col.name.clone());
84 if pk_def.auto_increment {
85 pk_auto_increment = true;
86 }
87 }
88 }
89 }
90 }
91
92 if !pk_columns.is_empty() {
94 let has_pk_constraint = constraints
95 .iter()
96 .any(|c| matches!(c, TableConstraint::PrimaryKey { .. }));
97
98 if !has_pk_constraint {
99 constraints.push(TableConstraint::PrimaryKey {
100 auto_increment: pk_auto_increment,
101 columns: pk_columns,
102 });
103 }
104 }
105
106 for col in &self.columns {
108 if let Some(ref unique_val) = col.unique {
110 match unique_val {
111 StrOrBoolOrArray::Str(name) => {
112 let constraint_name = Some(name.clone());
113
114 let exists = constraints.iter().any(|c| {
116 if let TableConstraint::Unique {
117 name: c_name,
118 columns,
119 } = c
120 {
121 c_name.as_ref() == Some(name)
122 && columns.len() == 1
123 && columns[0] == col.name
124 } else {
125 false
126 }
127 });
128
129 if !exists {
130 constraints.push(TableConstraint::Unique {
131 name: constraint_name,
132 columns: vec![col.name.clone()],
133 });
134 }
135 }
136 StrOrBoolOrArray::Bool(true) => {
137 let exists = constraints.iter().any(|c| {
138 if let TableConstraint::Unique {
139 name: None,
140 columns,
141 } = c
142 {
143 columns.len() == 1 && columns[0] == col.name
144 } else {
145 false
146 }
147 });
148
149 if !exists {
150 constraints.push(TableConstraint::Unique {
151 name: None,
152 columns: vec![col.name.clone()],
153 });
154 }
155 }
156 StrOrBoolOrArray::Bool(false) => continue,
157 StrOrBoolOrArray::Array(names) => {
158 for constraint_name in names {
161 if let Some(existing) = constraints.iter_mut().find(|c| {
163 if let TableConstraint::Unique { name: Some(n), .. } = c {
164 n == constraint_name
165 } else {
166 false
167 }
168 }) {
169 if let TableConstraint::Unique { columns, .. } = existing
171 && !columns.contains(&col.name)
172 {
173 columns.push(col.name.clone());
174 }
175 } else {
176 constraints.push(TableConstraint::Unique {
178 name: Some(constraint_name.clone()),
179 columns: vec![col.name.clone()],
180 });
181 }
182 }
183 }
184 }
185 }
186
187 if let Some(ref fk_syntax) = col.foreign_key {
189 let (ref_table, ref_columns, on_delete, on_update) = match fk_syntax {
191 ForeignKeySyntax::String(s) => {
192 let parts: Vec<&str> = s.split('.').collect();
194 if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
195 return Err(TableValidationError::InvalidForeignKeyFormat {
196 column_name: col.name.clone(),
197 value: s.clone(),
198 });
199 }
200 (parts[0].to_string(), vec![parts[1].to_string()], None, None)
201 }
202 ForeignKeySyntax::Object(fk_def) => (
203 fk_def.ref_table.clone(),
204 fk_def.ref_columns.clone(),
205 fk_def.on_delete.clone(),
206 fk_def.on_update.clone(),
207 ),
208 };
209
210 let exists = constraints.iter().any(|c| {
212 if let TableConstraint::ForeignKey { columns, .. } = c {
213 columns.len() == 1 && columns[0] == col.name
214 } else {
215 false
216 }
217 });
218
219 if !exists {
220 constraints.push(TableConstraint::ForeignKey {
221 name: None,
222 columns: vec![col.name.clone()],
223 ref_table,
224 ref_columns,
225 on_delete,
226 on_update,
227 });
228 }
229 }
230 }
231
232 let mut index_groups: HashMap<String, Vec<String>> = HashMap::new();
235 let mut index_order: Vec<String> = Vec::new(); let mut inline_index_column_tracker: HashMap<String, HashSet<String>> = HashMap::new();
239
240 for col in &self.columns {
241 if let Some(ref index_val) = col.index {
242 match index_val {
243 StrOrBoolOrArray::Str(name) => {
244 let index_name = name.clone();
246
247 if let Some(columns) = inline_index_column_tracker.get(name.as_str())
249 && columns.contains(col.name.as_str())
250 {
251 return Err(TableValidationError::DuplicateIndexColumn {
252 index_name: name.clone(),
253 column_name: col.name.clone(),
254 });
255 }
256
257 if !index_groups.contains_key(&index_name) {
258 index_order.push(index_name.clone());
259 }
260
261 index_groups
262 .entry(index_name.clone())
263 .or_default()
264 .push(col.name.clone());
265
266 inline_index_column_tracker
267 .entry(index_name)
268 .or_default()
269 .insert(col.name.clone());
270 }
271 StrOrBoolOrArray::Bool(true) => {
272 let index_name = format!("idx_{}_{}", self.name, col.name);
274
275 if let Some(columns) = inline_index_column_tracker.get(index_name.as_str())
278 && columns.contains(col.name.as_str())
279 {
280 return Err(TableValidationError::DuplicateIndexColumn {
281 index_name: index_name.clone(),
282 column_name: col.name.clone(),
283 });
284 }
285
286 if !index_groups.contains_key(&index_name) {
287 index_order.push(index_name.clone());
288 }
289
290 index_groups
291 .entry(index_name.clone())
292 .or_default()
293 .push(col.name.clone());
294
295 inline_index_column_tracker
296 .entry(index_name)
297 .or_default()
298 .insert(col.name.clone());
299 }
300 StrOrBoolOrArray::Bool(false) => continue,
301 StrOrBoolOrArray::Array(names) => {
302 let mut seen_in_array = HashSet::new();
306 for index_name in names {
307 if seen_in_array.contains(index_name.as_str()) {
309 return Err(TableValidationError::DuplicateIndexColumn {
310 index_name: index_name.clone(),
311 column_name: col.name.clone(),
312 });
313 }
314 seen_in_array.insert(index_name.clone());
315
316 if let Some(columns) =
319 inline_index_column_tracker.get(index_name.as_str())
320 && columns.contains(col.name.as_str())
321 {
322 return Err(TableValidationError::DuplicateIndexColumn {
323 index_name: index_name.clone(),
324 column_name: col.name.clone(),
325 });
326 }
327
328 if !index_groups.contains_key(index_name.as_str()) {
329 index_order.push(index_name.clone());
330 }
331
332 index_groups
333 .entry(index_name.clone())
334 .or_default()
335 .push(col.name.clone());
336
337 inline_index_column_tracker
338 .entry(index_name.clone())
339 .or_default()
340 .insert(col.name.clone());
341 }
342 }
343 }
344 }
345 }
346
347 for index_name in index_order {
349 let columns = index_groups.get(&index_name).unwrap().clone();
350
351 let exists = indexes.iter().any(|i| i.name == index_name);
354
355 if !exists {
356 indexes.push(IndexDef {
357 name: index_name,
358 columns,
359 unique: false,
360 });
361 }
362 }
363
364 Ok(TableDef {
365 name: self.name.clone(),
366 columns: self.columns.clone(),
367 constraints,
368 indexes,
369 })
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::schema::column::{ColumnType, SimpleColumnType};
377 use crate::schema::foreign_key::{ForeignKeyDef, ForeignKeySyntax};
378 use crate::schema::primary_key::PrimaryKeySyntax;
379 use crate::schema::reference::ReferenceAction;
380 use crate::schema::str_or_bool::StrOrBoolOrArray;
381
382 fn col(name: &str, ty: ColumnType) -> ColumnDef {
383 ColumnDef {
384 name: name.to_string(),
385 r#type: ty,
386 nullable: true,
387 default: None,
388 comment: None,
389 primary_key: None,
390 unique: None,
391 index: None,
392 foreign_key: None,
393 }
394 }
395
396 #[test]
397 fn normalize_inline_primary_key() {
398 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
399 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
400
401 let table = TableDef {
402 name: "users".into(),
403 columns: vec![
404 id_col,
405 col("name", ColumnType::Simple(SimpleColumnType::Text)),
406 ],
407 constraints: vec![],
408 indexes: vec![],
409 };
410
411 let normalized = table.normalize().unwrap();
412 assert_eq!(normalized.constraints.len(), 1);
413 assert!(matches!(
414 &normalized.constraints[0],
415 TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string()]
416 ));
417 }
418
419 #[test]
420 fn normalize_multiple_inline_primary_keys() {
421 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
422 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
423
424 let mut tenant_col = col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer));
425 tenant_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
426
427 let table = TableDef {
428 name: "users".into(),
429 columns: vec![id_col, tenant_col],
430 constraints: vec![],
431 indexes: vec![],
432 };
433
434 let normalized = table.normalize().unwrap();
435 assert_eq!(normalized.constraints.len(), 1);
436 assert!(matches!(
437 &normalized.constraints[0],
438 TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string(), "tenant_id".to_string()]
439 ));
440 }
441
442 #[test]
443 fn normalize_does_not_duplicate_existing_pk() {
444 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
445 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
446
447 let table = TableDef {
448 name: "users".into(),
449 columns: vec![id_col],
450 constraints: vec![TableConstraint::PrimaryKey {
451 auto_increment: false,
452 columns: vec!["id".into()],
453 }],
454 indexes: vec![],
455 };
456
457 let normalized = table.normalize().unwrap();
458 assert_eq!(normalized.constraints.len(), 1);
459 }
460
461 #[test]
462 fn normalize_ignores_primary_key_false() {
463 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
464 id_col.primary_key = Some(PrimaryKeySyntax::Bool(false));
465
466 let table = TableDef {
467 name: "users".into(),
468 columns: vec![
469 id_col,
470 col("name", ColumnType::Simple(SimpleColumnType::Text)),
471 ],
472 constraints: vec![],
473 indexes: vec![],
474 };
475
476 let normalized = table.normalize().unwrap();
477 assert_eq!(normalized.constraints.len(), 0);
479 }
480
481 #[test]
482 fn normalize_inline_unique_bool() {
483 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
484 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
485
486 let table = TableDef {
487 name: "users".into(),
488 columns: vec![
489 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
490 email_col,
491 ],
492 constraints: vec![],
493 indexes: vec![],
494 };
495
496 let normalized = table.normalize().unwrap();
497 assert_eq!(normalized.constraints.len(), 1);
498 assert!(matches!(
499 &normalized.constraints[0],
500 TableConstraint::Unique { name: None, columns } if columns == &["email".to_string()]
501 ));
502 }
503
504 #[test]
505 fn normalize_inline_unique_with_name() {
506 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
507 email_col.unique = Some(StrOrBoolOrArray::Str("uq_users_email".into()));
508
509 let table = TableDef {
510 name: "users".into(),
511 columns: vec![
512 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
513 email_col,
514 ],
515 constraints: vec![],
516 indexes: vec![],
517 };
518
519 let normalized = table.normalize().unwrap();
520 assert_eq!(normalized.constraints.len(), 1);
521 assert!(matches!(
522 &normalized.constraints[0],
523 TableConstraint::Unique { name: Some(n), columns }
524 if n == "uq_users_email" && columns == &["email".to_string()]
525 ));
526 }
527
528 #[test]
529 fn normalize_inline_index_bool() {
530 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
531 name_col.index = Some(StrOrBoolOrArray::Bool(true));
532
533 let table = TableDef {
534 name: "users".into(),
535 columns: vec![
536 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
537 name_col,
538 ],
539 constraints: vec![],
540 indexes: vec![],
541 };
542
543 let normalized = table.normalize().unwrap();
544 assert_eq!(normalized.indexes.len(), 1);
545 assert_eq!(normalized.indexes[0].name, "idx_users_name");
546 assert_eq!(normalized.indexes[0].columns, vec!["name".to_string()]);
547 assert!(!normalized.indexes[0].unique);
548 }
549
550 #[test]
551 fn normalize_inline_index_with_name() {
552 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
553 name_col.index = Some(StrOrBoolOrArray::Str("custom_idx_name".into()));
554
555 let table = TableDef {
556 name: "users".into(),
557 columns: vec![
558 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
559 name_col,
560 ],
561 constraints: vec![],
562 indexes: vec![],
563 };
564
565 let normalized = table.normalize().unwrap();
566 assert_eq!(normalized.indexes.len(), 1);
567 assert_eq!(normalized.indexes[0].name, "custom_idx_name");
568 }
569
570 #[test]
571 fn normalize_inline_foreign_key() {
572 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
573 user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
574 ref_table: "users".into(),
575 ref_columns: vec!["id".into()],
576 on_delete: Some(ReferenceAction::Cascade),
577 on_update: None,
578 }));
579
580 let table = TableDef {
581 name: "posts".into(),
582 columns: vec![
583 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
584 user_id_col,
585 ],
586 constraints: vec![],
587 indexes: vec![],
588 };
589
590 let normalized = table.normalize().unwrap();
591 assert_eq!(normalized.constraints.len(), 1);
592 assert!(matches!(
593 &normalized.constraints[0],
594 TableConstraint::ForeignKey {
595 name: None,
596 columns,
597 ref_table,
598 ref_columns,
599 on_delete: Some(ReferenceAction::Cascade),
600 on_update: None,
601 } if columns == &["user_id".to_string()]
602 && ref_table == "users"
603 && ref_columns == &["id".to_string()]
604 ));
605 }
606
607 #[test]
608 fn normalize_all_inline_constraints() {
609 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
610 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
611
612 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
613 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
614
615 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
616 name_col.index = Some(StrOrBoolOrArray::Bool(true));
617
618 let mut user_id_col = col("org_id", ColumnType::Simple(SimpleColumnType::Integer));
619 user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
620 ref_table: "orgs".into(),
621 ref_columns: vec!["id".into()],
622 on_delete: None,
623 on_update: None,
624 }));
625
626 let table = TableDef {
627 name: "users".into(),
628 columns: vec![id_col, email_col, name_col, user_id_col],
629 constraints: vec![],
630 indexes: vec![],
631 };
632
633 let normalized = table.normalize().unwrap();
634 assert_eq!(normalized.constraints.len(), 3);
636 assert_eq!(normalized.indexes.len(), 1);
638 }
639
640 #[test]
641 fn normalize_composite_index_from_string_name() {
642 let mut updated_at_col = col(
643 "updated_at",
644 ColumnType::Simple(SimpleColumnType::Timestamp),
645 );
646 updated_at_col.index = Some(StrOrBoolOrArray::Str("tuple".into()));
647
648 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
649 user_id_col.index = Some(StrOrBoolOrArray::Str("tuple".into()));
650
651 let table = TableDef {
652 name: "post".into(),
653 columns: vec![
654 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
655 updated_at_col,
656 user_id_col,
657 ],
658 constraints: vec![],
659 indexes: vec![],
660 };
661
662 let normalized = table.normalize().unwrap();
663 assert_eq!(normalized.indexes.len(), 1);
664 assert_eq!(normalized.indexes[0].name, "tuple");
665 assert_eq!(
666 normalized.indexes[0].columns,
667 vec!["updated_at".to_string(), "user_id".to_string()]
668 );
669 assert!(!normalized.indexes[0].unique);
670 }
671
672 #[test]
673 fn normalize_multiple_different_indexes() {
674 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
675 col1.index = Some(StrOrBoolOrArray::Str("idx_a".into()));
676
677 let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text));
678 col2.index = Some(StrOrBoolOrArray::Str("idx_a".into()));
679
680 let mut col3 = col("col3", ColumnType::Simple(SimpleColumnType::Text));
681 col3.index = Some(StrOrBoolOrArray::Str("idx_b".into()));
682
683 let mut col4 = col("col4", ColumnType::Simple(SimpleColumnType::Text));
684 col4.index = Some(StrOrBoolOrArray::Bool(true));
685
686 let table = TableDef {
687 name: "test".into(),
688 columns: vec![
689 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
690 col1,
691 col2,
692 col3,
693 col4,
694 ],
695 constraints: vec![],
696 indexes: vec![],
697 };
698
699 let normalized = table.normalize().unwrap();
700 assert_eq!(normalized.indexes.len(), 3);
701
702 let idx_a = normalized
704 .indexes
705 .iter()
706 .find(|i| i.name == "idx_a")
707 .unwrap();
708 assert_eq!(idx_a.columns, vec!["col1".to_string(), "col2".to_string()]);
709
710 let idx_b = normalized
712 .indexes
713 .iter()
714 .find(|i| i.name == "idx_b")
715 .unwrap();
716 assert_eq!(idx_b.columns, vec!["col3".to_string()]);
717
718 let idx_col4 = normalized
720 .indexes
721 .iter()
722 .find(|i| i.name == "idx_test_col4")
723 .unwrap();
724 assert_eq!(idx_col4.columns, vec!["col4".to_string()]);
725 }
726
727 #[test]
728 fn normalize_false_values_are_ignored() {
729 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
730 email_col.unique = Some(StrOrBoolOrArray::Bool(false));
731 email_col.index = Some(StrOrBoolOrArray::Bool(false));
732
733 let table = TableDef {
734 name: "users".into(),
735 columns: vec![
736 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
737 email_col,
738 ],
739 constraints: vec![],
740 indexes: vec![],
741 };
742
743 let normalized = table.normalize().unwrap();
744 assert_eq!(normalized.constraints.len(), 0);
745 assert_eq!(normalized.indexes.len(), 0);
746 }
747
748 #[test]
749 fn normalize_multiple_indexes_from_same_array() {
750 let mut updated_at_col = col(
752 "updated_at",
753 ColumnType::Simple(SimpleColumnType::Timestamp),
754 );
755 updated_at_col.index = Some(StrOrBoolOrArray::Array(vec![
756 "tuple".into(),
757 "tuple2".into(),
758 ]));
759
760 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
761 user_id_col.index = Some(StrOrBoolOrArray::Array(vec![
762 "tuple".into(),
763 "tuple2".into(),
764 ]));
765
766 let table = TableDef {
767 name: "post".into(),
768 columns: vec![
769 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
770 updated_at_col,
771 user_id_col,
772 ],
773 constraints: vec![],
774 indexes: vec![],
775 };
776
777 let normalized = table.normalize().unwrap();
778 assert_eq!(normalized.indexes.len(), 2);
780
781 let tuple_idx = normalized
782 .indexes
783 .iter()
784 .find(|i| i.name == "tuple")
785 .unwrap();
786 let mut sorted_cols = tuple_idx.columns.clone();
787 sorted_cols.sort();
788 assert_eq!(
789 sorted_cols,
790 vec!["updated_at".to_string(), "user_id".to_string()]
791 );
792
793 let tuple2_idx = normalized
794 .indexes
795 .iter()
796 .find(|i| i.name == "tuple2")
797 .unwrap();
798 let mut sorted_cols2 = tuple2_idx.columns.clone();
799 sorted_cols2.sort();
800 assert_eq!(
801 sorted_cols2,
802 vec!["updated_at".to_string(), "user_id".to_string()]
803 );
804 }
805
806 #[test]
807 fn normalize_inline_unique_with_array_existing_constraint() {
808 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
810 col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
811
812 let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text));
813 col2.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
814
815 let table = TableDef {
816 name: "test".into(),
817 columns: vec![
818 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
819 col1,
820 col2,
821 ],
822 constraints: vec![],
823 indexes: vec![],
824 };
825
826 let normalized = table.normalize().unwrap();
827 assert_eq!(normalized.constraints.len(), 1);
828 let unique_constraint = &normalized.constraints[0];
829 assert!(matches!(
830 unique_constraint,
831 TableConstraint::Unique { name: Some(n), columns: _ }
832 if n == "uq_group"
833 ));
834 if let TableConstraint::Unique { columns, .. } = unique_constraint {
835 let mut sorted_cols = columns.clone();
836 sorted_cols.sort();
837 assert_eq!(sorted_cols, vec!["col1".to_string(), "col2".to_string()]);
838 }
839 }
840
841 #[test]
842 fn normalize_inline_unique_with_array_column_already_in_constraint() {
843 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
845 col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
846
847 let table = TableDef {
848 name: "test".into(),
849 columns: vec![
850 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
851 col1.clone(),
852 ],
853 constraints: vec![],
854 indexes: vec![],
855 };
856
857 let normalized1 = table.normalize().unwrap();
858 assert_eq!(normalized1.constraints.len(), 1);
859
860 let table2 = TableDef {
862 name: "test".into(),
863 columns: vec![
864 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
865 col1,
866 ],
867 constraints: normalized1.constraints.clone(),
868 indexes: vec![],
869 };
870
871 let normalized2 = table2.normalize().unwrap();
872 assert_eq!(normalized2.constraints.len(), 1);
873 if let TableConstraint::Unique { columns, .. } = &normalized2.constraints[0] {
874 assert_eq!(columns.len(), 1);
875 assert_eq!(columns[0], "col1");
876 }
877 }
878
879 #[test]
880 fn normalize_inline_unique_str_already_exists() {
881 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
883 email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
884
885 let table = TableDef {
886 name: "users".into(),
887 columns: vec![
888 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
889 email_col,
890 ],
891 constraints: vec![TableConstraint::Unique {
892 name: Some("uq_email".into()),
893 columns: vec!["email".into()],
894 }],
895 indexes: vec![],
896 };
897
898 let normalized = table.normalize().unwrap();
899 let unique_constraints: Vec<_> = normalized
901 .constraints
902 .iter()
903 .filter(|c| matches!(c, TableConstraint::Unique { .. }))
904 .collect();
905 assert_eq!(unique_constraints.len(), 1);
906 }
907
908 #[test]
909 fn normalize_inline_unique_bool_already_exists() {
910 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
912 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
913
914 let table = TableDef {
915 name: "users".into(),
916 columns: vec![
917 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
918 email_col,
919 ],
920 constraints: vec![TableConstraint::Unique {
921 name: None,
922 columns: vec!["email".into()],
923 }],
924 indexes: vec![],
925 };
926
927 let normalized = table.normalize().unwrap();
928 let unique_constraints: Vec<_> = normalized
930 .constraints
931 .iter()
932 .filter(|c| matches!(c, TableConstraint::Unique { .. }))
933 .collect();
934 assert_eq!(unique_constraints.len(), 1);
935 }
936
937 #[test]
938 fn normalize_inline_foreign_key_already_exists() {
939 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
941 user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
942 ref_table: "users".into(),
943 ref_columns: vec!["id".into()],
944 on_delete: None,
945 on_update: None,
946 }));
947
948 let table = TableDef {
949 name: "posts".into(),
950 columns: vec![
951 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
952 user_id_col,
953 ],
954 constraints: vec![TableConstraint::ForeignKey {
955 name: None,
956 columns: vec!["user_id".into()],
957 ref_table: "users".into(),
958 ref_columns: vec!["id".into()],
959 on_delete: None,
960 on_update: None,
961 }],
962 indexes: vec![],
963 };
964
965 let normalized = table.normalize().unwrap();
966 let fk_constraints: Vec<_> = normalized
968 .constraints
969 .iter()
970 .filter(|c| matches!(c, TableConstraint::ForeignKey { .. }))
971 .collect();
972 assert_eq!(fk_constraints.len(), 1);
973 }
974
975 #[test]
976 fn normalize_duplicate_index_same_column_str() {
977 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
980 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
981
982 let table = TableDef {
983 name: "test".into(),
984 columns: vec![
985 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
986 col1.clone(),
987 {
988 let mut c = col1.clone();
990 c.index = Some(StrOrBoolOrArray::Str("idx1".into()));
991 c
992 },
993 ],
994 constraints: vec![],
995 indexes: vec![],
996 };
997
998 let result = table.normalize();
999 assert!(result.is_err());
1000 if let Err(TableValidationError::DuplicateIndexColumn {
1001 index_name,
1002 column_name,
1003 }) = result
1004 {
1005 assert_eq!(index_name, "idx1");
1006 assert_eq!(column_name, "col1");
1007 } else {
1008 panic!("Expected DuplicateIndexColumn error");
1009 }
1010 }
1011
1012 #[test]
1013 fn normalize_duplicate_index_same_column_array() {
1014 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1016 col1.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into(), "idx1".into()]));
1017
1018 let table = TableDef {
1019 name: "test".into(),
1020 columns: vec![
1021 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1022 col1,
1023 ],
1024 constraints: vec![],
1025 indexes: vec![],
1026 };
1027
1028 let result = table.normalize();
1029 assert!(result.is_err());
1030 if let Err(TableValidationError::DuplicateIndexColumn {
1031 index_name,
1032 column_name,
1033 }) = result
1034 {
1035 assert_eq!(index_name, "idx1");
1036 assert_eq!(column_name, "col1");
1037 } else {
1038 panic!("Expected DuplicateIndexColumn error");
1039 }
1040 }
1041
1042 #[test]
1043 fn normalize_duplicate_index_same_column_multiple_definitions() {
1044 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1046 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1047
1048 let table = TableDef {
1049 name: "test".into(),
1050 columns: vec![
1051 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1052 col1.clone(),
1053 {
1054 let mut c = col1.clone();
1055 c.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into()]));
1056 c
1057 },
1058 ],
1059 constraints: vec![],
1060 indexes: vec![],
1061 };
1062
1063 let result = table.normalize();
1064 assert!(result.is_err());
1065 if let Err(TableValidationError::DuplicateIndexColumn {
1066 index_name,
1067 column_name,
1068 }) = result
1069 {
1070 assert_eq!(index_name, "idx1");
1071 assert_eq!(column_name, "col1");
1072 } else {
1073 panic!("Expected DuplicateIndexColumn error");
1074 }
1075 }
1076
1077 #[test]
1078 fn test_table_validation_error_display() {
1079 let error = TableValidationError::DuplicateIndexColumn {
1080 index_name: "idx_test".into(),
1081 column_name: "col1".into(),
1082 };
1083 let error_msg = format!("{}", error);
1084 assert!(error_msg.contains("idx_test"));
1085 assert!(error_msg.contains("col1"));
1086 assert!(error_msg.contains("Duplicate index"));
1087 }
1088
1089 #[test]
1090 fn normalize_inline_unique_str_with_different_constraint_type() {
1091 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1093 email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
1094
1095 let table = TableDef {
1096 name: "users".into(),
1097 columns: vec![
1098 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1099 email_col,
1100 ],
1101 constraints: vec![
1102 TableConstraint::PrimaryKey {
1104 auto_increment: false,
1105 columns: vec!["id".into()],
1106 },
1107 ],
1108 indexes: vec![],
1109 };
1110
1111 let normalized = table.normalize().unwrap();
1112 assert_eq!(normalized.constraints.len(), 2);
1114 }
1115
1116 #[test]
1117 fn normalize_inline_unique_array_with_different_constraint_type() {
1118 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1120 col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
1121
1122 let table = TableDef {
1123 name: "test".into(),
1124 columns: vec![
1125 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1126 col1,
1127 ],
1128 constraints: vec![
1129 TableConstraint::PrimaryKey {
1131 auto_increment: false,
1132 columns: vec!["id".into()],
1133 },
1134 ],
1135 indexes: vec![],
1136 };
1137
1138 let normalized = table.normalize().unwrap();
1139 assert_eq!(normalized.constraints.len(), 2);
1141 }
1142
1143 #[test]
1144 fn normalize_duplicate_index_bool_true_same_column() {
1145 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1147 col1.index = Some(StrOrBoolOrArray::Bool(true));
1148
1149 let table = TableDef {
1150 name: "test".into(),
1151 columns: vec![
1152 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1153 col1.clone(),
1154 {
1155 let mut c = col1.clone();
1157 c.index = Some(StrOrBoolOrArray::Bool(true));
1158 c
1159 },
1160 ],
1161 constraints: vec![],
1162 indexes: vec![],
1163 };
1164
1165 let result = table.normalize();
1166 assert!(result.is_err());
1167 if let Err(TableValidationError::DuplicateIndexColumn {
1168 index_name,
1169 column_name,
1170 }) = result
1171 {
1172 assert!(index_name.contains("idx_test"));
1173 assert!(index_name.contains("col1"));
1174 assert_eq!(column_name, "col1");
1175 } else {
1176 panic!("Expected DuplicateIndexColumn error");
1177 }
1178 }
1179
1180 #[test]
1181 fn normalize_inline_foreign_key_string_syntax() {
1182 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1184 user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.id".into()));
1185
1186 let table = TableDef {
1187 name: "posts".into(),
1188 columns: vec![
1189 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1190 user_id_col,
1191 ],
1192 constraints: vec![],
1193 indexes: vec![],
1194 };
1195
1196 let normalized = table.normalize().unwrap();
1197 assert_eq!(normalized.constraints.len(), 1);
1198 assert!(matches!(
1199 &normalized.constraints[0],
1200 TableConstraint::ForeignKey {
1201 name: None,
1202 columns,
1203 ref_table,
1204 ref_columns,
1205 on_delete: None,
1206 on_update: None,
1207 } if columns == &["user_id".to_string()]
1208 && ref_table == "users"
1209 && ref_columns == &["id".to_string()]
1210 ));
1211 }
1212
1213 #[test]
1214 fn normalize_inline_foreign_key_invalid_format_no_dot() {
1215 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1217 user_id_col.foreign_key = Some(ForeignKeySyntax::String("usersid".into()));
1218
1219 let table = TableDef {
1220 name: "posts".into(),
1221 columns: vec![
1222 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1223 user_id_col,
1224 ],
1225 constraints: vec![],
1226 indexes: vec![],
1227 };
1228
1229 let result = table.normalize();
1230 assert!(result.is_err());
1231 if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1232 assert_eq!(column_name, "user_id");
1233 assert_eq!(value, "usersid");
1234 } else {
1235 panic!("Expected InvalidForeignKeyFormat error");
1236 }
1237 }
1238
1239 #[test]
1240 fn normalize_inline_foreign_key_invalid_format_empty_table() {
1241 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1243 user_id_col.foreign_key = Some(ForeignKeySyntax::String(".id".into()));
1244
1245 let table = TableDef {
1246 name: "posts".into(),
1247 columns: vec![
1248 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1249 user_id_col,
1250 ],
1251 constraints: vec![],
1252 indexes: vec![],
1253 };
1254
1255 let result = table.normalize();
1256 assert!(result.is_err());
1257 if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1258 assert_eq!(column_name, "user_id");
1259 assert_eq!(value, ".id");
1260 } else {
1261 panic!("Expected InvalidForeignKeyFormat error");
1262 }
1263 }
1264
1265 #[test]
1266 fn normalize_inline_foreign_key_invalid_format_empty_column() {
1267 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1269 user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.".into()));
1270
1271 let table = TableDef {
1272 name: "posts".into(),
1273 columns: vec![
1274 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1275 user_id_col,
1276 ],
1277 constraints: vec![],
1278 indexes: vec![],
1279 };
1280
1281 let result = table.normalize();
1282 assert!(result.is_err());
1283 if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1284 assert_eq!(column_name, "user_id");
1285 assert_eq!(value, "users.");
1286 } else {
1287 panic!("Expected InvalidForeignKeyFormat error");
1288 }
1289 }
1290
1291 #[test]
1292 fn normalize_inline_foreign_key_invalid_format_too_many_parts() {
1293 let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1295 user_id_col.foreign_key = Some(ForeignKeySyntax::String("schema.users.id".into()));
1296
1297 let table = TableDef {
1298 name: "posts".into(),
1299 columns: vec![
1300 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1301 user_id_col,
1302 ],
1303 constraints: vec![],
1304 indexes: vec![],
1305 };
1306
1307 let result = table.normalize();
1308 assert!(result.is_err());
1309 if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1310 assert_eq!(column_name, "user_id");
1311 assert_eq!(value, "schema.users.id");
1312 } else {
1313 panic!("Expected InvalidForeignKeyFormat error");
1314 }
1315 }
1316
1317 #[test]
1318 fn normalize_inline_primary_key_with_auto_increment() {
1319 use crate::schema::primary_key::PrimaryKeyDef;
1320
1321 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
1322 id_col.primary_key = Some(PrimaryKeySyntax::Object(PrimaryKeyDef {
1323 auto_increment: true,
1324 columns: vec![], }));
1326
1327 let table = TableDef {
1328 name: "users".into(),
1329 columns: vec![
1330 id_col,
1331 col("name", ColumnType::Simple(SimpleColumnType::Text)),
1332 ],
1333 constraints: vec![],
1334 indexes: vec![],
1335 };
1336
1337 let normalized = table.normalize().unwrap();
1338 assert_eq!(normalized.constraints.len(), 1);
1339 assert!(matches!(
1340 &normalized.constraints[0],
1341 TableConstraint::PrimaryKey { auto_increment: true, columns } if columns == &["id".to_string()]
1342 ));
1343 }
1344
1345 #[test]
1346 fn test_invalid_foreign_key_format_error_display() {
1347 let error = TableValidationError::InvalidForeignKeyFormat {
1348 column_name: "user_id".into(),
1349 value: "invalid".into(),
1350 };
1351 let error_msg = format!("{}", error);
1352 assert!(error_msg.contains("user_id"));
1353 assert!(error_msg.contains("invalid"));
1354 assert!(error_msg.contains("table.column"));
1355 }
1356}