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, 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    /// Normalizes inline column constraints (primary_key, unique, index, foreign_key)
45    /// into table-level constraints and indexes.
46    /// Returns a new TableDef with all inline constraints converted to table-level.
47    /// 
48    /// # Errors
49    /// 
50    /// Returns an error if the same index name is applied to the same column multiple times.
51    pub fn normalize(&self) -> Result<Self, TableValidationError> {
52        let mut constraints = self.constraints.clone();
53        let mut indexes = self.indexes.clone();
54
55        // Collect columns with inline primary_key
56        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        // Add primary key constraint if any columns have inline pk and no existing pk constraint
64        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        // Process inline unique and index for each column
76        for col in &self.columns {
77            // Handle inline unique
78            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                        // Check if this unique constraint already exists
84                        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                        // Array format: each element is a constraint name
118                        // This column will be part of all these named constraints
119                        for constraint_name in names {
120                            // Check if constraint with this name already exists
121                            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                                // Add this column to existing composite constraint
129                                if let TableConstraint::Unique { columns, .. } = existing && !columns.contains(&col.name) {
130                                        columns.push(col.name.clone());
131                                }
132                            } else {
133                                // Create new constraint with this column
134                                constraints.push(TableConstraint::Unique {
135                                    name: Some(constraint_name.clone()),
136                                    columns: vec![col.name.clone()],
137                                });
138                            }
139                        }
140                    }
141                }
142            }
143
144            // Handle inline foreign_key
145            if let Some(ref fk) = col.foreign_key {
146                // Check if this foreign key already exists
147                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        // Group columns by index name to create composite indexes
169        // Use a HashMap to group, but preserve column order by tracking first occurrence
170        let mut index_groups: HashMap<String, Vec<String>> = HashMap::new();
171        let mut index_order: Vec<String> = Vec::new(); // Preserve order of first occurrence
172        // Track which columns are already in each index from inline definitions to detect duplicates
173        // Only track inline definitions, not existing table-level indexes (they can be extended)
174        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                        // Named index - group by name
181                        let index_name = name.clone();
182                        
183                        // Check for duplicate - only check inline definitions, not existing table-level indexes
184                        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                        // Auto-generated index name
207                        let index_name = format!("idx_{}_{}", self.name, col.name);
208                        
209                        // Check for duplicate (auto-generated names are unique per column, so this shouldn't happen)
210                        // But we check anyway for consistency - only check inline definitions
211                        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                        // Array format: each element is an index name
235                        // This column will be part of all these named indexes
236                        // Check for duplicates within the array
237                        let mut seen_in_array = HashSet::new();
238                        for index_name in names {
239                        // Check for duplicate within the same array
240                        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                        // Check for duplicate across different inline definitions
249                        // Only check inline definitions, not existing table-level indexes
250                        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        // Create indexes from grouped columns in order
277        for index_name in index_order {
278            let columns = index_groups.get(&index_name).unwrap().clone();
279
280            // Check if this index already exists (by name only, not by column match)
281            // Multiple indexes can have the same columns but different names
282            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        // Should have: PrimaryKey, Unique, ForeignKey
526        assert_eq!(normalized.constraints.len(), 3);
527        // Should have: 1 index
528        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        // Check idx_a composite index
581        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        // Check idx_b single column index
589        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        // Check auto-generated index for col4
597        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        // Multiple columns with same array of index names should create multiple composite indexes
626        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        // Should have: tuple (composite: updated_at, user_id), tuple2 (composite: updated_at, user_id)
641        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        // Test Array format where constraint already exists - should add column to existing
657        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        // Test Array format where column is already in constraint - should not duplicate
688        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        // Add same column again - should not create duplicate
702        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        // Test that existing unique constraint with same name and column is not duplicated
720        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        // Should not duplicate the constraint
735        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        // Test that existing unnamed unique constraint with same column is not duplicated
746        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        // Should not duplicate the constraint
761        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        // Test that existing foreign key constraint is not duplicated
772        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        // Should not duplicate the foreign key
796        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        // Same index name applied to the same column multiple times should error
807        // This tests inline index duplicate, not table-level index
808        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                    // Same column with same index name again
818                    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        // Same index name in array applied to the same column should error
840        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        // Same index name applied to the same column in different ways should error
863        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        // Test that other constraint types don't match in the exists check
906        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                // Add a PrimaryKey constraint (different type) - should not match
914                TableConstraint::PrimaryKey {
915                    columns: vec!["id".into()],
916                },
917            ],
918            indexes: vec![],
919        };
920
921        let normalized = table.normalize().unwrap();
922        // Should have: PrimaryKey (existing) + Unique (new)
923        assert_eq!(normalized.constraints.len(), 2);
924    }
925
926    #[test]
927    fn normalize_inline_unique_array_with_different_constraint_type() {
928        // Test that other constraint types don't match in the exists check for Array case
929        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                // Add a PrimaryKey constraint (different type) - should not match
937                TableConstraint::PrimaryKey {
938                    columns: vec!["id".into()],
939                },
940            ],
941            indexes: vec![],
942        };
943
944        let normalized = table.normalize().unwrap();
945        // Should have: PrimaryKey (existing) + Unique (new)
946        assert_eq!(normalized.constraints.len(), 2);
947    }
948
949    #[test]
950    fn normalize_duplicate_index_bool_true_same_column() {
951        // Test that Bool(true) with duplicate on same column errors
952        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                    // Same column with Bool(true) again
962                    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}