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_table_without_primary_key() {
750        // Test normalize with a table that has no primary key columns
751        // This should cover lines 67-69, 72-73, and 93 (pk_columns.is_empty() branch)
752        let table = TableDef {
753            name: "users".into(),
754            columns: vec![
755                col("name", ColumnType::Simple(SimpleColumnType::Text)),
756                col("email", ColumnType::Simple(SimpleColumnType::Text)),
757            ],
758            constraints: vec![],
759            indexes: vec![],
760        };
761
762        let normalized = table.normalize().unwrap();
763        // Should not add any primary key constraint
764        assert_eq!(normalized.constraints.len(), 0);
765        assert_eq!(normalized.indexes.len(), 0);
766    }
767
768    #[test]
769    fn normalize_multiple_indexes_from_same_array() {
770        // Multiple columns with same array of index names should create multiple composite indexes
771        let mut updated_at_col = col(
772            "updated_at",
773            ColumnType::Simple(SimpleColumnType::Timestamp),
774        );
775        updated_at_col.index = Some(StrOrBoolOrArray::Array(vec![
776            "tuple".into(),
777            "tuple2".into(),
778        ]));
779
780        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
781        user_id_col.index = Some(StrOrBoolOrArray::Array(vec![
782            "tuple".into(),
783            "tuple2".into(),
784        ]));
785
786        let table = TableDef {
787            name: "post".into(),
788            columns: vec![
789                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
790                updated_at_col,
791                user_id_col,
792            ],
793            constraints: vec![],
794            indexes: vec![],
795        };
796
797        let normalized = table.normalize().unwrap();
798        // Should have: tuple (composite: updated_at, user_id), tuple2 (composite: updated_at, user_id)
799        assert_eq!(normalized.indexes.len(), 2);
800
801        let tuple_idx = normalized
802            .indexes
803            .iter()
804            .find(|i| i.name == "tuple")
805            .unwrap();
806        let mut sorted_cols = tuple_idx.columns.clone();
807        sorted_cols.sort();
808        assert_eq!(
809            sorted_cols,
810            vec!["updated_at".to_string(), "user_id".to_string()]
811        );
812
813        let tuple2_idx = normalized
814            .indexes
815            .iter()
816            .find(|i| i.name == "tuple2")
817            .unwrap();
818        let mut sorted_cols2 = tuple2_idx.columns.clone();
819        sorted_cols2.sort();
820        assert_eq!(
821            sorted_cols2,
822            vec!["updated_at".to_string(), "user_id".to_string()]
823        );
824    }
825
826    #[test]
827    fn normalize_inline_unique_with_array_existing_constraint() {
828        // Test Array format where constraint already exists - should add column to existing
829        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
830        col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
831
832        let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text));
833        col2.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
834
835        let table = TableDef {
836            name: "test".into(),
837            columns: vec![
838                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
839                col1,
840                col2,
841            ],
842            constraints: vec![],
843            indexes: vec![],
844        };
845
846        let normalized = table.normalize().unwrap();
847        assert_eq!(normalized.constraints.len(), 1);
848        let unique_constraint = &normalized.constraints[0];
849        assert!(matches!(
850            unique_constraint,
851            TableConstraint::Unique { name: Some(n), columns: _ }
852                if n == "uq_group"
853        ));
854        if let TableConstraint::Unique { columns, .. } = unique_constraint {
855            let mut sorted_cols = columns.clone();
856            sorted_cols.sort();
857            assert_eq!(sorted_cols, vec!["col1".to_string(), "col2".to_string()]);
858        }
859    }
860
861    #[test]
862    fn normalize_inline_unique_with_array_column_already_in_constraint() {
863        // Test Array format where column is already in constraint - should not duplicate
864        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
865        col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
866
867        let table = TableDef {
868            name: "test".into(),
869            columns: vec![
870                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
871                col1.clone(),
872            ],
873            constraints: vec![],
874            indexes: vec![],
875        };
876
877        let normalized1 = table.normalize().unwrap();
878        assert_eq!(normalized1.constraints.len(), 1);
879
880        // Add same column again - should not create duplicate
881        let table2 = TableDef {
882            name: "test".into(),
883            columns: vec![
884                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
885                col1,
886            ],
887            constraints: normalized1.constraints.clone(),
888            indexes: vec![],
889        };
890
891        let normalized2 = table2.normalize().unwrap();
892        assert_eq!(normalized2.constraints.len(), 1);
893        if let TableConstraint::Unique { columns, .. } = &normalized2.constraints[0] {
894            assert_eq!(columns.len(), 1);
895            assert_eq!(columns[0], "col1");
896        }
897    }
898
899    #[test]
900    fn normalize_inline_unique_str_already_exists() {
901        // Test that existing unique constraint with same name and column is not duplicated
902        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
903        email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
904
905        let table = TableDef {
906            name: "users".into(),
907            columns: vec![
908                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
909                email_col,
910            ],
911            constraints: vec![TableConstraint::Unique {
912                name: Some("uq_email".into()),
913                columns: vec!["email".into()],
914            }],
915            indexes: vec![],
916        };
917
918        let normalized = table.normalize().unwrap();
919        // Should not duplicate the constraint
920        let unique_constraints: Vec<_> = normalized
921            .constraints
922            .iter()
923            .filter(|c| matches!(c, TableConstraint::Unique { .. }))
924            .collect();
925        assert_eq!(unique_constraints.len(), 1);
926    }
927
928    #[test]
929    fn normalize_inline_unique_bool_already_exists() {
930        // Test that existing unnamed unique constraint with same column is not duplicated
931        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
932        email_col.unique = Some(StrOrBoolOrArray::Bool(true));
933
934        let table = TableDef {
935            name: "users".into(),
936            columns: vec![
937                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
938                email_col,
939            ],
940            constraints: vec![TableConstraint::Unique {
941                name: None,
942                columns: vec!["email".into()],
943            }],
944            indexes: vec![],
945        };
946
947        let normalized = table.normalize().unwrap();
948        // Should not duplicate the constraint
949        let unique_constraints: Vec<_> = normalized
950            .constraints
951            .iter()
952            .filter(|c| matches!(c, TableConstraint::Unique { .. }))
953            .collect();
954        assert_eq!(unique_constraints.len(), 1);
955    }
956
957    #[test]
958    fn normalize_inline_foreign_key_already_exists() {
959        // Test that existing foreign key constraint is not duplicated
960        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
961        user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
962            ref_table: "users".into(),
963            ref_columns: vec!["id".into()],
964            on_delete: None,
965            on_update: None,
966        }));
967
968        let table = TableDef {
969            name: "posts".into(),
970            columns: vec![
971                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
972                user_id_col,
973            ],
974            constraints: vec![TableConstraint::ForeignKey {
975                name: None,
976                columns: vec!["user_id".into()],
977                ref_table: "users".into(),
978                ref_columns: vec!["id".into()],
979                on_delete: None,
980                on_update: None,
981            }],
982            indexes: vec![],
983        };
984
985        let normalized = table.normalize().unwrap();
986        // Should not duplicate the foreign key
987        let fk_constraints: Vec<_> = normalized
988            .constraints
989            .iter()
990            .filter(|c| matches!(c, TableConstraint::ForeignKey { .. }))
991            .collect();
992        assert_eq!(fk_constraints.len(), 1);
993    }
994
995    #[test]
996    fn normalize_duplicate_index_same_column_str() {
997        // Same index name applied to the same column multiple times should error
998        // This tests inline index duplicate, not table-level index
999        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1000        col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1001
1002        let table = TableDef {
1003            name: "test".into(),
1004            columns: vec![
1005                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1006                col1.clone(),
1007                {
1008                    // Same column with same index name again
1009                    let mut c = col1.clone();
1010                    c.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1011                    c
1012                },
1013            ],
1014            constraints: vec![],
1015            indexes: vec![],
1016        };
1017
1018        let result = table.normalize();
1019        assert!(result.is_err());
1020        if let Err(TableValidationError::DuplicateIndexColumn {
1021            index_name,
1022            column_name,
1023        }) = result
1024        {
1025            assert_eq!(index_name, "idx1");
1026            assert_eq!(column_name, "col1");
1027        } else {
1028            panic!("Expected DuplicateIndexColumn error");
1029        }
1030    }
1031
1032    #[test]
1033    fn normalize_duplicate_index_same_column_array() {
1034        // Same index name in array applied to the same column should error
1035        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1036        col1.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into(), "idx1".into()]));
1037
1038        let table = TableDef {
1039            name: "test".into(),
1040            columns: vec![
1041                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1042                col1,
1043            ],
1044            constraints: vec![],
1045            indexes: vec![],
1046        };
1047
1048        let result = table.normalize();
1049        assert!(result.is_err());
1050        if let Err(TableValidationError::DuplicateIndexColumn {
1051            index_name,
1052            column_name,
1053        }) = result
1054        {
1055            assert_eq!(index_name, "idx1");
1056            assert_eq!(column_name, "col1");
1057        } else {
1058            panic!("Expected DuplicateIndexColumn error");
1059        }
1060    }
1061
1062    #[test]
1063    fn normalize_duplicate_index_same_column_multiple_definitions() {
1064        // Same index name applied to the same column in different ways should error
1065        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1066        col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1067
1068        let table = TableDef {
1069            name: "test".into(),
1070            columns: vec![
1071                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1072                col1.clone(),
1073                {
1074                    let mut c = col1.clone();
1075                    c.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into()]));
1076                    c
1077                },
1078            ],
1079            constraints: vec![],
1080            indexes: vec![],
1081        };
1082
1083        let result = table.normalize();
1084        assert!(result.is_err());
1085        if let Err(TableValidationError::DuplicateIndexColumn {
1086            index_name,
1087            column_name,
1088        }) = result
1089        {
1090            assert_eq!(index_name, "idx1");
1091            assert_eq!(column_name, "col1");
1092        } else {
1093            panic!("Expected DuplicateIndexColumn error");
1094        }
1095    }
1096
1097    #[test]
1098    fn test_table_validation_error_display() {
1099        let error = TableValidationError::DuplicateIndexColumn {
1100            index_name: "idx_test".into(),
1101            column_name: "col1".into(),
1102        };
1103        let error_msg = format!("{}", error);
1104        assert!(error_msg.contains("idx_test"));
1105        assert!(error_msg.contains("col1"));
1106        assert!(error_msg.contains("Duplicate index"));
1107    }
1108
1109    #[test]
1110    fn normalize_inline_unique_str_with_different_constraint_type() {
1111        // Test that other constraint types don't match in the exists check
1112        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1113        email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into()));
1114
1115        let table = TableDef {
1116            name: "users".into(),
1117            columns: vec![
1118                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1119                email_col,
1120            ],
1121            constraints: vec![
1122                // Add a PrimaryKey constraint (different type) - should not match
1123                TableConstraint::PrimaryKey {
1124                    auto_increment: false,
1125                    columns: vec!["id".into()],
1126                },
1127            ],
1128            indexes: vec![],
1129        };
1130
1131        let normalized = table.normalize().unwrap();
1132        // Should have: PrimaryKey (existing) + Unique (new)
1133        assert_eq!(normalized.constraints.len(), 2);
1134    }
1135
1136    #[test]
1137    fn normalize_inline_unique_array_with_different_constraint_type() {
1138        // Test that other constraint types don't match in the exists check for Array case
1139        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1140        col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()]));
1141
1142        let table = TableDef {
1143            name: "test".into(),
1144            columns: vec![
1145                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1146                col1,
1147            ],
1148            constraints: vec![
1149                // Add a PrimaryKey constraint (different type) - should not match
1150                TableConstraint::PrimaryKey {
1151                    auto_increment: false,
1152                    columns: vec!["id".into()],
1153                },
1154            ],
1155            indexes: vec![],
1156        };
1157
1158        let normalized = table.normalize().unwrap();
1159        // Should have: PrimaryKey (existing) + Unique (new)
1160        assert_eq!(normalized.constraints.len(), 2);
1161    }
1162
1163    #[test]
1164    fn normalize_duplicate_index_bool_true_same_column() {
1165        // Test that Bool(true) with duplicate on same column errors
1166        let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1167        col1.index = Some(StrOrBoolOrArray::Bool(true));
1168
1169        let table = TableDef {
1170            name: "test".into(),
1171            columns: vec![
1172                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1173                col1.clone(),
1174                {
1175                    // Same column with Bool(true) again
1176                    let mut c = col1.clone();
1177                    c.index = Some(StrOrBoolOrArray::Bool(true));
1178                    c
1179                },
1180            ],
1181            constraints: vec![],
1182            indexes: vec![],
1183        };
1184
1185        let result = table.normalize();
1186        assert!(result.is_err());
1187        if let Err(TableValidationError::DuplicateIndexColumn {
1188            index_name,
1189            column_name,
1190        }) = result
1191        {
1192            assert!(index_name.contains("idx_test"));
1193            assert!(index_name.contains("col1"));
1194            assert_eq!(column_name, "col1");
1195        } else {
1196            panic!("Expected DuplicateIndexColumn error");
1197        }
1198    }
1199
1200    #[test]
1201    fn normalize_inline_foreign_key_string_syntax() {
1202        // Test ForeignKeySyntax::String with valid "table.column" format
1203        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1204        user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.id".into()));
1205
1206        let table = TableDef {
1207            name: "posts".into(),
1208            columns: vec![
1209                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1210                user_id_col,
1211            ],
1212            constraints: vec![],
1213            indexes: vec![],
1214        };
1215
1216        let normalized = table.normalize().unwrap();
1217        assert_eq!(normalized.constraints.len(), 1);
1218        assert!(matches!(
1219            &normalized.constraints[0],
1220            TableConstraint::ForeignKey {
1221                name: None,
1222                columns,
1223                ref_table,
1224                ref_columns,
1225                on_delete: None,
1226                on_update: None,
1227            } if columns == &["user_id".to_string()]
1228                && ref_table == "users"
1229                && ref_columns == &["id".to_string()]
1230        ));
1231    }
1232
1233    #[test]
1234    fn normalize_inline_foreign_key_invalid_format_no_dot() {
1235        // Test ForeignKeySyntax::String with invalid format (no dot)
1236        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1237        user_id_col.foreign_key = Some(ForeignKeySyntax::String("usersid".into()));
1238
1239        let table = TableDef {
1240            name: "posts".into(),
1241            columns: vec![
1242                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1243                user_id_col,
1244            ],
1245            constraints: vec![],
1246            indexes: vec![],
1247        };
1248
1249        let result = table.normalize();
1250        assert!(result.is_err());
1251        if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1252            assert_eq!(column_name, "user_id");
1253            assert_eq!(value, "usersid");
1254        } else {
1255            panic!("Expected InvalidForeignKeyFormat error");
1256        }
1257    }
1258
1259    #[test]
1260    fn normalize_inline_foreign_key_invalid_format_empty_table() {
1261        // Test ForeignKeySyntax::String with empty table part
1262        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1263        user_id_col.foreign_key = Some(ForeignKeySyntax::String(".id".into()));
1264
1265        let table = TableDef {
1266            name: "posts".into(),
1267            columns: vec![
1268                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1269                user_id_col,
1270            ],
1271            constraints: vec![],
1272            indexes: vec![],
1273        };
1274
1275        let result = table.normalize();
1276        assert!(result.is_err());
1277        if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1278            assert_eq!(column_name, "user_id");
1279            assert_eq!(value, ".id");
1280        } else {
1281            panic!("Expected InvalidForeignKeyFormat error");
1282        }
1283    }
1284
1285    #[test]
1286    fn normalize_inline_foreign_key_invalid_format_empty_column() {
1287        // Test ForeignKeySyntax::String with empty column part
1288        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1289        user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.".into()));
1290
1291        let table = TableDef {
1292            name: "posts".into(),
1293            columns: vec![
1294                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1295                user_id_col,
1296            ],
1297            constraints: vec![],
1298            indexes: vec![],
1299        };
1300
1301        let result = table.normalize();
1302        assert!(result.is_err());
1303        if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1304            assert_eq!(column_name, "user_id");
1305            assert_eq!(value, "users.");
1306        } else {
1307            panic!("Expected InvalidForeignKeyFormat error");
1308        }
1309    }
1310
1311    #[test]
1312    fn normalize_inline_foreign_key_invalid_format_too_many_parts() {
1313        // Test ForeignKeySyntax::String with too many parts
1314        let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer));
1315        user_id_col.foreign_key = Some(ForeignKeySyntax::String("schema.users.id".into()));
1316
1317        let table = TableDef {
1318            name: "posts".into(),
1319            columns: vec![
1320                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1321                user_id_col,
1322            ],
1323            constraints: vec![],
1324            indexes: vec![],
1325        };
1326
1327        let result = table.normalize();
1328        assert!(result.is_err());
1329        if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result {
1330            assert_eq!(column_name, "user_id");
1331            assert_eq!(value, "schema.users.id");
1332        } else {
1333            panic!("Expected InvalidForeignKeyFormat error");
1334        }
1335    }
1336
1337    #[test]
1338    fn normalize_inline_primary_key_with_auto_increment() {
1339        use crate::schema::primary_key::PrimaryKeyDef;
1340
1341        let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
1342        id_col.primary_key = Some(PrimaryKeySyntax::Object(PrimaryKeyDef {
1343            auto_increment: true,
1344            columns: vec![], // columns is ignored for inline definition
1345        }));
1346
1347        let table = TableDef {
1348            name: "users".into(),
1349            columns: vec![
1350                id_col,
1351                col("name", ColumnType::Simple(SimpleColumnType::Text)),
1352            ],
1353            constraints: vec![],
1354            indexes: vec![],
1355        };
1356
1357        let normalized = table.normalize().unwrap();
1358        assert_eq!(normalized.constraints.len(), 1);
1359        assert!(matches!(
1360            &normalized.constraints[0],
1361            TableConstraint::PrimaryKey { auto_increment: true, columns } if columns == &["id".to_string()]
1362        ));
1363    }
1364
1365    #[test]
1366    fn normalize_duplicate_inline_index_on_same_column() {
1367        // This test triggers the DuplicateIndexColumn error (lines 251-253)
1368        // by having the same column appear twice in the same named index group
1369        use crate::schema::str_or_bool::StrOrBoolOrArray;
1370
1371        // Create a column that references the same index name twice (via Array)
1372        let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
1373        email_col.index = Some(StrOrBoolOrArray::Array(vec![
1374            "idx_email".into(),
1375            "idx_email".into(), // Duplicate reference
1376        ]));
1377
1378        let table = TableDef {
1379            name: "users".into(),
1380            columns: vec![
1381                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1382                email_col,
1383            ],
1384            constraints: vec![],
1385            indexes: vec![],
1386        };
1387
1388        let result = table.normalize();
1389        assert!(result.is_err());
1390        if let Err(TableValidationError::DuplicateIndexColumn {
1391            index_name,
1392            column_name,
1393        }) = result
1394        {
1395            assert_eq!(index_name, "idx_email");
1396            assert_eq!(column_name, "email");
1397        } else {
1398            panic!("Expected DuplicateIndexColumn error, got: {:?}", result);
1399        }
1400    }
1401
1402    #[test]
1403    fn test_invalid_foreign_key_format_error_display() {
1404        let error = TableValidationError::InvalidForeignKeyFormat {
1405            column_name: "user_id".into(),
1406            value: "invalid".into(),
1407        };
1408        let error_msg = format!("{}", error);
1409        assert!(error_msg.contains("user_id"));
1410        assert!(error_msg.contains("invalid"));
1411        assert!(error_msg.contains("table.column"));
1412    }
1413}