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