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