Skip to main content

vespertide_query/sql/
mod.rs

1pub mod add_column;
2pub mod add_constraint;
3pub mod create_table;
4pub mod delete_column;
5pub mod delete_table;
6pub mod helpers;
7pub mod modify_column_comment;
8pub mod modify_column_default;
9pub mod modify_column_nullable;
10pub mod modify_column_type;
11pub mod raw_sql;
12pub mod remove_constraint;
13pub mod rename_column;
14pub mod rename_table;
15pub mod types;
16
17pub use helpers::*;
18pub use types::{BuiltQuery, DatabaseBackend, RawSql};
19
20use crate::error::QueryError;
21use vespertide_core::{MigrationAction, TableConstraint, TableDef};
22
23use self::{
24    add_column::build_add_column, add_constraint::build_add_constraint,
25    create_table::build_create_table, delete_column::build_delete_column,
26    delete_table::build_delete_table, modify_column_comment::build_modify_column_comment,
27    modify_column_default::build_modify_column_default,
28    modify_column_nullable::build_modify_column_nullable,
29    modify_column_type::build_modify_column_type, raw_sql::build_raw_sql,
30    remove_constraint::build_remove_constraint, rename_column::build_rename_column,
31    rename_table::build_rename_table,
32};
33
34pub fn build_action_queries(
35    backend: &DatabaseBackend,
36    action: &MigrationAction,
37    current_schema: &[TableDef],
38) -> Result<Vec<BuiltQuery>, QueryError> {
39    build_action_queries_with_pending(backend, action, current_schema, &[])
40}
41
42/// Build SQL queries for a migration action, with awareness of pending constraints.
43///
44/// `pending_constraints` are constraints that exist in the logical schema but haven't been
45/// physically created as database indexes yet. This is used by SQLite temp table rebuilds
46/// to avoid recreating indexes that will be created by future AddConstraint actions.
47pub fn build_action_queries_with_pending(
48    backend: &DatabaseBackend,
49    action: &MigrationAction,
50    current_schema: &[TableDef],
51    pending_constraints: &[TableConstraint],
52) -> Result<Vec<BuiltQuery>, QueryError> {
53    match action {
54        MigrationAction::CreateTable {
55            table,
56            columns,
57            constraints,
58        } => build_create_table(backend, table, columns, constraints),
59
60        MigrationAction::DeleteTable { table } => Ok(vec![build_delete_table(table)]),
61
62        MigrationAction::AddColumn {
63            table,
64            column,
65            fill_with,
66        } => build_add_column(backend, table, column, fill_with.as_deref(), current_schema),
67
68        MigrationAction::RenameColumn { table, from, to } => {
69            Ok(vec![build_rename_column(table, from, to)])
70        }
71
72        MigrationAction::DeleteColumn { table, column } => {
73            // Find the column type from current schema for enum DROP TYPE support
74            let column_type = current_schema
75                .iter()
76                .find(|t| t.name == *table)
77                .and_then(|t| t.columns.iter().find(|c| c.name == *column))
78                .map(|c| &c.r#type);
79            Ok(build_delete_column(
80                backend,
81                table,
82                column,
83                column_type,
84                current_schema,
85            ))
86        }
87
88        MigrationAction::ModifyColumnType {
89            table,
90            column,
91            new_type,
92        } => build_modify_column_type(backend, table, column, new_type, current_schema),
93
94        MigrationAction::ModifyColumnNullable {
95            table,
96            column,
97            nullable,
98            fill_with,
99        } => build_modify_column_nullable(
100            backend,
101            table,
102            column,
103            *nullable,
104            fill_with.as_deref(),
105            current_schema,
106        ),
107
108        MigrationAction::ModifyColumnDefault {
109            table,
110            column,
111            new_default,
112        } => build_modify_column_default(
113            backend,
114            table,
115            column,
116            new_default.as_deref(),
117            current_schema,
118        ),
119
120        MigrationAction::ModifyColumnComment {
121            table,
122            column,
123            new_comment,
124        } => build_modify_column_comment(
125            backend,
126            table,
127            column,
128            new_comment.as_deref(),
129            current_schema,
130        ),
131
132        MigrationAction::RenameTable { from, to } => Ok(vec![build_rename_table(from, to)]),
133
134        MigrationAction::RawSql { sql } => Ok(vec![build_raw_sql(sql.clone())]),
135
136        MigrationAction::AddConstraint { table, constraint } => build_add_constraint(
137            backend,
138            table,
139            constraint,
140            current_schema,
141            pending_constraints,
142        ),
143
144        MigrationAction::RemoveConstraint { table, constraint } => {
145            build_remove_constraint(backend, table, constraint, current_schema)
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use insta::{assert_snapshot, with_settings};
154    use rstest::rstest;
155    use vespertide_core::schema::primary_key::PrimaryKeySyntax;
156    use vespertide_core::{
157        ColumnDef, ColumnType, MigrationAction, ReferenceAction, SimpleColumnType, TableConstraint,
158    };
159
160    fn col(name: &str, ty: ColumnType) -> ColumnDef {
161        ColumnDef {
162            name: name.to_string(),
163            r#type: ty,
164            nullable: true,
165            default: None,
166            comment: None,
167            primary_key: None,
168            unique: None,
169            index: None,
170            foreign_key: None,
171        }
172    }
173
174    #[test]
175    fn test_backend_specific_quoting() {
176        let action = MigrationAction::CreateTable {
177            table: "users".into(),
178            columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
179            constraints: vec![],
180        };
181        let result = build_action_queries(&DatabaseBackend::Postgres, &action, &[]).unwrap();
182
183        // PostgreSQL uses double quotes
184        let pg_sql = result[0].build(DatabaseBackend::Postgres);
185        assert!(pg_sql.contains("\"users\""));
186
187        // MySQL uses backticks
188        let mysql_sql = result[0].build(DatabaseBackend::MySql);
189        assert!(mysql_sql.contains("`users`"));
190
191        // SQLite uses double quotes
192        let sqlite_sql = result[0].build(DatabaseBackend::Sqlite);
193        assert!(sqlite_sql.contains("\"users\""));
194    }
195
196    #[rstest]
197    #[case::create_table_with_default_postgres(
198        "create_table_with_default_postgres",
199        MigrationAction::CreateTable {
200            table: "users".into(),
201            columns: vec![ColumnDef {
202                name: "status".into(),
203                r#type: ColumnType::Simple(SimpleColumnType::Text),
204                nullable: true,
205                default: Some("'active'".into()),
206                comment: None,
207                primary_key: None,
208                unique: None,
209                index: None,
210                foreign_key: None,
211            }],
212            constraints: vec![],
213        },
214        DatabaseBackend::Postgres,
215        &["DEFAULT", "'active'"]
216    )]
217    #[case::create_table_with_default_mysql(
218        "create_table_with_default_mysql",
219        MigrationAction::CreateTable {
220            table: "users".into(),
221            columns: vec![ColumnDef {
222                name: "status".into(),
223                r#type: ColumnType::Simple(SimpleColumnType::Text),
224                nullable: true,
225                default: Some("'active'".into()),
226                comment: None,
227                primary_key: None,
228                unique: None,
229                index: None,
230                foreign_key: None,
231            }],
232            constraints: vec![],
233        },
234        DatabaseBackend::Postgres,
235        &["DEFAULT", "'active'"]
236    )]
237    #[case::create_table_with_default_sqlite(
238        "create_table_with_default_sqlite",
239        MigrationAction::CreateTable {
240            table: "users".into(),
241            columns: vec![ColumnDef {
242                name: "status".into(),
243                r#type: ColumnType::Simple(SimpleColumnType::Text),
244                nullable: true,
245                default: Some("'active'".into()),
246                comment: None,
247                primary_key: None,
248                unique: None,
249                index: None,
250                foreign_key: None,
251            }],
252            constraints: vec![],
253        },
254        DatabaseBackend::Postgres,
255        &["DEFAULT", "'active'"]
256    )]
257    #[case::create_table_with_inline_primary_key_postgres(
258        "create_table_with_inline_primary_key_postgres",
259        MigrationAction::CreateTable {
260            table: "users".into(),
261            columns: vec![ColumnDef {
262                name: "id".into(),
263                r#type: ColumnType::Simple(SimpleColumnType::Integer),
264                nullable: false,
265                default: None,
266                comment: None,
267                primary_key: Some(PrimaryKeySyntax::Bool(true)),
268                unique: None,
269                index: None,
270                foreign_key: None,
271            }],
272            constraints: vec![],
273        },
274        DatabaseBackend::Postgres,
275        &["PRIMARY KEY"]
276    )]
277    #[case::create_table_with_inline_primary_key_mysql(
278        "create_table_with_inline_primary_key_mysql",
279        MigrationAction::CreateTable {
280            table: "users".into(),
281            columns: vec![ColumnDef {
282                name: "id".into(),
283                r#type: ColumnType::Simple(SimpleColumnType::Integer),
284                nullable: false,
285                default: None,
286                comment: None,
287                primary_key: Some(PrimaryKeySyntax::Bool(true)),
288                unique: None,
289                index: None,
290                foreign_key: None,
291            }],
292            constraints: vec![],
293        },
294        DatabaseBackend::Postgres,
295        &["PRIMARY KEY"]
296    )]
297    #[case::create_table_with_inline_primary_key_sqlite(
298        "create_table_with_inline_primary_key_sqlite",
299        MigrationAction::CreateTable {
300            table: "users".into(),
301            columns: vec![ColumnDef {
302                name: "id".into(),
303                r#type: ColumnType::Simple(SimpleColumnType::Integer),
304                nullable: false,
305                default: None,
306                comment: None,
307                primary_key: Some(PrimaryKeySyntax::Bool(true)),
308                unique: None,
309                index: None,
310                foreign_key: None,
311            }],
312            constraints: vec![],
313        },
314        DatabaseBackend::Postgres,
315        &["PRIMARY KEY"]
316    )]
317    #[case::create_table_with_fk_postgres(
318        "create_table_with_fk_postgres",
319        MigrationAction::CreateTable {
320            table: "posts".into(),
321            columns: vec![
322                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
323                col("user_id", ColumnType::Simple(SimpleColumnType::Integer)),
324            ],
325            constraints: vec![TableConstraint::ForeignKey {
326                name: Some("fk_user".into()),
327                columns: vec!["user_id".into()],
328                ref_table: "users".into(),
329                ref_columns: vec!["id".into()],
330                on_delete: Some(ReferenceAction::Cascade),
331                on_update: Some(ReferenceAction::Restrict),
332            }],
333        },
334        DatabaseBackend::Postgres,
335        &["REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"]
336    )]
337    #[case::create_table_with_fk_mysql(
338        "create_table_with_fk_mysql",
339        MigrationAction::CreateTable {
340            table: "posts".into(),
341            columns: vec![
342                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
343                col("user_id", ColumnType::Simple(SimpleColumnType::Integer)),
344            ],
345            constraints: vec![TableConstraint::ForeignKey {
346                name: Some("fk_user".into()),
347                columns: vec!["user_id".into()],
348                ref_table: "users".into(),
349                ref_columns: vec!["id".into()],
350                on_delete: Some(ReferenceAction::Cascade),
351                on_update: Some(ReferenceAction::Restrict),
352            }],
353        },
354        DatabaseBackend::Postgres,
355        &["REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"]
356    )]
357    #[case::create_table_with_fk_sqlite(
358        "create_table_with_fk_sqlite",
359        MigrationAction::CreateTable {
360            table: "posts".into(),
361            columns: vec![
362                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
363                col("user_id", ColumnType::Simple(SimpleColumnType::Integer)),
364            ],
365            constraints: vec![TableConstraint::ForeignKey {
366                name: Some("fk_user".into()),
367                columns: vec!["user_id".into()],
368                ref_table: "users".into(),
369                ref_columns: vec!["id".into()],
370                on_delete: Some(ReferenceAction::Cascade),
371                on_update: Some(ReferenceAction::Restrict),
372            }],
373        },
374        DatabaseBackend::Postgres,
375        &["REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"]
376    )]
377    fn test_build_migration_action(
378        #[case] title: &str,
379        #[case] action: MigrationAction,
380        #[case] backend: DatabaseBackend,
381        #[case] expected: &[&str],
382    ) {
383        let result = build_action_queries(&backend, &action, &[]).unwrap();
384        let sql = result[0].build(backend);
385        for exp in expected {
386            assert!(
387                sql.contains(exp),
388                "Expected SQL to contain '{}', got: {}",
389                exp,
390                sql
391            );
392        }
393
394        with_settings!({ snapshot_suffix => format!("build_migration_action_{}", title) }, {
395            assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::<Vec<String>>().join("\n"));
396        });
397    }
398
399    #[rstest]
400    #[case::rename_column_postgres(DatabaseBackend::Postgres)]
401    #[case::rename_column_mysql(DatabaseBackend::MySql)]
402    #[case::rename_column_sqlite(DatabaseBackend::Sqlite)]
403    fn test_build_action_queries_rename_column(#[case] backend: DatabaseBackend) {
404        // Test MigrationAction::RenameColumn (lines 51-52)
405        let action = MigrationAction::RenameColumn {
406            table: "users".into(),
407            from: "old_name".into(),
408            to: "new_name".into(),
409        };
410        let result = build_action_queries(&backend, &action, &[]).unwrap();
411        assert_eq!(result.len(), 1);
412        let sql = result[0].build(backend);
413        assert!(sql.contains("RENAME"));
414        assert!(sql.contains("old_name"));
415        assert!(sql.contains("new_name"));
416
417        with_settings!({ snapshot_suffix => format!("rename_column_{:?}", backend) }, {
418            assert_snapshot!(sql);
419        });
420    }
421
422    #[rstest]
423    #[case::delete_column_postgres(DatabaseBackend::Postgres)]
424    #[case::delete_column_mysql(DatabaseBackend::MySql)]
425    #[case::delete_column_sqlite(DatabaseBackend::Sqlite)]
426    fn test_build_action_queries_delete_column(#[case] backend: DatabaseBackend) {
427        // Test MigrationAction::DeleteColumn (lines 55-56)
428        let action = MigrationAction::DeleteColumn {
429            table: "users".into(),
430            column: "email".into(),
431        };
432        let result = build_action_queries(&backend, &action, &[]).unwrap();
433        assert_eq!(result.len(), 1);
434        let sql = result[0].build(backend);
435        assert!(sql.contains("DROP COLUMN"));
436        assert!(sql.contains("email"));
437
438        with_settings!({ snapshot_suffix => format!("delete_column_{:?}", backend) }, {
439            assert_snapshot!(sql);
440        });
441    }
442
443    #[rstest]
444    #[case::modify_column_type_postgres(DatabaseBackend::Postgres)]
445    #[case::modify_column_type_mysql(DatabaseBackend::MySql)]
446    #[case::modify_column_type_sqlite(DatabaseBackend::Sqlite)]
447    fn test_build_action_queries_modify_column_type(#[case] backend: DatabaseBackend) {
448        // Test MigrationAction::ModifyColumnType (lines 60-63)
449        let action = MigrationAction::ModifyColumnType {
450            table: "users".into(),
451            column: "age".into(),
452            new_type: ColumnType::Simple(SimpleColumnType::BigInt),
453        };
454        let current_schema = vec![TableDef {
455            name: "users".into(),
456            description: None,
457            columns: vec![ColumnDef {
458                name: "age".into(),
459                r#type: ColumnType::Simple(SimpleColumnType::Integer),
460                nullable: true,
461                default: None,
462                comment: None,
463                primary_key: None,
464                unique: None,
465                index: None,
466                foreign_key: None,
467            }],
468            constraints: vec![],
469        }];
470        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
471        assert!(!result.is_empty());
472        let sql = result
473            .iter()
474            .map(|q| q.build(backend))
475            .collect::<Vec<String>>()
476            .join("\n");
477        assert!(sql.contains("ALTER TABLE"));
478
479        with_settings!({ snapshot_suffix => format!("modify_column_type_{:?}", backend) }, {
480            assert_snapshot!(sql);
481        });
482    }
483
484    #[rstest]
485    #[case::remove_index_constraint_postgres(DatabaseBackend::Postgres)]
486    #[case::remove_index_constraint_mysql(DatabaseBackend::MySql)]
487    #[case::remove_index_constraint_sqlite(DatabaseBackend::Sqlite)]
488    fn test_build_action_queries_remove_index_constraint(#[case] backend: DatabaseBackend) {
489        // Test MigrationAction::RemoveConstraint with Index variant
490        let action = MigrationAction::RemoveConstraint {
491            table: "users".into(),
492            constraint: TableConstraint::Index {
493                name: Some("idx_email".into()),
494                columns: vec!["email".into()],
495            },
496        };
497        let result = build_action_queries(&backend, &action, &[]).unwrap();
498        assert_eq!(result.len(), 1);
499        let sql = result[0].build(backend);
500        assert!(sql.contains("DROP INDEX"));
501        assert!(sql.contains("idx_email"));
502
503        with_settings!({ snapshot_suffix => format!("remove_index_constraint_{:?}", backend) }, {
504            assert_snapshot!(sql);
505        });
506    }
507
508    #[rstest]
509    #[case::rename_table_postgres(DatabaseBackend::Postgres)]
510    #[case::rename_table_mysql(DatabaseBackend::MySql)]
511    #[case::rename_table_sqlite(DatabaseBackend::Sqlite)]
512    fn test_build_action_queries_rename_table(#[case] backend: DatabaseBackend) {
513        // Test MigrationAction::RenameTable (line 69)
514        let action = MigrationAction::RenameTable {
515            from: "old_table".into(),
516            to: "new_table".into(),
517        };
518        let result = build_action_queries(&backend, &action, &[]).unwrap();
519        assert_eq!(result.len(), 1);
520        let sql = result[0].build(backend);
521        assert!(sql.contains("RENAME"));
522        assert!(sql.contains("old_table"));
523        assert!(sql.contains("new_table"));
524
525        with_settings!({ snapshot_suffix => format!("rename_table_{:?}", backend) }, {
526            assert_snapshot!(sql);
527        });
528    }
529
530    #[rstest]
531    #[case::add_constraint_postgres(DatabaseBackend::Postgres)]
532    #[case::add_constraint_mysql(DatabaseBackend::MySql)]
533    #[case::add_constraint_sqlite(DatabaseBackend::Sqlite)]
534    fn test_build_action_queries_add_constraint(#[case] backend: DatabaseBackend) {
535        // Test MigrationAction::AddConstraint (lines 73-74)
536        let action = MigrationAction::AddConstraint {
537            table: "users".into(),
538            constraint: TableConstraint::Unique {
539                name: Some("uq_email".into()),
540                columns: vec!["email".into()],
541            },
542        };
543        let current_schema = vec![TableDef {
544            name: "users".into(),
545            description: None,
546            columns: vec![
547                ColumnDef {
548                    name: "id".into(),
549                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
550                    nullable: false,
551                    default: None,
552                    comment: None,
553                    primary_key: None,
554                    unique: None,
555                    index: None,
556                    foreign_key: None,
557                },
558                ColumnDef {
559                    name: "email".into(),
560                    r#type: ColumnType::Simple(SimpleColumnType::Text),
561                    nullable: true,
562                    default: None,
563                    comment: None,
564                    primary_key: None,
565                    unique: None,
566                    index: None,
567                    foreign_key: None,
568                },
569            ],
570            constraints: vec![],
571        }];
572        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
573        assert!(!result.is_empty());
574        let sql = result
575            .iter()
576            .map(|q| q.build(backend))
577            .collect::<Vec<String>>()
578            .join("\n");
579        assert!(sql.contains("UNIQUE") || sql.contains("uq_email"));
580
581        with_settings!({ snapshot_suffix => format!("add_constraint_{:?}", backend) }, {
582            assert_snapshot!(sql);
583        });
584    }
585
586    #[rstest]
587    #[case::remove_constraint_postgres(DatabaseBackend::Postgres)]
588    #[case::remove_constraint_mysql(DatabaseBackend::MySql)]
589    #[case::remove_constraint_sqlite(DatabaseBackend::Sqlite)]
590    fn test_build_action_queries_remove_constraint(#[case] backend: DatabaseBackend) {
591        // Test MigrationAction::RemoveConstraint (lines 77-78)
592        let action = MigrationAction::RemoveConstraint {
593            table: "users".into(),
594            constraint: TableConstraint::Unique {
595                name: Some("uq_email".into()),
596                columns: vec!["email".into()],
597            },
598        };
599        let current_schema = vec![TableDef {
600            name: "users".into(),
601            description: None,
602            columns: vec![
603                ColumnDef {
604                    name: "id".into(),
605                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
606                    nullable: false,
607                    default: None,
608                    comment: None,
609                    primary_key: None,
610                    unique: None,
611                    index: None,
612                    foreign_key: None,
613                },
614                ColumnDef {
615                    name: "email".into(),
616                    r#type: ColumnType::Simple(SimpleColumnType::Text),
617                    nullable: true,
618                    default: None,
619                    comment: None,
620                    primary_key: None,
621                    unique: None,
622                    index: None,
623                    foreign_key: None,
624                },
625            ],
626            constraints: vec![TableConstraint::Unique {
627                name: Some("uq_email".into()),
628                columns: vec!["email".into()],
629            }],
630        }];
631        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
632        assert!(!result.is_empty());
633        let sql = result
634            .iter()
635            .map(|q| q.build(backend))
636            .collect::<Vec<String>>()
637            .join("\n");
638        assert!(sql.contains("DROP") || sql.contains("CONSTRAINT"));
639
640        with_settings!({ snapshot_suffix => format!("remove_constraint_{:?}", backend) }, {
641            assert_snapshot!(sql);
642        });
643    }
644
645    #[rstest]
646    #[case::add_column_postgres(DatabaseBackend::Postgres)]
647    #[case::add_column_mysql(DatabaseBackend::MySql)]
648    #[case::add_column_sqlite(DatabaseBackend::Sqlite)]
649    fn test_build_action_queries_add_column(#[case] backend: DatabaseBackend) {
650        // Test MigrationAction::AddColumn (lines 46-49)
651        let action = MigrationAction::AddColumn {
652            table: "users".into(),
653            column: Box::new(ColumnDef {
654                name: "email".into(),
655                r#type: ColumnType::Simple(SimpleColumnType::Text),
656                nullable: true,
657                default: None,
658                comment: None,
659                primary_key: None,
660                unique: None,
661                index: None,
662                foreign_key: None,
663            }),
664            fill_with: None,
665        };
666        let current_schema = vec![TableDef {
667            name: "users".into(),
668            description: None,
669            columns: vec![ColumnDef {
670                name: "id".into(),
671                r#type: ColumnType::Simple(SimpleColumnType::Integer),
672                nullable: false,
673                default: None,
674                comment: None,
675                primary_key: None,
676                unique: None,
677                index: None,
678                foreign_key: None,
679            }],
680            constraints: vec![],
681        }];
682        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
683        assert!(!result.is_empty());
684        let sql = result
685            .iter()
686            .map(|q| q.build(backend))
687            .collect::<Vec<String>>()
688            .join("\n");
689        assert!(sql.contains("ALTER TABLE"));
690        assert!(sql.contains("ADD COLUMN") || sql.contains("ADD"));
691
692        with_settings!({ snapshot_suffix => format!("add_column_{:?}", backend) }, {
693            assert_snapshot!(sql);
694        });
695    }
696
697    #[rstest]
698    #[case::add_index_constraint_postgres(DatabaseBackend::Postgres)]
699    #[case::add_index_constraint_mysql(DatabaseBackend::MySql)]
700    #[case::add_index_constraint_sqlite(DatabaseBackend::Sqlite)]
701    fn test_build_action_queries_add_index_constraint(#[case] backend: DatabaseBackend) {
702        // Test MigrationAction::AddConstraint with Index variant
703        let action = MigrationAction::AddConstraint {
704            table: "users".into(),
705            constraint: TableConstraint::Index {
706                name: Some("idx_email".into()),
707                columns: vec!["email".into()],
708            },
709        };
710        let result = build_action_queries(&backend, &action, &[]).unwrap();
711        assert_eq!(result.len(), 1);
712        let sql = result[0].build(backend);
713        assert!(sql.contains("CREATE INDEX"));
714        assert!(sql.contains("idx_email"));
715
716        with_settings!({ snapshot_suffix => format!("add_index_constraint_{:?}", backend) }, {
717            assert_snapshot!(sql);
718        });
719    }
720
721    #[rstest]
722    #[case::raw_sql_postgres(DatabaseBackend::Postgres)]
723    #[case::raw_sql_mysql(DatabaseBackend::MySql)]
724    #[case::raw_sql_sqlite(DatabaseBackend::Sqlite)]
725    fn test_build_action_queries_raw_sql(#[case] backend: DatabaseBackend) {
726        // Test MigrationAction::RawSql (line 71)
727        let action = MigrationAction::RawSql {
728            sql: "SELECT 1;".into(),
729        };
730        let result = build_action_queries(&backend, &action, &[]).unwrap();
731        assert_eq!(result.len(), 1);
732        let sql = result[0].build(backend);
733        assert_eq!(sql, "SELECT 1;");
734
735        with_settings!({ snapshot_suffix => format!("raw_sql_{:?}", backend) }, {
736            assert_snapshot!(sql);
737        });
738    }
739
740    // Comprehensive index naming tests
741    #[rstest]
742    #[case::add_index_with_custom_name_postgres(
743        DatabaseBackend::Postgres,
744        "hello",
745        vec!["email", "password"]
746    )]
747    #[case::add_index_with_custom_name_mysql(
748        DatabaseBackend::MySql,
749        "hello",
750        vec!["email", "password"]
751    )]
752    #[case::add_index_with_custom_name_sqlite(
753        DatabaseBackend::Sqlite,
754        "hello",
755        vec!["email", "password"]
756    )]
757    #[case::add_index_single_column_postgres(
758        DatabaseBackend::Postgres,
759        "email_idx",
760        vec!["email"]
761    )]
762    #[case::add_index_single_column_mysql(
763        DatabaseBackend::MySql,
764        "email_idx",
765        vec!["email"]
766    )]
767    #[case::add_index_single_column_sqlite(
768        DatabaseBackend::Sqlite,
769        "email_idx",
770        vec!["email"]
771    )]
772    fn test_add_index_with_custom_name(
773        #[case] backend: DatabaseBackend,
774        #[case] index_name: &str,
775        #[case] columns: Vec<&str>,
776    ) {
777        // Test that custom index names follow ix_table__name pattern
778        let action = MigrationAction::AddConstraint {
779            table: "user".into(),
780            constraint: TableConstraint::Index {
781                name: Some(index_name.into()),
782                columns: columns.iter().map(|s| s.to_string()).collect(),
783            },
784        };
785        let result = build_action_queries(&backend, &action, &[]).unwrap();
786        let sql = result[0].build(backend);
787
788        // Should use ix_table__name pattern
789        let expected_name = format!("ix_user__{}", index_name);
790        assert!(
791            sql.contains(&expected_name),
792            "Expected index name '{}' in SQL: {}",
793            expected_name,
794            sql
795        );
796
797        with_settings!({ snapshot_suffix => format!("add_index_custom_{}_{:?}", index_name, backend) }, {
798            assert_snapshot!(sql);
799        });
800    }
801
802    #[rstest]
803    #[case::add_unnamed_index_single_column_postgres(
804        DatabaseBackend::Postgres,
805        vec!["email"]
806    )]
807    #[case::add_unnamed_index_single_column_mysql(
808        DatabaseBackend::MySql,
809        vec!["email"]
810    )]
811    #[case::add_unnamed_index_single_column_sqlite(
812        DatabaseBackend::Sqlite,
813        vec!["email"]
814    )]
815    #[case::add_unnamed_index_multiple_columns_postgres(
816        DatabaseBackend::Postgres,
817        vec!["email", "password"]
818    )]
819    #[case::add_unnamed_index_multiple_columns_mysql(
820        DatabaseBackend::MySql,
821        vec!["email", "password"]
822    )]
823    #[case::add_unnamed_index_multiple_columns_sqlite(
824        DatabaseBackend::Sqlite,
825        vec!["email", "password"]
826    )]
827    fn test_add_unnamed_index(#[case] backend: DatabaseBackend, #[case] columns: Vec<&str>) {
828        // Test that unnamed indexes follow ix_table__col1_col2 pattern
829        let action = MigrationAction::AddConstraint {
830            table: "user".into(),
831            constraint: TableConstraint::Index {
832                name: None,
833                columns: columns.iter().map(|s| s.to_string()).collect(),
834            },
835        };
836        let result = build_action_queries(&backend, &action, &[]).unwrap();
837        let sql = result[0].build(backend);
838
839        // Should use ix_table__col1_col2... pattern
840        let expected_name = format!("ix_user__{}", columns.join("_"));
841        assert!(
842            sql.contains(&expected_name),
843            "Expected index name '{}' in SQL: {}",
844            expected_name,
845            sql
846        );
847
848        with_settings!({ snapshot_suffix => format!("add_unnamed_index_{}_{:?}", columns.join("_"), backend) }, {
849            assert_snapshot!(sql);
850        });
851    }
852
853    #[rstest]
854    #[case::remove_index_with_custom_name_postgres(
855        DatabaseBackend::Postgres,
856        "hello",
857        vec!["email", "password"]
858    )]
859    #[case::remove_index_with_custom_name_mysql(
860        DatabaseBackend::MySql,
861        "hello",
862        vec!["email", "password"]
863    )]
864    #[case::remove_index_with_custom_name_sqlite(
865        DatabaseBackend::Sqlite,
866        "hello",
867        vec!["email", "password"]
868    )]
869    fn test_remove_index_with_custom_name(
870        #[case] backend: DatabaseBackend,
871        #[case] index_name: &str,
872        #[case] columns: Vec<&str>,
873    ) {
874        // Test that removing custom index uses ix_table__name pattern
875        let action = MigrationAction::RemoveConstraint {
876            table: "user".into(),
877            constraint: TableConstraint::Index {
878                name: Some(index_name.into()),
879                columns: columns.iter().map(|s| s.to_string()).collect(),
880            },
881        };
882        let result = build_action_queries(&backend, &action, &[]).unwrap();
883        let sql = result[0].build(backend);
884
885        // Should use ix_table__name pattern
886        let expected_name = format!("ix_user__{}", index_name);
887        assert!(
888            sql.contains(&expected_name),
889            "Expected index name '{}' in SQL: {}",
890            expected_name,
891            sql
892        );
893
894        with_settings!({ snapshot_suffix => format!("remove_index_custom_{}_{:?}", index_name, backend) }, {
895            assert_snapshot!(sql);
896        });
897    }
898
899    #[rstest]
900    #[case::remove_unnamed_index_single_column_postgres(
901        DatabaseBackend::Postgres,
902        vec!["email"]
903    )]
904    #[case::remove_unnamed_index_single_column_mysql(
905        DatabaseBackend::MySql,
906        vec!["email"]
907    )]
908    #[case::remove_unnamed_index_single_column_sqlite(
909        DatabaseBackend::Sqlite,
910        vec!["email"]
911    )]
912    #[case::remove_unnamed_index_multiple_columns_postgres(
913        DatabaseBackend::Postgres,
914        vec!["email", "password"]
915    )]
916    #[case::remove_unnamed_index_multiple_columns_mysql(
917        DatabaseBackend::MySql,
918        vec!["email", "password"]
919    )]
920    #[case::remove_unnamed_index_multiple_columns_sqlite(
921        DatabaseBackend::Sqlite,
922        vec!["email", "password"]
923    )]
924    fn test_remove_unnamed_index(#[case] backend: DatabaseBackend, #[case] columns: Vec<&str>) {
925        // Test that removing unnamed indexes uses ix_table__col1_col2 pattern
926        let action = MigrationAction::RemoveConstraint {
927            table: "user".into(),
928            constraint: TableConstraint::Index {
929                name: None,
930                columns: columns.iter().map(|s| s.to_string()).collect(),
931            },
932        };
933        let result = build_action_queries(&backend, &action, &[]).unwrap();
934        let sql = result[0].build(backend);
935
936        // Should use ix_table__col1_col2... pattern
937        let expected_name = format!("ix_user__{}", columns.join("_"));
938        assert!(
939            sql.contains(&expected_name),
940            "Expected index name '{}' in SQL: {}",
941            expected_name,
942            sql
943        );
944
945        with_settings!({ snapshot_suffix => format!("remove_unnamed_index_{}_{:?}", columns.join("_"), backend) }, {
946            assert_snapshot!(sql);
947        });
948    }
949
950    // Comprehensive unique constraint naming tests
951    #[rstest]
952    #[case::add_unique_with_custom_name_postgres(
953        DatabaseBackend::Postgres,
954        "email_unique",
955        vec!["email"]
956    )]
957    #[case::add_unique_with_custom_name_mysql(
958        DatabaseBackend::MySql,
959        "email_unique",
960        vec!["email"]
961    )]
962    #[case::add_unique_with_custom_name_sqlite(
963        DatabaseBackend::Sqlite,
964        "email_unique",
965        vec!["email"]
966    )]
967    fn test_add_unique_with_custom_name(
968        #[case] backend: DatabaseBackend,
969        #[case] constraint_name: &str,
970        #[case] columns: Vec<&str>,
971    ) {
972        // Test that custom unique constraint names follow uq_table__name pattern
973        let action = MigrationAction::AddConstraint {
974            table: "user".into(),
975            constraint: TableConstraint::Unique {
976                name: Some(constraint_name.into()),
977                columns: columns.iter().map(|s| s.to_string()).collect(),
978            },
979        };
980
981        let current_schema = vec![TableDef {
982            name: "user".into(),
983            description: None,
984            columns: vec![ColumnDef {
985                name: "email".into(),
986                r#type: ColumnType::Simple(SimpleColumnType::Text),
987                nullable: true,
988                default: None,
989                comment: None,
990                primary_key: None,
991                unique: None,
992                index: None,
993                foreign_key: None,
994            }],
995            constraints: vec![],
996        }];
997
998        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
999        let sql = result
1000            .iter()
1001            .map(|q| q.build(backend))
1002            .collect::<Vec<String>>()
1003            .join("\n");
1004
1005        // Should use uq_table__name pattern
1006        let expected_name = format!("uq_user__{}", constraint_name);
1007        assert!(
1008            sql.contains(&expected_name),
1009            "Expected unique constraint name '{}' in SQL: {}",
1010            expected_name,
1011            sql
1012        );
1013
1014        with_settings!({ snapshot_suffix => format!("add_unique_custom_{}_{:?}", constraint_name, backend) }, {
1015            assert_snapshot!(sql);
1016        });
1017    }
1018
1019    #[rstest]
1020    #[case::add_unnamed_unique_single_column_postgres(
1021        DatabaseBackend::Postgres,
1022        vec!["email"]
1023    )]
1024    #[case::add_unnamed_unique_single_column_mysql(
1025        DatabaseBackend::MySql,
1026        vec!["email"]
1027    )]
1028    #[case::add_unnamed_unique_single_column_sqlite(
1029        DatabaseBackend::Sqlite,
1030        vec!["email"]
1031    )]
1032    #[case::add_unnamed_unique_multiple_columns_postgres(
1033        DatabaseBackend::Postgres,
1034        vec!["email", "username"]
1035    )]
1036    #[case::add_unnamed_unique_multiple_columns_mysql(
1037        DatabaseBackend::MySql,
1038        vec!["email", "username"]
1039    )]
1040    #[case::add_unnamed_unique_multiple_columns_sqlite(
1041        DatabaseBackend::Sqlite,
1042        vec!["email", "username"]
1043    )]
1044    fn test_add_unnamed_unique(#[case] backend: DatabaseBackend, #[case] columns: Vec<&str>) {
1045        // Test that unnamed unique constraints follow uq_table__col1_col2 pattern
1046        let action = MigrationAction::AddConstraint {
1047            table: "user".into(),
1048            constraint: TableConstraint::Unique {
1049                name: None,
1050                columns: columns.iter().map(|s| s.to_string()).collect(),
1051            },
1052        };
1053
1054        let schema_columns: Vec<ColumnDef> = columns
1055            .iter()
1056            .map(|col| ColumnDef {
1057                name: col.to_string(),
1058                r#type: ColumnType::Simple(SimpleColumnType::Text),
1059                nullable: true,
1060                default: None,
1061                comment: None,
1062                primary_key: None,
1063                unique: None,
1064                index: None,
1065                foreign_key: None,
1066            })
1067            .collect();
1068
1069        let current_schema = vec![TableDef {
1070            name: "user".into(),
1071            description: None,
1072            columns: schema_columns,
1073            constraints: vec![],
1074        }];
1075
1076        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
1077        let sql = result
1078            .iter()
1079            .map(|q| q.build(backend))
1080            .collect::<Vec<String>>()
1081            .join("\n");
1082
1083        // Should use uq_table__col1_col2... pattern
1084        let expected_name = format!("uq_user__{}", columns.join("_"));
1085        assert!(
1086            sql.contains(&expected_name),
1087            "Expected unique constraint name '{}' in SQL: {}",
1088            expected_name,
1089            sql
1090        );
1091
1092        with_settings!({ snapshot_suffix => format!("add_unnamed_unique_{}_{:?}", columns.join("_"), backend) }, {
1093            assert_snapshot!(sql);
1094        });
1095    }
1096
1097    #[rstest]
1098    #[case::remove_unique_with_custom_name_postgres(
1099        DatabaseBackend::Postgres,
1100        "email_unique",
1101        vec!["email"]
1102    )]
1103    #[case::remove_unique_with_custom_name_mysql(
1104        DatabaseBackend::MySql,
1105        "email_unique",
1106        vec!["email"]
1107    )]
1108    #[case::remove_unique_with_custom_name_sqlite(
1109        DatabaseBackend::Sqlite,
1110        "email_unique",
1111        vec!["email"]
1112    )]
1113    fn test_remove_unique_with_custom_name(
1114        #[case] backend: DatabaseBackend,
1115        #[case] constraint_name: &str,
1116        #[case] columns: Vec<&str>,
1117    ) {
1118        // Test that removing custom unique constraint uses uq_table__name pattern
1119        let constraint = TableConstraint::Unique {
1120            name: Some(constraint_name.into()),
1121            columns: columns.iter().map(|s| s.to_string()).collect(),
1122        };
1123
1124        let current_schema = vec![TableDef {
1125            name: "user".into(),
1126            description: None,
1127            columns: vec![ColumnDef {
1128                name: "email".into(),
1129                r#type: ColumnType::Simple(SimpleColumnType::Text),
1130                nullable: true,
1131                default: None,
1132                comment: None,
1133                primary_key: None,
1134                unique: None,
1135                index: None,
1136                foreign_key: None,
1137            }],
1138            constraints: vec![constraint.clone()],
1139        }];
1140
1141        let action = MigrationAction::RemoveConstraint {
1142            table: "user".into(),
1143            constraint,
1144        };
1145
1146        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
1147        let sql = result
1148            .iter()
1149            .map(|q| q.build(backend))
1150            .collect::<Vec<String>>()
1151            .join("\n");
1152
1153        // Should use uq_table__name pattern (for Postgres/MySQL, not SQLite which rebuilds table)
1154        if backend != DatabaseBackend::Sqlite {
1155            let expected_name = format!("uq_user__{}", constraint_name);
1156            assert!(
1157                sql.contains(&expected_name),
1158                "Expected unique constraint name '{}' in SQL: {}",
1159                expected_name,
1160                sql
1161            );
1162        }
1163
1164        with_settings!({ snapshot_suffix => format!("remove_unique_custom_{}_{:?}", constraint_name, backend) }, {
1165            assert_snapshot!(sql);
1166        });
1167    }
1168
1169    #[rstest]
1170    #[case::remove_unnamed_unique_single_column_postgres(
1171        DatabaseBackend::Postgres,
1172        vec!["email"]
1173    )]
1174    #[case::remove_unnamed_unique_single_column_mysql(
1175        DatabaseBackend::MySql,
1176        vec!["email"]
1177    )]
1178    #[case::remove_unnamed_unique_single_column_sqlite(
1179        DatabaseBackend::Sqlite,
1180        vec!["email"]
1181    )]
1182    #[case::remove_unnamed_unique_multiple_columns_postgres(
1183        DatabaseBackend::Postgres,
1184        vec!["email", "username"]
1185    )]
1186    #[case::remove_unnamed_unique_multiple_columns_mysql(
1187        DatabaseBackend::MySql,
1188        vec!["email", "username"]
1189    )]
1190    #[case::remove_unnamed_unique_multiple_columns_sqlite(
1191        DatabaseBackend::Sqlite,
1192        vec!["email", "username"]
1193    )]
1194    fn test_remove_unnamed_unique(#[case] backend: DatabaseBackend, #[case] columns: Vec<&str>) {
1195        // Test that removing unnamed unique constraints uses uq_table__col1_col2 pattern
1196        let constraint = TableConstraint::Unique {
1197            name: None,
1198            columns: columns.iter().map(|s| s.to_string()).collect(),
1199        };
1200
1201        let schema_columns: Vec<ColumnDef> = columns
1202            .iter()
1203            .map(|col| ColumnDef {
1204                name: col.to_string(),
1205                r#type: ColumnType::Simple(SimpleColumnType::Text),
1206                nullable: true,
1207                default: None,
1208                comment: None,
1209                primary_key: None,
1210                unique: None,
1211                index: None,
1212                foreign_key: None,
1213            })
1214            .collect();
1215
1216        let current_schema = vec![TableDef {
1217            name: "user".into(),
1218            description: None,
1219            columns: schema_columns,
1220            constraints: vec![constraint.clone()],
1221        }];
1222
1223        let action = MigrationAction::RemoveConstraint {
1224            table: "user".into(),
1225            constraint,
1226        };
1227
1228        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
1229        let sql = result
1230            .iter()
1231            .map(|q| q.build(backend))
1232            .collect::<Vec<String>>()
1233            .join("\n");
1234
1235        // Should use uq_table__col1_col2... pattern (for Postgres/MySQL, not SQLite which rebuilds table)
1236        if backend != DatabaseBackend::Sqlite {
1237            let expected_name = format!("uq_user__{}", columns.join("_"));
1238            assert!(
1239                sql.contains(&expected_name),
1240                "Expected unique constraint name '{}' in SQL: {}",
1241                expected_name,
1242                sql
1243            );
1244        }
1245
1246        with_settings!({ snapshot_suffix => format!("remove_unnamed_unique_{}_{:?}", columns.join("_"), backend) }, {
1247            assert_snapshot!(sql);
1248        });
1249    }
1250
1251    /// Test build_action_queries for ModifyColumnNullable
1252    #[rstest]
1253    #[case::postgres_modify_nullable(DatabaseBackend::Postgres)]
1254    #[case::mysql_modify_nullable(DatabaseBackend::MySql)]
1255    #[case::sqlite_modify_nullable(DatabaseBackend::Sqlite)]
1256    fn test_build_action_queries_modify_column_nullable(#[case] backend: DatabaseBackend) {
1257        let action = MigrationAction::ModifyColumnNullable {
1258            table: "users".into(),
1259            column: "email".into(),
1260            nullable: false,
1261            fill_with: Some("'unknown'".into()),
1262        };
1263        let current_schema = vec![TableDef {
1264            name: "users".into(),
1265            description: None,
1266            columns: vec![ColumnDef {
1267                name: "email".into(),
1268                r#type: ColumnType::Simple(SimpleColumnType::Text),
1269                nullable: true,
1270                default: None,
1271                comment: None,
1272                primary_key: None,
1273                unique: None,
1274                index: None,
1275                foreign_key: None,
1276            }],
1277            constraints: vec![],
1278        }];
1279        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
1280        assert!(!result.is_empty());
1281        let sql = result
1282            .iter()
1283            .map(|q| q.build(backend))
1284            .collect::<Vec<String>>()
1285            .join("\n");
1286
1287        // Should contain UPDATE for fill_with and ALTER for nullable change
1288        assert!(sql.contains("UPDATE"));
1289        assert!(sql.contains("unknown"));
1290
1291        let suffix = format!(
1292            "{}_modify_nullable",
1293            match backend {
1294                DatabaseBackend::Postgres => "postgres",
1295                DatabaseBackend::MySql => "mysql",
1296                DatabaseBackend::Sqlite => "sqlite",
1297            }
1298        );
1299
1300        with_settings!({ snapshot_suffix => suffix }, {
1301            assert_snapshot!(sql);
1302        });
1303    }
1304
1305    /// Test build_action_queries for ModifyColumnDefault
1306    #[rstest]
1307    #[case::postgres_modify_default(DatabaseBackend::Postgres)]
1308    #[case::mysql_modify_default(DatabaseBackend::MySql)]
1309    #[case::sqlite_modify_default(DatabaseBackend::Sqlite)]
1310    fn test_build_action_queries_modify_column_default(#[case] backend: DatabaseBackend) {
1311        let action = MigrationAction::ModifyColumnDefault {
1312            table: "users".into(),
1313            column: "status".into(),
1314            new_default: Some("'active'".into()),
1315        };
1316        let current_schema = vec![TableDef {
1317            name: "users".into(),
1318            description: None,
1319            columns: vec![ColumnDef {
1320                name: "status".into(),
1321                r#type: ColumnType::Simple(SimpleColumnType::Text),
1322                nullable: true,
1323                default: None,
1324                comment: None,
1325                primary_key: None,
1326                unique: None,
1327                index: None,
1328                foreign_key: None,
1329            }],
1330            constraints: vec![],
1331        }];
1332        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
1333        assert!(!result.is_empty());
1334        let sql = result
1335            .iter()
1336            .map(|q| q.build(backend))
1337            .collect::<Vec<String>>()
1338            .join("\n");
1339
1340        // Should contain DEFAULT and 'active'
1341        assert!(sql.contains("DEFAULT") || sql.contains("active"));
1342
1343        let suffix = format!(
1344            "{}_modify_default",
1345            match backend {
1346                DatabaseBackend::Postgres => "postgres",
1347                DatabaseBackend::MySql => "mysql",
1348                DatabaseBackend::Sqlite => "sqlite",
1349            }
1350        );
1351
1352        with_settings!({ snapshot_suffix => suffix }, {
1353            assert_snapshot!(sql);
1354        });
1355    }
1356
1357    /// Test build_action_queries for ModifyColumnComment
1358    #[rstest]
1359    #[case::postgres_modify_comment(DatabaseBackend::Postgres)]
1360    #[case::mysql_modify_comment(DatabaseBackend::MySql)]
1361    #[case::sqlite_modify_comment(DatabaseBackend::Sqlite)]
1362    fn test_build_action_queries_modify_column_comment(#[case] backend: DatabaseBackend) {
1363        let action = MigrationAction::ModifyColumnComment {
1364            table: "users".into(),
1365            column: "email".into(),
1366            new_comment: Some("User email address".into()),
1367        };
1368        let current_schema = vec![TableDef {
1369            name: "users".into(),
1370            description: None,
1371            columns: vec![ColumnDef {
1372                name: "email".into(),
1373                r#type: ColumnType::Simple(SimpleColumnType::Text),
1374                nullable: true,
1375                default: None,
1376                comment: None,
1377                primary_key: None,
1378                unique: None,
1379                index: None,
1380                foreign_key: None,
1381            }],
1382            constraints: vec![],
1383        }];
1384        let result = build_action_queries(&backend, &action, &current_schema).unwrap();
1385        let sql = result
1386            .iter()
1387            .map(|q| q.build(backend))
1388            .collect::<Vec<String>>()
1389            .join("\n");
1390
1391        // Postgres and MySQL should have comment, SQLite returns empty
1392        if backend != DatabaseBackend::Sqlite {
1393            assert!(sql.contains("COMMENT") || sql.contains("User email address"));
1394        }
1395
1396        let suffix = format!(
1397            "{}_modify_comment",
1398            match backend {
1399                DatabaseBackend::Postgres => "postgres",
1400                DatabaseBackend::MySql => "mysql",
1401                DatabaseBackend::Sqlite => "sqlite",
1402            }
1403        );
1404
1405        with_settings!({ snapshot_suffix => suffix }, {
1406            assert_snapshot!(sql);
1407        });
1408    }
1409
1410    #[rstest]
1411    #[case::create_table_func_default_postgres(DatabaseBackend::Postgres)]
1412    #[case::create_table_func_default_mysql(DatabaseBackend::MySql)]
1413    #[case::create_table_func_default_sqlite(DatabaseBackend::Sqlite)]
1414    fn test_create_table_with_function_default(#[case] backend: DatabaseBackend) {
1415        // SQLite requires DEFAULT (expr) for function-call defaults.
1416        // This test ensures parentheses are added for SQLite.
1417        let action = MigrationAction::CreateTable {
1418            table: "users".into(),
1419            columns: vec![
1420                ColumnDef {
1421                    name: "id".into(),
1422                    r#type: ColumnType::Simple(SimpleColumnType::Uuid),
1423                    nullable: false,
1424                    default: Some("gen_random_uuid()".into()),
1425                    comment: None,
1426                    primary_key: None,
1427                    unique: None,
1428                    index: None,
1429                    foreign_key: None,
1430                },
1431                ColumnDef {
1432                    name: "created_at".into(),
1433                    r#type: ColumnType::Simple(SimpleColumnType::Timestamptz),
1434                    nullable: false,
1435                    default: Some("now()".into()),
1436                    comment: None,
1437                    primary_key: None,
1438                    unique: None,
1439                    index: None,
1440                    foreign_key: None,
1441                },
1442            ],
1443            constraints: vec![],
1444        };
1445        let result = build_action_queries(&backend, &action, &[]).unwrap();
1446        let sql = result
1447            .iter()
1448            .map(|q| q.build(backend))
1449            .collect::<Vec<_>>()
1450            .join(";\n");
1451
1452        with_settings!({ snapshot_suffix => format!("create_table_func_default_{:?}", backend) }, {
1453            assert_snapshot!(sql);
1454        });
1455    }
1456
1457    #[rstest]
1458    #[case::delete_enum_column_postgres(DatabaseBackend::Postgres)]
1459    #[case::delete_enum_column_mysql(DatabaseBackend::MySql)]
1460    #[case::delete_enum_column_sqlite(DatabaseBackend::Sqlite)]
1461    fn test_delete_column_with_enum_type(#[case] backend: DatabaseBackend) {
1462        // Deleting a column with an enum type — SQLite uses temp table approach,
1463        // Postgres drops the enum type, MySQL uses simple DROP COLUMN.
1464        let action = MigrationAction::DeleteColumn {
1465            table: "orders".into(),
1466            column: "status".into(),
1467        };
1468        let schema = vec![TableDef {
1469            name: "orders".into(),
1470            description: None,
1471            columns: vec![
1472                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1473                ColumnDef {
1474                    name: "status".into(),
1475                    r#type: ColumnType::Complex(vespertide_core::ComplexColumnType::Enum {
1476                        name: "order_status".into(),
1477                        values: vespertide_core::EnumValues::String(vec![
1478                            "pending".into(),
1479                            "shipped".into(),
1480                        ]),
1481                    }),
1482                    nullable: false,
1483                    default: None,
1484                    comment: None,
1485                    primary_key: None,
1486                    unique: None,
1487                    index: None,
1488                    foreign_key: None,
1489                },
1490            ],
1491            constraints: vec![],
1492        }];
1493        let result = build_action_queries(&backend, &action, &schema).unwrap();
1494        let sql = result
1495            .iter()
1496            .map(|q| q.build(backend))
1497            .collect::<Vec<_>>()
1498            .join(";\n");
1499
1500        with_settings!({ snapshot_suffix => format!("delete_enum_column_{:?}", backend) }, {
1501            assert_snapshot!(sql);
1502        });
1503    }
1504}