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