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