vespertide_core/schema/
table.rs

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