Skip to main content

vespertide_core/schema/
table.rs

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