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,
8    foreign_key::ForeignKeySyntax, names::TableName, primary_key::PrimaryKeySyntax,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum TableValidationError {
13    DuplicateIndexColumn {
14        index_name: String,
15        column_name: String,
16    },
17    InvalidForeignKeyFormat {
18        column_name: String,
19        value: String,
20    },
21}
22
23impl std::fmt::Display for TableValidationError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            TableValidationError::DuplicateIndexColumn {
27                index_name,
28                column_name,
29            } => {
30                write!(
31                    f,
32                    "Duplicate index '{}' on column '{}': the same index name cannot be applied to the same column multiple times",
33                    index_name, column_name
34                )
35            }
36            TableValidationError::InvalidForeignKeyFormat { column_name, value } => {
37                write!(
38                    f,
39                    "Invalid foreign key format '{}' on column '{}': expected 'table.column' format",
40                    value, column_name
41                )
42            }
43        }
44    }
45}
46
47impl std::error::Error for TableValidationError {}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
50#[serde(rename_all = "snake_case")]
51pub struct TableDef {
52    pub name: TableName,
53    pub columns: Vec<ColumnDef>,
54    pub constraints: Vec<TableConstraint>,
55}
56
57impl TableDef {
58    /// Normalizes inline column constraints (primary_key, unique, index, foreign_key)
59    /// into table-level constraints.
60    /// Returns a new TableDef with all inline constraints converted to table-level.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the same index name is applied to the same column multiple times.
65    pub fn normalize(&self) -> Result<Self, TableValidationError> {
66        let mut constraints = self.constraints.clone();
67
68        // Collect columns with inline primary_key and check for auto_increment
69        let mut pk_columns: Vec<String> = Vec::new();
70        let mut pk_auto_increment = false;
71
72        for col in &self.columns {
73            if let Some(ref pk) = col.primary_key {
74                match pk {
75                    PrimaryKeySyntax::Bool(true) => {
76                        pk_columns.push(col.name.clone());
77                    }
78                    PrimaryKeySyntax::Bool(false) => {}
79                    PrimaryKeySyntax::Object(pk_def) => {
80                        pk_columns.push(col.name.clone());
81                        if pk_def.auto_increment {
82                            pk_auto_increment = true;
83                        }
84                    }
85                }
86            }
87        }
88
89        // Add primary key constraint if any columns have inline pk and no existing pk constraint.
90        if !pk_columns.is_empty() {
91            let has_pk_constraint = constraints
92                .iter()
93                .any(|c| matches!(c, TableConstraint::PrimaryKey { .. }));
94
95            if !has_pk_constraint {
96                constraints.push(TableConstraint::PrimaryKey {
97                    auto_increment: pk_auto_increment,
98                    columns: pk_columns,
99                });
100            }
101        }
102
103        // Group columns by unique constraint name to create composite unique constraints
104        // Use same pattern as index grouping
105        let mut unique_groups: HashMap<String, Vec<String>> = HashMap::new();
106        let mut unique_order: Vec<String> = Vec::new(); // Preserve order of first occurrence
107
108        for col in &self.columns {
109            if let Some(ref unique_val) = col.unique {
110                match unique_val {
111                    StrOrBoolOrArray::Str(name) => {
112                        // Named unique constraint - group by name for composite constraints
113                        let unique_name = name.clone();
114
115                        if !unique_groups.contains_key(&unique_name) {
116                            unique_order.push(unique_name.clone());
117                        }
118
119                        unique_groups
120                            .entry(unique_name)
121                            .or_default()
122                            .push(col.name.clone());
123                    }
124                    StrOrBoolOrArray::Bool(true) => {
125                        // Use special marker for auto-generated unique constraints (without custom name)
126                        let group_key = format!("__auto_{}", col.name);
127
128                        if !unique_groups.contains_key(&group_key) {
129                            unique_order.push(group_key.clone());
130                        }
131
132                        unique_groups
133                            .entry(group_key)
134                            .or_default()
135                            .push(col.name.clone());
136                    }
137                    StrOrBoolOrArray::Bool(false) => continue,
138                    StrOrBoolOrArray::Array(names) => {
139                        // Array format: each element is a constraint name
140                        // This column will be part of all these named constraints
141                        for unique_name in names {
142                            if !unique_groups.contains_key(unique_name.as_str()) {
143                                unique_order.push(unique_name.clone());
144                            }
145
146                            unique_groups
147                                .entry(unique_name.clone())
148                                .or_default()
149                                .push(col.name.clone());
150                        }
151                    }
152                }
153            }
154        }
155
156        // Create unique constraints from grouped columns in order
157        for unique_name in unique_order {
158            let columns = unique_groups.get(&unique_name).unwrap().clone();
159
160            // Determine if this is an auto-generated unique (from unique: true)
161            // or a named unique (from unique: "name")
162            let constraint_name = if unique_name.starts_with("__auto_") {
163                // Auto-generated unique - use None so SQL generation can create the name
164                None
165            } else {
166                // Named unique - preserve the custom name
167                Some(unique_name.clone())
168            };
169
170            // Check if this unique constraint already exists
171            let exists = constraints.iter().any(|c| {
172                if let TableConstraint::Unique {
173                    name,
174                    columns: cols,
175                } = c
176                {
177                    // Match by name if both have names, otherwise match by columns
178                    match (&constraint_name, name) {
179                        (Some(n1), Some(n2)) => n1 == n2,
180                        (None, None) => cols == &columns,
181                        _ => false,
182                    }
183                } else {
184                    false
185                }
186            });
187
188            if !exists {
189                constraints.push(TableConstraint::Unique {
190                    name: constraint_name,
191                    columns,
192                });
193            }
194        }
195
196        // Process inline foreign_key and index for each column
197        for col in &self.columns {
198            // Handle inline foreign_key
199            if let Some(ref fk_syntax) = col.foreign_key {
200                // Convert ForeignKeySyntax to ForeignKeyDef
201                let (ref_table, ref_columns, on_delete, on_update) = match fk_syntax {
202                    ForeignKeySyntax::String(s) => {
203                        // Parse "table.column" format
204                        let parts: Vec<&str> = s.split('.').collect();
205                        if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
206                            return Err(TableValidationError::InvalidForeignKeyFormat {
207                                column_name: col.name.clone(),
208                                value: s.clone(),
209                            });
210                        }
211                        (parts[0].to_string(), vec![parts[1].to_string()], None, None)
212                    }
213                    ForeignKeySyntax::Object(fk_def) => (
214                        fk_def.ref_table.clone(),
215                        fk_def.ref_columns.clone(),
216                        fk_def.on_delete.clone(),
217                        fk_def.on_update.clone(),
218                    ),
219                };
220
221                // Check if this foreign key already exists
222                let exists = constraints.iter().any(|c| {
223                    if let TableConstraint::ForeignKey { columns, .. } = c {
224                        columns.len() == 1 && columns[0] == col.name
225                    } else {
226                        false
227                    }
228                });
229
230                if !exists {
231                    constraints.push(TableConstraint::ForeignKey {
232                        name: None,
233                        columns: vec![col.name.clone()],
234                        ref_table,
235                        ref_columns,
236                        on_delete,
237                        on_update,
238                    });
239                }
240            }
241        }
242
243        // Group columns by index name to create composite indexes
244        // Use a HashMap to group, but preserve column order by tracking first occurrence
245        let mut index_groups: HashMap<String, Vec<String>> = HashMap::new();
246        let mut index_order: Vec<String> = Vec::new(); // Preserve order of first occurrence
247        // Track which columns are already in each index from inline definitions to detect duplicates
248        // Only track inline definitions, not existing table-level indexes (they can be extended)
249        let mut inline_index_column_tracker: HashMap<String, HashSet<String>> = HashMap::new();
250
251        for col in &self.columns {
252            if let Some(ref index_val) = col.index {
253                match index_val {
254                    StrOrBoolOrArray::Str(name) => {
255                        // Named index - group by name
256                        let index_name = name.clone();
257
258                        // Check for duplicate - only check inline definitions, not existing table-level indexes
259                        if let Some(columns) = inline_index_column_tracker.get(name.as_str())
260                            && columns.contains(col.name.as_str())
261                        {
262                            return Err(TableValidationError::DuplicateIndexColumn {
263                                index_name: name.clone(),
264                                column_name: col.name.clone(),
265                            });
266                        }
267
268                        if !index_groups.contains_key(&index_name) {
269                            index_order.push(index_name.clone());
270                        }
271
272                        index_groups
273                            .entry(index_name.clone())
274                            .or_default()
275                            .push(col.name.clone());
276
277                        inline_index_column_tracker
278                            .entry(index_name)
279                            .or_default()
280                            .insert(col.name.clone());
281                    }
282                    StrOrBoolOrArray::Bool(true) => {
283                        // Use special marker for auto-generated indexes (without custom name)
284                        // We use the column name as a unique key to group, but will use None for the constraint name
285                        // This allows SQL generation to auto-generate the name based on naming conventions
286                        let group_key = format!("__auto_{}", col.name);
287
288                        // Check for duplicate - only check inline definitions
289                        if let Some(columns) = inline_index_column_tracker.get(group_key.as_str())
290                            && columns.contains(col.name.as_str())
291                        {
292                            return Err(TableValidationError::DuplicateIndexColumn {
293                                index_name: group_key.clone(),
294                                column_name: col.name.clone(),
295                            });
296                        }
297
298                        if !index_groups.contains_key(&group_key) {
299                            index_order.push(group_key.clone());
300                        }
301
302                        index_groups
303                            .entry(group_key.clone())
304                            .or_default()
305                            .push(col.name.clone());
306
307                        inline_index_column_tracker
308                            .entry(group_key)
309                            .or_default()
310                            .insert(col.name.clone());
311                    }
312                    StrOrBoolOrArray::Bool(false) => continue,
313                    StrOrBoolOrArray::Array(names) => {
314                        // Array format: each element is an index name
315                        // This column will be part of all these named indexes
316                        // Check for duplicates within the array
317                        let mut seen_in_array = HashSet::new();
318                        for index_name in names {
319                            // Check for duplicate within the same array
320                            if seen_in_array.contains(index_name.as_str()) {
321                                return Err(TableValidationError::DuplicateIndexColumn {
322                                    index_name: index_name.clone(),
323                                    column_name: col.name.clone(),
324                                });
325                            }
326                            seen_in_array.insert(index_name.clone());
327
328                            // Check for duplicate across different inline definitions
329                            // Only check inline definitions, not existing table-level indexes
330                            if let Some(columns) =
331                                inline_index_column_tracker.get(index_name.as_str())
332                                && columns.contains(col.name.as_str())
333                            {
334                                return Err(TableValidationError::DuplicateIndexColumn {
335                                    index_name: index_name.clone(),
336                                    column_name: col.name.clone(),
337                                });
338                            }
339
340                            if !index_groups.contains_key(index_name.as_str()) {
341                                index_order.push(index_name.clone());
342                            }
343
344                            index_groups
345                                .entry(index_name.clone())
346                                .or_default()
347                                .push(col.name.clone());
348
349                            inline_index_column_tracker
350                                .entry(index_name.clone())
351                                .or_default()
352                                .insert(col.name.clone());
353                        }
354                    }
355                }
356            }
357        }
358
359        // Create indexes from grouped columns in order
360        for index_name in index_order {
361            let columns = index_groups.get(&index_name).unwrap().clone();
362
363            // Determine if this is an auto-generated index (from index: true)
364            // or a named index (from index: "name")
365            let constraint_name = if index_name.starts_with("__auto_") {
366                // Auto-generated index - use None so SQL generation can create the name
367                None
368            } else {
369                // Named index - preserve the custom name
370                Some(index_name.clone())
371            };
372
373            // Check if this index already exists
374            let exists = constraints.iter().any(|c| {
375                if let TableConstraint::Index {
376                    name,
377                    columns: cols,
378                } = c
379                {
380                    // Match by name if both have names, otherwise match by columns
381                    match (&constraint_name, name) {
382                        (Some(n1), Some(n2)) => n1 == n2,
383                        (None, None) => cols == &columns,
384                        _ => false,
385                    }
386                } else {
387                    false
388                }
389            });
390
391            if !exists {
392                constraints.push(TableConstraint::Index {
393                    name: constraint_name,
394                    columns,
395                });
396            }
397        }
398
399        Ok(TableDef {
400            name: self.name.clone(),
401            columns: self.columns.clone(),
402            constraints,
403        })
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use crate::schema::column::{ColumnType, SimpleColumnType};
411    use crate::schema::foreign_key::{ForeignKeyDef, ForeignKeySyntax};
412    use crate::schema::primary_key::PrimaryKeySyntax;
413    use crate::schema::reference::ReferenceAction;
414    use crate::schema::str_or_bool::StrOrBoolOrArray;
415
416    fn col(name: &str, ty: ColumnType) -> ColumnDef {
417        ColumnDef {
418            name: name.to_string(),
419            r#type: ty,
420            nullable: true,
421            default: None,
422            comment: None,
423            primary_key: None,
424            unique: None,
425            index: None,
426            foreign_key: None,
427        }
428    }
429
430    #[test]
431    fn normalize_inline_primary_key() {
432        let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
433        id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
434
435        let table = TableDef {
436            name: "users".into(),
437            columns: vec![
438                id_col,
439                col("name", ColumnType::Simple(SimpleColumnType::Text)),
440            ],
441            constraints: vec![],
442        };
443
444        let normalized = table.normalize().unwrap();
445        assert_eq!(normalized.constraints.len(), 1);
446        assert!(matches!(
447            &normalized.constraints[0],
448            TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string()]
449        ));
450    }
451
452    #[test]
453    fn normalize_multiple_inline_primary_keys() {
454        let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
455        id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
456
457        let mut tenant_col = col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer));
458        tenant_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
459
460        let table = TableDef {
461            name: "users".into(),
462            columns: vec![id_col, tenant_col],
463            constraints: vec![],
464        };
465
466        let normalized = table.normalize().unwrap();
467        assert_eq!(normalized.constraints.len(), 1);
468        assert!(matches!(
469            &normalized.constraints[0],
470            TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string(), "tenant_id".to_string()]
471        ));
472    }
473
474    #[test]
475    fn normalize_does_not_duplicate_existing_pk() {
476        let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
477        id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
478
479        let table = TableDef {
480            name: "users".into(),
481            columns: vec![id_col],
482            constraints: vec![TableConstraint::PrimaryKey {
483                auto_increment: false,
484                columns: vec!["id".into()],
485            }],
486        };
487
488        let normalized = table.normalize().unwrap();
489        assert_eq!(normalized.constraints.len(), 1);
490    }
491
492    #[test]
493    fn normalize_ignores_primary_key_false() {
494        let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
495        id_col.primary_key = Some(PrimaryKeySyntax::Bool(false));
496
497        let table = TableDef {
498            name: "users".into(),
499            columns: vec![
500                id_col,
501                col("name", ColumnType::Simple(SimpleColumnType::Text)),
502            ],
503            constraints: vec![],
504        };
505
506        let normalized = table.normalize().unwrap();
507        // primary_key: false should be ignored, so no primary key constraint should be added
508        assert_eq!(normalized.constraints.len(), 0);
509    }
510
511    #[test]
512    fn normalize_inline_unique_bool() {
513        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
514        email_col.unique = Some(StrOrBoolOrArray::Bool(true));
515
516        let table = TableDef {
517            name: "users".into(),
518            columns: vec![
519                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
520                email_col,
521            ],
522            constraints: vec![],
523        };
524
525        let normalized = table.normalize().unwrap();
526        assert_eq!(normalized.constraints.len(), 1);
527        assert!(matches!(
528            &normalized.constraints[0],
529            TableConstraint::Unique { name: None, columns } if columns == &["email".to_string()]
530        ));
531    }
532
533    #[test]
534    fn normalize_inline_unique_with_name() {
535        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
536        email_col.unique = Some(StrOrBoolOrArray::Str("uq_users_email".into()));
537
538        let table = TableDef {
539            name: "users".into(),
540            columns: vec![
541                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
542                email_col,
543            ],
544            constraints: vec![],
545        };
546
547        let normalized = table.normalize().unwrap();
548        assert_eq!(normalized.constraints.len(), 1);
549        assert!(matches!(
550            &normalized.constraints[0],
551            TableConstraint::Unique { name: Some(n), columns }
552                if n == "uq_users_email" && columns == &["email".to_string()]
553        ));
554    }
555
556    #[test]
557    fn normalize_composite_unique_from_string_name() {
558        // Test that multiple columns with the same unique constraint name
559        // are grouped into a single composite unique constraint
560        let mut route_col = col("join_route", ColumnType::Simple(SimpleColumnType::Text));
561        route_col.unique = Some(StrOrBoolOrArray::Str("route_provider_id".into()));
562
563        let mut provider_col = col("provider_id", ColumnType::Simple(SimpleColumnType::Text));
564        provider_col.unique = Some(StrOrBoolOrArray::Str("route_provider_id".into()));
565
566        let table = TableDef {
567            name: "user".into(),
568            columns: vec![
569                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
570                route_col,
571                provider_col,
572            ],
573            constraints: vec![],
574        };
575
576        let normalized = table.normalize().unwrap();
577        assert_eq!(normalized.constraints.len(), 1);
578        assert!(matches!(
579            &normalized.constraints[0],
580            TableConstraint::Unique { name: Some(n), columns }
581                if n == "route_provider_id"
582                    && columns == &["join_route".to_string(), "provider_id".to_string()]
583        ));
584    }
585
586    #[test]
587    fn normalize_unique_name_mismatch_creates_both_constraints() {
588        // Test coverage for line 181: When an inline unique has a name but existing doesn't (or vice versa),
589        // they should not match and both constraints should be created
590        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
591        email_col.unique = Some(StrOrBoolOrArray::Str("named_unique".into()));
592
593        let table = TableDef {
594            name: "user".into(),
595            columns: vec![
596                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
597                email_col,
598            ],
599            constraints: vec![
600                // Existing unnamed unique constraint on same column
601                TableConstraint::Unique {
602                    name: None,
603                    columns: vec!["email".into()],
604                },
605            ],
606        };
607
608        let normalized = table.normalize().unwrap();
609
610        // Should have 2 unique constraints: one named, one unnamed
611        let unique_constraints: Vec<_> = normalized
612            .constraints
613            .iter()
614            .filter(|c| matches!(c, TableConstraint::Unique { .. }))
615            .collect();
616        assert_eq!(
617            unique_constraints.len(),
618            2,
619            "Should keep both named and unnamed unique constraints as they don't match"
620        );
621
622        // Verify we have one named and one unnamed
623        let has_named = unique_constraints.iter().any(
624            |c| matches!(c, TableConstraint::Unique { name: Some(n), .. } if n == "named_unique"),
625        );
626        let has_unnamed = unique_constraints
627            .iter()
628            .any(|c| matches!(c, TableConstraint::Unique { name: None, .. }));
629
630        assert!(has_named, "Should have named unique constraint");
631        assert!(has_unnamed, "Should have unnamed unique constraint");
632    }
633
634    #[test]
635    fn normalize_inline_index_bool() {
636        let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
637        name_col.index = Some(StrOrBoolOrArray::Bool(true));
638
639        let table = TableDef {
640            name: "users".into(),
641            columns: vec![
642                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
643                name_col,
644            ],
645            constraints: vec![],
646        };
647
648        let normalized = table.normalize().unwrap();
649        // Count Index constraints
650        let indexes: Vec<_> = normalized
651            .constraints
652            .iter()
653            .filter(|c| matches!(c, TableConstraint::Index { .. }))
654            .collect();
655        assert_eq!(indexes.len(), 1);
656        // Auto-generated indexes (from index: true) should have name: None
657        // SQL generation will create the actual name based on naming conventions
658        assert!(matches!(
659            indexes[0],
660            TableConstraint::Index { name: None, columns }
661                if columns == &["name".to_string()]
662        ));
663    }
664
665    #[test]
666    fn normalize_inline_index_with_name() {
667        let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
668        name_col.index = Some(StrOrBoolOrArray::Str("custom_idx_name".into()));
669
670        let table = TableDef {
671            name: "users".into(),
672            columns: vec![
673                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
674                name_col,
675            ],
676            constraints: vec![],
677        };
678
679        let normalized = table.normalize().unwrap();
680        let indexes: Vec<_> = normalized
681            .constraints
682            .iter()
683            .filter(|c| matches!(c, TableConstraint::Index { .. }))
684            .collect();
685        assert_eq!(indexes.len(), 1);
686        assert!(matches!(
687            indexes[0],
688            TableConstraint::Index { name: Some(n), .. }
689                if n == "custom_idx_name"
690        ));
691    }
692
693    #[test]
694    fn normalize_inline_foreign_key() {
695        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
696        user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
697            ref_table: "users".into(),
698            ref_columns: vec!["id".into()],
699            on_delete: Some(ReferenceAction::Cascade),
700            on_update: None,
701        }));
702
703        let table = TableDef {
704            name: "posts".into(),
705            columns: vec![
706                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
707                user_id_col,
708            ],
709            constraints: vec![],
710        };
711
712        let normalized = table.normalize().unwrap();
713        assert_eq!(normalized.constraints.len(), 1);
714        assert!(matches!(
715            &normalized.constraints[0],
716            TableConstraint::ForeignKey {
717                name: None,
718                columns,
719                ref_table,
720                ref_columns,
721                on_delete: Some(ReferenceAction::Cascade),
722                on_update: None,
723            } if columns == &["user_id".to_string()]
724                && ref_table == "users"
725                && ref_columns == &["id".to_string()]
726        ));
727    }
728
729    #[test]
730    fn normalize_all_inline_constraints() {
731        let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
732        id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
733
734        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
735        email_col.unique = Some(StrOrBoolOrArray::Bool(true));
736
737        let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
738        name_col.index = Some(StrOrBoolOrArray::Bool(true));
739
740        let mut user_id_col = col("org_id", ColumnType::Simple(SimpleColumnType::Integer));
741        user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
742            ref_table: "orgs".into(),
743            ref_columns: vec!["id".into()],
744            on_delete: None,
745            on_update: None,
746        }));
747
748        let table = TableDef {
749            name: "users".into(),
750            columns: vec![id_col, email_col, name_col, user_id_col],
751            constraints: vec![],
752        };
753
754        let normalized = table.normalize().unwrap();
755        // Should have: PrimaryKey, Unique, ForeignKey, Index
756        // Count non-Index constraints
757        let non_index_constraints: Vec<_> = normalized
758            .constraints
759            .iter()
760            .filter(|c| !matches!(c, TableConstraint::Index { .. }))
761            .collect();
762        assert_eq!(non_index_constraints.len(), 3);
763        // Should have: 1 index
764        let indexes: Vec<_> = normalized
765            .constraints
766            .iter()
767            .filter(|c| matches!(c, TableConstraint::Index { .. }))
768            .collect();
769        assert_eq!(indexes.len(), 1);
770    }
771
772    #[test]
773    fn normalize_composite_index_from_string_name() {
774        let mut updated_at_col = col(
775            "updated_at",
776            ColumnType::Simple(SimpleColumnType::Timestamp),
777        );
778        updated_at_col.index = Some(StrOrBoolOrArray::Str("tuple".into()));
779
780        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
781        user_id_col.index = Some(StrOrBoolOrArray::Str("tuple".into()));
782
783        let table = TableDef {
784            name: "post".into(),
785            columns: vec![
786                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
787                updated_at_col,
788                user_id_col,
789            ],
790            constraints: vec![],
791        };
792
793        let normalized = table.normalize().unwrap();
794        let indexes: Vec<_> = normalized
795            .constraints
796            .iter()
797            .filter_map(|c| {
798                if let TableConstraint::Index { name, columns } = c {
799                    Some((name.clone(), columns.clone()))
800                } else {
801                    None
802                }
803            })
804            .collect();
805        assert_eq!(indexes.len(), 1);
806        assert_eq!(indexes[0].0, Some("tuple".to_string()));
807        assert_eq!(
808            indexes[0].1,
809            vec!["updated_at".to_string(), "user_id".to_string()]
810        );
811    }
812
813    #[test]
814    fn normalize_multiple_different_indexes() {
815        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
816        col1.index = Some(StrOrBoolOrArray::Str("idx_a".into()));
817
818        let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text));
819        col2.index = Some(StrOrBoolOrArray::Str("idx_a".into()));
820
821        let mut col3 = col("col3", ColumnType::Simple(SimpleColumnType::Text));
822        col3.index = Some(StrOrBoolOrArray::Str("idx_b".into()));
823
824        let mut col4 = col("col4", ColumnType::Simple(SimpleColumnType::Text));
825        col4.index = Some(StrOrBoolOrArray::Bool(true));
826
827        let table = TableDef {
828            name: "test".into(),
829            columns: vec![
830                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
831                col1,
832                col2,
833                col3,
834                col4,
835            ],
836            constraints: vec![],
837        };
838
839        let normalized = table.normalize().unwrap();
840        let indexes: Vec<_> = normalized
841            .constraints
842            .iter()
843            .filter_map(|c| {
844                if let TableConstraint::Index { name, columns } = c {
845                    Some((name.clone(), columns.clone()))
846                } else {
847                    None
848                }
849            })
850            .collect();
851        assert_eq!(indexes.len(), 3);
852
853        // Check idx_a composite index
854        let idx_a = indexes
855            .iter()
856            .find(|(n, _)| n == &Some("idx_a".to_string()))
857            .unwrap();
858        assert_eq!(idx_a.1, vec!["col1".to_string(), "col2".to_string()]);
859
860        // Check idx_b single column index
861        let idx_b = indexes
862            .iter()
863            .find(|(n, _)| n == &Some("idx_b".to_string()))
864            .unwrap();
865        assert_eq!(idx_b.1, vec!["col3".to_string()]);
866
867        // Check auto-generated index for col4 (should have name: None)
868        let idx_col4 = indexes.iter().find(|(n, _)| n.is_none()).unwrap();
869        assert_eq!(idx_col4.1, vec!["col4".to_string()]);
870    }
871
872    #[test]
873    fn normalize_false_values_are_ignored() {
874        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
875        email_col.unique = Some(StrOrBoolOrArray::Bool(false));
876        email_col.index = Some(StrOrBoolOrArray::Bool(false));
877
878        let table = TableDef {
879            name: "users".into(),
880            columns: vec![
881                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
882                email_col,
883            ],
884            constraints: vec![],
885        };
886
887        let normalized = table.normalize().unwrap();
888        assert_eq!(normalized.constraints.len(), 0);
889    }
890
891    #[test]
892    fn normalize_table_without_primary_key() {
893        // Test normalize with a table that has no primary key columns
894        // This should cover lines 67-69, 72-73, and 93 (pk_columns.is_empty() branch)
895        let table = TableDef {
896            name: "users".into(),
897            columns: vec![
898                col("name", ColumnType::Simple(SimpleColumnType::Text)),
899                col("email", ColumnType::Simple(SimpleColumnType::Text)),
900            ],
901            constraints: vec![],
902        };
903
904        let normalized = table.normalize().unwrap();
905        // Should not add any primary key constraint
906        assert_eq!(normalized.constraints.len(), 0);
907    }
908
909    #[test]
910    fn normalize_multiple_indexes_from_same_array() {
911        // Multiple columns with same array of index names should create multiple composite indexes
912        let mut updated_at_col = col(
913            "updated_at",
914            ColumnType::Simple(SimpleColumnType::Timestamp),
915        );
916        updated_at_col.index = Some(StrOrBoolOrArray::Array(vec![
917            "tuple".into(),
918            "tuple2".into(),
919        ]));
920
921        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
922        user_id_col.index = Some(StrOrBoolOrArray::Array(vec![
923            "tuple".into(),
924            "tuple2".into(),
925        ]));
926
927        let table = TableDef {
928            name: "post".into(),
929            columns: vec![
930                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
931                updated_at_col,
932                user_id_col,
933            ],
934            constraints: vec![],
935        };
936
937        let normalized = table.normalize().unwrap();
938        // Should have: tuple (composite: updated_at, user_id), tuple2 (composite: updated_at, user_id)
939        let indexes: Vec<_> = normalized
940            .constraints
941            .iter()
942            .filter_map(|c| {
943                if let TableConstraint::Index { name, columns } = c {
944                    Some((name.clone(), columns.clone()))
945                } else {
946                    None
947                }
948            })
949            .collect();
950        assert_eq!(indexes.len(), 2);
951
952        let tuple_idx = indexes
953            .iter()
954            .find(|(n, _)| n == &Some("tuple".to_string()))
955            .unwrap();
956        let mut sorted_cols = tuple_idx.1.clone();
957        sorted_cols.sort();
958        assert_eq!(
959            sorted_cols,
960            vec!["updated_at".to_string(), "user_id".to_string()]
961        );
962
963        let tuple2_idx = indexes
964            .iter()
965            .find(|(n, _)| n == &Some("tuple2".to_string()))
966            .unwrap();
967        let mut sorted_cols2 = tuple2_idx.1.clone();
968        sorted_cols2.sort();
969        assert_eq!(
970            sorted_cols2,
971            vec!["updated_at".to_string(), "user_id".to_string()]
972        );
973    }
974
975    #[test]
976    fn normalize_inline_unique_with_array_existing_constraint() {
977        // Test Array format where constraint already exists - should add column to existing
978        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
979        col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
980
981        let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text));
982        col2.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
983
984        let table = TableDef {
985            name: "test".into(),
986            columns: vec![
987                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
988                col1,
989                col2,
990            ],
991            constraints: vec![],
992        };
993
994        let normalized = table.normalize().unwrap();
995        assert_eq!(normalized.constraints.len(), 1);
996        let unique_constraint = &normalized.constraints[0];
997        assert!(matches!(
998            unique_constraint,
999            TableConstraint::Unique { name: Some(n), columns: _ }
1000                if n == "uq_group"
1001        ));
1002        if let TableConstraint::Unique { columns, .. } = unique_constraint {
1003            let mut sorted_cols = columns.clone();
1004            sorted_cols.sort();
1005            assert_eq!(sorted_cols, vec!["col1".to_string(), "col2".to_string()]);
1006        }
1007    }
1008
1009    #[test]
1010    fn normalize_inline_unique_with_array_column_already_in_constraint() {
1011        // Test Array format where column is already in constraint - should not duplicate
1012        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1013        col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
1014
1015        let table = TableDef {
1016            name: "test".into(),
1017            columns: vec![
1018                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1019                col1.clone(),
1020            ],
1021            constraints: vec![],
1022        };
1023
1024        let normalized1 = table.normalize().unwrap();
1025        assert_eq!(normalized1.constraints.len(), 1);
1026
1027        // Add same column again - should not create duplicate
1028        let table2 = TableDef {
1029            name: "test".into(),
1030            columns: vec![
1031                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1032                col1,
1033            ],
1034            constraints: normalized1.constraints.clone(),
1035        };
1036
1037        let normalized2 = table2.normalize().unwrap();
1038        assert_eq!(normalized2.constraints.len(), 1);
1039        if let TableConstraint::Unique { columns, .. } = &normalized2.constraints[0] {
1040            assert_eq!(columns.len(), 1);
1041            assert_eq!(columns[0], "col1");
1042        }
1043    }
1044
1045    #[test]
1046    fn normalize_inline_unique_str_already_exists() {
1047        // Test that existing unique constraint with same name and column is not duplicated
1048        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1049        email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
1050
1051        let table = TableDef {
1052            name: "users".into(),
1053            columns: vec![
1054                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1055                email_col,
1056            ],
1057            constraints: vec![TableConstraint::Unique {
1058                name: Some("uq_email".into()),
1059                columns: vec!["email".into()],
1060            }],
1061        };
1062
1063        let normalized = table.normalize().unwrap();
1064        // Should not duplicate the constraint
1065        let unique_constraints: Vec<_> = normalized
1066            .constraints
1067            .iter()
1068            .filter(|c| matches!(c, TableConstraint::Unique { .. }))
1069            .collect();
1070        assert_eq!(unique_constraints.len(), 1);
1071    }
1072
1073    #[test]
1074    fn normalize_inline_unique_bool_already_exists() {
1075        // Test that existing unnamed unique constraint with same column is not duplicated
1076        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1077        email_col.unique = Some(StrOrBoolOrArray::Bool(true));
1078
1079        let table = TableDef {
1080            name: "users".into(),
1081            columns: vec![
1082                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1083                email_col,
1084            ],
1085            constraints: vec![TableConstraint::Unique {
1086                name: None,
1087                columns: vec!["email".into()],
1088            }],
1089        };
1090
1091        let normalized = table.normalize().unwrap();
1092        // Should not duplicate the constraint
1093        let unique_constraints: Vec<_> = normalized
1094            .constraints
1095            .iter()
1096            .filter(|c| matches!(c, TableConstraint::Unique { .. }))
1097            .collect();
1098        assert_eq!(unique_constraints.len(), 1);
1099    }
1100
1101    #[test]
1102    fn normalize_inline_foreign_key_already_exists() {
1103        // Test that existing foreign key constraint is not duplicated
1104        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1105        user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
1106            ref_table: "users".into(),
1107            ref_columns: vec!["id".into()],
1108            on_delete: None,
1109            on_update: None,
1110        }));
1111
1112        let table = TableDef {
1113            name: "posts".into(),
1114            columns: vec![
1115                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1116                user_id_col,
1117            ],
1118            constraints: vec![TableConstraint::ForeignKey {
1119                name: None,
1120                columns: vec!["user_id".into()],
1121                ref_table: "users".into(),
1122                ref_columns: vec!["id".into()],
1123                on_delete: None,
1124                on_update: None,
1125            }],
1126        };
1127
1128        let normalized = table.normalize().unwrap();
1129        // Should not duplicate the foreign key
1130        let fk_constraints: Vec<_> = normalized
1131            .constraints
1132            .iter()
1133            .filter(|c| matches!(c, TableConstraint::ForeignKey { .. }))
1134            .collect();
1135        assert_eq!(fk_constraints.len(), 1);
1136    }
1137
1138    #[test]
1139    fn normalize_duplicate_index_same_column_str() {
1140        // Same index name applied to the same column multiple times should error
1141        // This tests inline index duplicate, not table-level index
1142        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1143        col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1144
1145        let table = TableDef {
1146            name: "test".into(),
1147            columns: vec![
1148                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1149                col1.clone(),
1150                {
1151                    // Same column with same index name again
1152                    let mut c = col1.clone();
1153                    c.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1154                    c
1155                },
1156            ],
1157            constraints: vec![],
1158        };
1159
1160        let result = table.normalize();
1161        assert!(result.is_err());
1162        if let Err(TableValidationError::DuplicateIndexColumn {
1163            index_name,
1164            column_name,
1165        }) = result
1166        {
1167            assert_eq!(index_name, "idx1");
1168            assert_eq!(column_name, "col1");
1169        } else {
1170            panic!("Expected DuplicateIndexColumn error");
1171        }
1172    }
1173
1174    #[test]
1175    fn normalize_duplicate_index_same_column_array() {
1176        // Same index name in array applied to the same column should error
1177        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1178        col1.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into(), "idx1".into()]));
1179
1180        let table = TableDef {
1181            name: "test".into(),
1182            columns: vec![
1183                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1184                col1,
1185            ],
1186            constraints: vec![],
1187        };
1188
1189        let result = table.normalize();
1190        assert!(result.is_err());
1191        if let Err(TableValidationError::DuplicateIndexColumn {
1192            index_name,
1193            column_name,
1194        }) = result
1195        {
1196            assert_eq!(index_name, "idx1");
1197            assert_eq!(column_name, "col1");
1198        } else {
1199            panic!("Expected DuplicateIndexColumn error");
1200        }
1201    }
1202
1203    #[test]
1204    fn normalize_duplicate_index_same_column_multiple_definitions() {
1205        // Same index name applied to the same column in different ways should error
1206        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1207        col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1208
1209        let table = TableDef {
1210            name: "test".into(),
1211            columns: vec![
1212                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1213                col1.clone(),
1214                {
1215                    let mut c = col1.clone();
1216                    c.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into()]));
1217                    c
1218                },
1219            ],
1220            constraints: vec![],
1221        };
1222
1223        let result = table.normalize();
1224        assert!(result.is_err());
1225        if let Err(TableValidationError::DuplicateIndexColumn {
1226            index_name,
1227            column_name,
1228        }) = result
1229        {
1230            assert_eq!(index_name, "idx1");
1231            assert_eq!(column_name, "col1");
1232        } else {
1233            panic!("Expected DuplicateIndexColumn error");
1234        }
1235    }
1236
1237    #[test]
1238    fn test_table_validation_error_display() {
1239        let error = TableValidationError::DuplicateIndexColumn {
1240            index_name: "idx_test".into(),
1241            column_name: "col1".into(),
1242        };
1243        let error_msg = format!("{}", error);
1244        assert!(error_msg.contains("idx_test"));
1245        assert!(error_msg.contains("col1"));
1246        assert!(error_msg.contains("Duplicate index"));
1247    }
1248
1249    #[test]
1250    fn normalize_inline_unique_str_with_different_constraint_type() {
1251        // Test that other constraint types don't match in the exists check
1252        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1253        email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
1254
1255        let table = TableDef {
1256            name: "users".into(),
1257            columns: vec![
1258                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1259                email_col,
1260            ],
1261            constraints: vec![
1262                // Add a PrimaryKey constraint (different type) - should not match
1263                TableConstraint::PrimaryKey {
1264                    auto_increment: false,
1265                    columns: vec!["id".into()],
1266                },
1267            ],
1268        };
1269
1270        let normalized = table.normalize().unwrap();
1271        // Should have: PrimaryKey (existing) + Unique (new)
1272        assert_eq!(normalized.constraints.len(), 2);
1273    }
1274
1275    #[test]
1276    fn normalize_inline_unique_array_with_different_constraint_type() {
1277        // Test that other constraint types don't match in the exists check for Array case
1278        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1279        col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
1280
1281        let table = TableDef {
1282            name: "test".into(),
1283            columns: vec![
1284                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1285                col1,
1286            ],
1287            constraints: vec![
1288                // Add a PrimaryKey constraint (different type) - should not match
1289                TableConstraint::PrimaryKey {
1290                    auto_increment: false,
1291                    columns: vec!["id".into()],
1292                },
1293            ],
1294        };
1295
1296        let normalized = table.normalize().unwrap();
1297        // Should have: PrimaryKey (existing) + Unique (new)
1298        assert_eq!(normalized.constraints.len(), 2);
1299    }
1300
1301    #[test]
1302    fn normalize_duplicate_index_bool_true_same_column() {
1303        // Test that Bool(true) with duplicate on same column errors
1304        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1305        col1.index = Some(StrOrBoolOrArray::Bool(true));
1306
1307        let table = TableDef {
1308            name: "test".into(),
1309            columns: vec![
1310                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1311                col1.clone(),
1312                {
1313                    // Same column with Bool(true) again
1314                    let mut c = col1.clone();
1315                    c.index = Some(StrOrBoolOrArray::Bool(true));
1316                    c
1317                },
1318            ],
1319            constraints: vec![],
1320        };
1321
1322        let result = table.normalize();
1323        assert!(result.is_err());
1324        if let Err(TableValidationError::DuplicateIndexColumn {
1325            index_name,
1326            column_name,
1327        }) = result
1328        {
1329            // The group key for auto-generated indexes is "__auto_{column}"
1330            assert!(index_name.contains("__auto_"));
1331            assert!(index_name.contains("col1"));
1332            assert_eq!(column_name, "col1");
1333        } else {
1334            panic!("Expected DuplicateIndexColumn error");
1335        }
1336    }
1337
1338    #[test]
1339    fn normalize_inline_foreign_key_string_syntax() {
1340        // Test ForeignKeySyntax::String with valid "table.column" format
1341        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1342        user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.id".into()));
1343
1344        let table = TableDef {
1345            name: "posts".into(),
1346            columns: vec![
1347                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1348                user_id_col,
1349            ],
1350            constraints: vec![],
1351        };
1352
1353        let normalized = table.normalize().unwrap();
1354        assert_eq!(normalized.constraints.len(), 1);
1355        assert!(matches!(
1356            &normalized.constraints[0],
1357            TableConstraint::ForeignKey {
1358                name: None,
1359                columns,
1360                ref_table,
1361                ref_columns,
1362                on_delete: None,
1363                on_update: None,
1364            } if columns == &["user_id".to_string()]
1365                && ref_table == "users"
1366                && ref_columns == &["id".to_string()]
1367        ));
1368    }
1369
1370    #[test]
1371    fn normalize_inline_foreign_key_invalid_format_no_dot() {
1372        // Test ForeignKeySyntax::String with invalid format (no dot)
1373        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1374        user_id_col.foreign_key = Some(ForeignKeySyntax::String("usersid".into()));
1375
1376        let table = TableDef {
1377            name: "posts".into(),
1378            columns: vec![
1379                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1380                user_id_col,
1381            ],
1382            constraints: vec![],
1383        };
1384
1385        let result = table.normalize();
1386        assert!(result.is_err());
1387        if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1388            assert_eq!(column_name, "user_id");
1389            assert_eq!(value, "usersid");
1390        } else {
1391            panic!("Expected InvalidForeignKeyFormat error");
1392        }
1393    }
1394
1395    #[test]
1396    fn normalize_inline_foreign_key_invalid_format_empty_table() {
1397        // Test ForeignKeySyntax::String with empty table part
1398        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1399        user_id_col.foreign_key = Some(ForeignKeySyntax::String(".id".into()));
1400
1401        let table = TableDef {
1402            name: "posts".into(),
1403            columns: vec![
1404                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1405                user_id_col,
1406            ],
1407            constraints: vec![],
1408        };
1409
1410        let result = table.normalize();
1411        assert!(result.is_err());
1412        if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1413            assert_eq!(column_name, "user_id");
1414            assert_eq!(value, ".id");
1415        } else {
1416            panic!("Expected InvalidForeignKeyFormat error");
1417        }
1418    }
1419
1420    #[test]
1421    fn normalize_inline_foreign_key_invalid_format_empty_column() {
1422        // Test ForeignKeySyntax::String with empty column part
1423        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1424        user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.".into()));
1425
1426        let table = TableDef {
1427            name: "posts".into(),
1428            columns: vec![
1429                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1430                user_id_col,
1431            ],
1432            constraints: vec![],
1433        };
1434
1435        let result = table.normalize();
1436        assert!(result.is_err());
1437        if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1438            assert_eq!(column_name, "user_id");
1439            assert_eq!(value, "users.");
1440        } else {
1441            panic!("Expected InvalidForeignKeyFormat error");
1442        }
1443    }
1444
1445    #[test]
1446    fn normalize_inline_foreign_key_invalid_format_too_many_parts() {
1447        // Test ForeignKeySyntax::String with too many parts
1448        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1449        user_id_col.foreign_key = Some(ForeignKeySyntax::String("schema.users.id".into()));
1450
1451        let table = TableDef {
1452            name: "posts".into(),
1453            columns: vec![
1454                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1455                user_id_col,
1456            ],
1457            constraints: vec![],
1458        };
1459
1460        let result = table.normalize();
1461        assert!(result.is_err());
1462        if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1463            assert_eq!(column_name, "user_id");
1464            assert_eq!(value, "schema.users.id");
1465        } else {
1466            panic!("Expected InvalidForeignKeyFormat error");
1467        }
1468    }
1469
1470    #[test]
1471    fn normalize_inline_primary_key_with_auto_increment() {
1472        use crate::schema::primary_key::PrimaryKeyDef;
1473
1474        let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
1475        id_col.primary_key = Some(PrimaryKeySyntax::Object(PrimaryKeyDef {
1476            auto_increment: true,
1477        }));
1478
1479        let table = TableDef {
1480            name: "users".into(),
1481            columns: vec![
1482                id_col,
1483                col("name", ColumnType::Simple(SimpleColumnType::Text)),
1484            ],
1485            constraints: vec![],
1486        };
1487
1488        let normalized = table.normalize().unwrap();
1489        assert_eq!(normalized.constraints.len(), 1);
1490        assert!(matches!(
1491            &normalized.constraints[0],
1492            TableConstraint::PrimaryKey { auto_increment: true, columns } if columns == &["id".to_string()]
1493        ));
1494    }
1495
1496    #[test]
1497    fn normalize_duplicate_inline_index_on_same_column() {
1498        // This test triggers the DuplicateIndexColumn error (lines 251-253)
1499        // by having the same column appear twice in the same named index group
1500        use crate::schema::str_or_bool::StrOrBoolOrArray;
1501
1502        // Create a column that references the same index name twice (via Array)
1503        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1504        email_col.index = Some(StrOrBoolOrArray::Array(vec![
1505            "idx_email".into(),
1506            "idx_email".into(), // Duplicate reference
1507        ]));
1508
1509        let table = TableDef {
1510            name: "users".into(),
1511            columns: vec![
1512                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1513                email_col,
1514            ],
1515            constraints: vec![],
1516        };
1517
1518        let result = table.normalize();
1519        assert!(result.is_err());
1520        if let Err(TableValidationError::DuplicateIndexColumn {
1521            index_name,
1522            column_name,
1523        }) = result
1524        {
1525            assert_eq!(index_name, "idx_email");
1526            assert_eq!(column_name, "email");
1527        } else {
1528            panic!("Expected DuplicateIndexColumn error, got: {:?}", result);
1529        }
1530    }
1531
1532    #[test]
1533    fn test_invalid_foreign_key_format_error_display() {
1534        let error = TableValidationError::InvalidForeignKeyFormat {
1535            column_name: "user_id".into(),
1536            value: "invalid".into(),
1537        };
1538        let error_msg = format!("{}", error);
1539        assert!(error_msg.contains("user_id"));
1540        assert!(error_msg.contains("invalid"));
1541        assert!(error_msg.contains("table.column"));
1542    }
1543}