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