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