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