vespertide_core/schema/
table.rs

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    /// Normalizes inline column constraints (primary_key, unique, index, foreign_key)
49    /// into table-level constraints and indexes.
50    /// Returns a new TableDef with all inline constraints converted to table-level.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the same index name is applied to the same column multiple times.
55    pub fn normalize(&self) -> Result<Self, TableValidationError> {
56        let mut constraints = self.constraints.clone();
57        let mut indexes = self.indexes.clone();
58
59        // Collect columns with inline primary_key
60        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        // Add primary key constraint if any columns have inline pk and no existing pk constraint
68        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        // Process inline unique and index for each column
80        for col in &self.columns {
81            // Handle inline unique
82            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                        // Check if this unique constraint already exists
88                        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                        // Array format: each element is a constraint name
132                        // This column will be part of all these named constraints
133                        for constraint_name in names {
134                            // Check if constraint with this name already exists
135                            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                                // Add this column to existing composite constraint
143                                if let TableConstraint::Unique { columns, .. } = existing
144                                    && !columns.contains(&col.name)
145                                {
146                                    columns.push(col.name.clone());
147                                }
148                            } else {
149                                // Create new constraint with this column
150                                constraints.push(TableConstraint::Unique {
151                                    name: Some(constraint_name.clone()),
152                                    columns: vec![col.name.clone()],
153                                });
154                            }
155                        }
156                    }
157                }
158            }
159
160            // Handle inline foreign_key
161            if let Some(ref fk) = col.foreign_key {
162                // Check if this foreign key already exists
163                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        // Group columns by index name to create composite indexes
185        // Use a HashMap to group, but preserve column order by tracking first occurrence
186        let mut index_groups: HashMap<String, Vec<String>> = HashMap::new();
187        let mut index_order: Vec<String> = Vec::new(); // Preserve order of first occurrence
188        // Track which columns are already in each index from inline definitions to detect duplicates
189        // Only track inline definitions, not existing table-level indexes (they can be extended)
190        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                        // Named index - group by name
197                        let index_name = name.clone();
198
199                        // Check for duplicate - only check inline definitions, not existing table-level indexes
200                        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                        // Auto-generated index name
225                        let index_name = format!("idx_{}_{}", self.name, col.name);
226
227                        // Check for duplicate (auto-generated names are unique per column, so this shouldn't happen)
228                        // But we check anyway for consistency - only check inline definitions
229                        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                        // Array format: each element is an index name
255                        // This column will be part of all these named indexes
256                        // Check for duplicates within the array
257                        let mut seen_in_array = HashSet::new();
258                        for index_name in names {
259                            // Check for duplicate within the same array
260                            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                            // Check for duplicate across different inline definitions
269                            // Only check inline definitions, not existing table-level indexes
270                            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        // Create indexes from grouped columns in order
300        for index_name in index_order {
301            let columns = index_groups.get(&index_name).unwrap().clone();
302
303            // Check if this index already exists (by name only, not by column match)
304            // Multiple indexes can have the same columns but different names
305            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        // Should have: PrimaryKey, Unique, ForeignKey
565        assert_eq!(normalized.constraints.len(), 3);
566        // Should have: 1 index
567        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        // Check idx_a composite index
633        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        // Check idx_b single column index
641        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        // Check auto-generated index for col4
649        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        // Multiple columns with same array of index names should create multiple composite indexes
681        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        // Should have: tuple (composite: updated_at, user_id), tuple2 (composite: updated_at, user_id)
709        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        // Test Array format where constraint already exists - should add column to existing
739        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        // Test Array format where column is already in constraint - should not duplicate
774        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        // Add same column again - should not create duplicate
791        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        // Test that existing unique constraint with same name and column is not duplicated
812        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        // Should not duplicate the constraint
830        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        // Test that existing unnamed unique constraint with same column is not duplicated
841        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        // Should not duplicate the constraint
859        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        // Test that existing foreign key constraint is not duplicated
870        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        // Should not duplicate the foreign key
897        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        // Same index name applied to the same column multiple times should error
908        // This tests inline index duplicate, not table-level index
909        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                    // Same column with same index name again
919                    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        // Same index name in array applied to the same column should error
945        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        // Same index name applied to the same column in different ways should error
975        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        // Test that other constraint types don't match in the exists check
1022        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                // Add a PrimaryKey constraint (different type) - should not match
1033                TableConstraint::PrimaryKey {
1034                    columns: vec!["id".into()],
1035                },
1036            ],
1037            indexes: vec![],
1038        };
1039
1040        let normalized = table.normalize().unwrap();
1041        // Should have: PrimaryKey (existing) + Unique (new)
1042        assert_eq!(normalized.constraints.len(), 2);
1043    }
1044
1045    #[test]
1046    fn normalize_inline_unique_array_with_different_constraint_type() {
1047        // Test that other constraint types don't match in the exists check for Array case
1048        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                // Add a PrimaryKey constraint (different type) - should not match
1059                TableConstraint::PrimaryKey {
1060                    columns: vec!["id".into()],
1061                },
1062            ],
1063            indexes: vec![],
1064        };
1065
1066        let normalized = table.normalize().unwrap();
1067        // Should have: PrimaryKey (existing) + Unique (new)
1068        assert_eq!(normalized.constraints.len(), 2);
1069    }
1070
1071    #[test]
1072    fn normalize_duplicate_index_bool_true_same_column() {
1073        // Test that Bool(true) with duplicate on same column errors
1074        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                    // Same column with Bool(true) again
1084                    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}