Skip to main content

vespertide_query/sql/
remove_constraint.rs

1use sea_query::{Alias, ForeignKey, Query, Table};
2
3use vespertide_core::{TableConstraint, TableDef};
4
5use super::helpers::{build_sqlite_temp_table_create, recreate_indexes_after_rebuild};
6use super::rename_table::build_rename_table;
7use super::types::{BuiltQuery, DatabaseBackend};
8use crate::error::QueryError;
9use crate::sql::RawSql;
10
11pub fn build_remove_constraint(
12    backend: &DatabaseBackend,
13    table: &str,
14    constraint: &TableConstraint,
15    current_schema: &[TableDef],
16    pending_constraints: &[TableConstraint],
17) -> Result<Vec<BuiltQuery>, QueryError> {
18    match constraint {
19        TableConstraint::PrimaryKey { .. } => {
20            if *backend == DatabaseBackend::Sqlite {
21                // SQLite does not support dropping primary key constraints, use temp table approach
22                let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to remove constraints.", table)))?;
23
24                // Remove the primary key constraint
25                let mut new_constraints = table_def.constraints.clone();
26                new_constraints.retain(|c| !matches!(c, TableConstraint::PrimaryKey { .. }));
27
28                let temp_table = format!("{}_temp", table);
29
30                // 1. Create temporary table without primary key constraint + CHECK constraints
31                let create_query = build_sqlite_temp_table_create(
32                    backend,
33                    &temp_table,
34                    table,
35                    &table_def.columns,
36                    &new_constraints,
37                );
38
39                // 2. Copy data (all columns)
40                let column_aliases: Vec<Alias> = table_def
41                    .columns
42                    .iter()
43                    .map(|c| Alias::new(&c.name))
44                    .collect();
45                let mut select_query = Query::select();
46                for col_alias in &column_aliases {
47                    select_query = select_query.column(col_alias.clone()).to_owned();
48                }
49                select_query = select_query.from(Alias::new(table)).to_owned();
50
51                let insert_stmt = Query::insert()
52                    .into_table(Alias::new(&temp_table))
53                    .columns(column_aliases.clone())
54                    .select_from(select_query)
55                    .unwrap()
56                    .to_owned();
57                let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
58
59                // 3. Drop original table
60                let drop_table = Table::drop().table(Alias::new(table)).to_owned();
61                let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
62
63                // 4. Rename temporary table to original name
64                let rename_query = build_rename_table(&temp_table, table);
65
66                // 5. Recreate indexes (both regular and UNIQUE)
67                let index_queries = recreate_indexes_after_rebuild(
68                    table,
69                    &table_def.constraints,
70                    pending_constraints,
71                );
72
73                let mut queries = vec![create_query, insert_query, drop_query, rename_query];
74                queries.extend(index_queries);
75                Ok(queries)
76            } else {
77                // Other backends: use raw SQL
78                let pg_sql = format!(
79                    "ALTER TABLE \"{}\" DROP CONSTRAINT \"{}_pkey\"",
80                    table, table
81                );
82                let mysql_sql = format!("ALTER TABLE `{}` DROP PRIMARY KEY", table);
83                Ok(vec![BuiltQuery::Raw(RawSql::per_backend(
84                    pg_sql.clone(),
85                    mysql_sql,
86                    pg_sql,
87                ))])
88            }
89        }
90        TableConstraint::Unique { name, columns } => {
91            // SQLite does not support ALTER TABLE ... DROP CONSTRAINT UNIQUE
92            if *backend == DatabaseBackend::Sqlite {
93                // Use temporary table approach for SQLite
94                let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to remove constraints.", table)))?;
95
96                // Create new constraints without the removed unique constraint
97                let mut new_constraints = table_def.constraints.clone();
98                new_constraints.retain(|c| {
99                    match (c, constraint) {
100                        (
101                            TableConstraint::Unique {
102                                name: c_name,
103                                columns: c_cols,
104                            },
105                            TableConstraint::Unique {
106                                name: r_name,
107                                columns: r_cols,
108                            },
109                        ) => {
110                            // Remove if names match, or if no name and columns match
111                            if let (Some(cn), Some(rn)) = (c_name, r_name) {
112                                cn != rn
113                            } else {
114                                c_cols != r_cols
115                            }
116                        }
117                        _ => true,
118                    }
119                });
120
121                let temp_table = format!("{}_temp", table);
122
123                // 1. Create temporary table without the removed constraint + CHECK constraints
124                let create_query = build_sqlite_temp_table_create(
125                    backend,
126                    &temp_table,
127                    table,
128                    &table_def.columns,
129                    &new_constraints,
130                );
131
132                // 2. Copy data (all columns)
133                let column_aliases: Vec<Alias> = table_def
134                    .columns
135                    .iter()
136                    .map(|c| Alias::new(&c.name))
137                    .collect();
138                let mut select_query = Query::select();
139                for col_alias in &column_aliases {
140                    select_query = select_query.column(col_alias.clone()).to_owned();
141                }
142                select_query = select_query.from(Alias::new(table)).to_owned();
143
144                let insert_stmt = Query::insert()
145                    .into_table(Alias::new(&temp_table))
146                    .columns(column_aliases.clone())
147                    .select_from(select_query)
148                    .unwrap()
149                    .to_owned();
150                let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
151
152                // 3. Drop original table
153                let drop_table = Table::drop().table(Alias::new(table)).to_owned();
154                let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
155
156                // 4. Rename temporary table to original name
157                let rename_query = build_rename_table(&temp_table, table);
158
159                // 5. Recreate indexes (both regular and UNIQUE, from new_constraints which has the unique removed)
160                let index_queries =
161                    recreate_indexes_after_rebuild(table, &new_constraints, pending_constraints);
162
163                let mut queries = vec![create_query, insert_query, drop_query, rename_query];
164                queries.extend(index_queries);
165                Ok(queries)
166            } else {
167                // For unique constraints created via CREATE UNIQUE INDEX (PostgreSQL/SQLite),
168                // we use DROP INDEX. MySQL uses ALTER TABLE DROP INDEX.
169                let constraint_name = vespertide_naming::build_unique_constraint_name(
170                    table,
171                    columns,
172                    name.as_deref(),
173                );
174                // PostgreSQL and SQLite: DROP INDEX (matches how we create them)
175                // MySQL: ALTER TABLE DROP INDEX
176                let pg_sql = format!("DROP INDEX \"{}\"", constraint_name);
177                let mysql_sql = format!("ALTER TABLE `{}` DROP INDEX `{}`", table, constraint_name);
178                let sqlite_sql = format!("DROP INDEX \"{}\"", constraint_name);
179                Ok(vec![BuiltQuery::Raw(RawSql::per_backend(
180                    pg_sql, mysql_sql, sqlite_sql,
181                ))])
182            }
183        }
184        TableConstraint::ForeignKey { name, columns, .. } => {
185            // SQLite does not support ALTER TABLE ... DROP CONSTRAINT FOREIGN KEY
186            if *backend == DatabaseBackend::Sqlite {
187                // Use temporary table approach for SQLite
188                let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to remove constraints.", table)))?;
189
190                // Create new constraints without the removed foreign key constraint
191                let mut new_constraints = table_def.constraints.clone();
192                new_constraints.retain(|c| {
193                    match (c, constraint) {
194                        (
195                            TableConstraint::ForeignKey {
196                                name: c_name,
197                                columns: c_cols,
198                                ..
199                            },
200                            TableConstraint::ForeignKey {
201                                name: r_name,
202                                columns: r_cols,
203                                ..
204                            },
205                        ) => {
206                            // Remove if names match, or if no name and columns match
207                            if let (Some(cn), Some(rn)) = (c_name, r_name) {
208                                cn != rn
209                            } else {
210                                c_cols != r_cols
211                            }
212                        }
213                        _ => true,
214                    }
215                });
216
217                let temp_table = format!("{}_temp", table);
218
219                // 1. Create temporary table without the removed constraint + CHECK constraints
220                let create_query = build_sqlite_temp_table_create(
221                    backend,
222                    &temp_table,
223                    table,
224                    &table_def.columns,
225                    &new_constraints,
226                );
227
228                // 2. Copy data (all columns)
229                let column_aliases: Vec<Alias> = table_def
230                    .columns
231                    .iter()
232                    .map(|c| Alias::new(&c.name))
233                    .collect();
234                let mut select_query = Query::select();
235                for col_alias in &column_aliases {
236                    select_query = select_query.column(col_alias.clone()).to_owned();
237                }
238                select_query = select_query.from(Alias::new(table)).to_owned();
239
240                let insert_stmt = Query::insert()
241                    .into_table(Alias::new(&temp_table))
242                    .columns(column_aliases.clone())
243                    .select_from(select_query)
244                    .unwrap()
245                    .to_owned();
246                let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
247
248                // 3. Drop original table
249                let drop_table = Table::drop().table(Alias::new(table)).to_owned();
250                let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
251
252                // 4. Rename temporary table to original name
253                let rename_query = build_rename_table(&temp_table, table);
254
255                // 5. Recreate indexes (both regular and UNIQUE)
256                let index_queries = recreate_indexes_after_rebuild(
257                    table,
258                    &table_def.constraints,
259                    pending_constraints,
260                );
261
262                let mut queries = vec![create_query, insert_query, drop_query, rename_query];
263                queries.extend(index_queries);
264                Ok(queries)
265            } else {
266                // Build foreign key drop using ForeignKey::drop()
267                let constraint_name =
268                    vespertide_naming::build_foreign_key_name(table, columns, name.as_deref());
269                let fk_drop = ForeignKey::drop()
270                    .name(&constraint_name)
271                    .table(Alias::new(table))
272                    .to_owned();
273                Ok(vec![BuiltQuery::DropForeignKey(Box::new(fk_drop))])
274            }
275        }
276        TableConstraint::Index { name, columns } => {
277            // Index constraints are simple DROP INDEX statements for all backends
278            let index_name = if let Some(n) = name {
279                // Use naming convention for named indexes
280                vespertide_naming::build_index_name(table, columns, Some(n))
281            } else {
282                // Generate name from table and columns for unnamed indexes
283                vespertide_naming::build_index_name(table, columns, None)
284            };
285            let idx_drop = sea_query::Index::drop()
286                .table(Alias::new(table))
287                .name(&index_name)
288                .to_owned();
289            Ok(vec![BuiltQuery::DropIndex(Box::new(idx_drop))])
290        }
291        TableConstraint::Check { name, .. } => {
292            // SQLite does not support ALTER TABLE ... DROP CONSTRAINT CHECK
293            if *backend == DatabaseBackend::Sqlite {
294                // Use temporary table approach for SQLite
295                let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to remove constraints.", table)))?;
296
297                // Create new constraints without the removed check constraint
298                let mut new_constraints = table_def.constraints.clone();
299                new_constraints.retain(|c| match (c, constraint) {
300                    (
301                        TableConstraint::Check { name: c_name, .. },
302                        TableConstraint::Check { name: r_name, .. },
303                    ) => c_name != r_name,
304                    _ => true,
305                });
306
307                let temp_table = format!("{}_temp", table);
308
309                // 1. Create temporary table without the removed constraint + remaining CHECK constraints
310                let create_query = build_sqlite_temp_table_create(
311                    backend,
312                    &temp_table,
313                    table,
314                    &table_def.columns,
315                    &new_constraints,
316                );
317
318                // 2. Copy data (all columns)
319                let column_aliases: Vec<Alias> = table_def
320                    .columns
321                    .iter()
322                    .map(|c| Alias::new(&c.name))
323                    .collect();
324                let mut select_query = Query::select();
325                for col_alias in &column_aliases {
326                    select_query = select_query.column(col_alias.clone()).to_owned();
327                }
328                select_query = select_query.from(Alias::new(table)).to_owned();
329
330                let insert_stmt = Query::insert()
331                    .into_table(Alias::new(&temp_table))
332                    .columns(column_aliases.clone())
333                    .select_from(select_query)
334                    .unwrap()
335                    .to_owned();
336                let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
337
338                // 3. Drop original table
339                let drop_table = Table::drop().table(Alias::new(table)).to_owned();
340                let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
341
342                // 4. Rename temporary table to original name
343                let rename_query = build_rename_table(&temp_table, table);
344
345                // 5. Recreate indexes (both regular and UNIQUE)
346                let index_queries = recreate_indexes_after_rebuild(
347                    table,
348                    &table_def.constraints,
349                    pending_constraints,
350                );
351
352                let mut queries = vec![create_query, insert_query, drop_query, rename_query];
353                queries.extend(index_queries);
354                Ok(queries)
355            } else {
356                let pg_sql = format!("ALTER TABLE \"{}\" DROP CONSTRAINT \"{}\"", table, name);
357                let mysql_sql = format!("ALTER TABLE `{}` DROP CHECK `{}`", table, name);
358                Ok(vec![BuiltQuery::Raw(RawSql::per_backend(
359                    pg_sql.clone(),
360                    mysql_sql,
361                    pg_sql,
362                ))])
363            }
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use crate::sql::types::DatabaseBackend;
372    use insta::{assert_snapshot, with_settings};
373    use rstest::rstest;
374    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint, TableDef};
375
376    #[rstest]
377    #[case::remove_constraint_primary_key_postgres(
378        "remove_constraint_primary_key_postgres",
379        DatabaseBackend::Postgres,
380        &["DROP CONSTRAINT \"users_pkey\""]
381    )]
382    #[case::remove_constraint_primary_key_mysql(
383        "remove_constraint_primary_key_mysql",
384        DatabaseBackend::MySql,
385        &["DROP PRIMARY KEY"]
386    )]
387    #[case::remove_constraint_primary_key_sqlite(
388        "remove_constraint_primary_key_sqlite",
389        DatabaseBackend::Sqlite,
390        &["CREATE TABLE \"users_temp\""]
391    )]
392    #[case::remove_constraint_unique_named_postgres(
393        "remove_constraint_unique_named_postgres",
394        DatabaseBackend::Postgres,
395        &["DROP INDEX \"uq_users__uq_email\""]
396    )]
397    #[case::remove_constraint_unique_named_mysql(
398        "remove_constraint_unique_named_mysql",
399        DatabaseBackend::MySql,
400        &["DROP INDEX `uq_users__uq_email`"]
401    )]
402    #[case::remove_constraint_unique_named_sqlite(
403        "remove_constraint_unique_named_sqlite",
404        DatabaseBackend::Sqlite,
405        &["CREATE TABLE \"users_temp\""]
406    )]
407    #[case::remove_constraint_foreign_key_named_postgres(
408        "remove_constraint_foreign_key_named_postgres",
409        DatabaseBackend::Postgres,
410        &["DROP CONSTRAINT \"fk_users__fk_user\""]
411    )]
412    #[case::remove_constraint_foreign_key_named_mysql(
413        "remove_constraint_foreign_key_named_mysql",
414        DatabaseBackend::MySql,
415        &["DROP FOREIGN KEY `fk_users__fk_user`"]
416    )]
417    #[case::remove_constraint_foreign_key_named_sqlite(
418        "remove_constraint_foreign_key_named_sqlite",
419        DatabaseBackend::Sqlite,
420        &["CREATE TABLE \"users_temp\""]
421    )]
422    #[case::remove_constraint_check_named_postgres(
423        "remove_constraint_check_named_postgres",
424        DatabaseBackend::Postgres,
425        &["DROP CONSTRAINT \"chk_age\""]
426    )]
427    #[case::remove_constraint_check_named_mysql(
428        "remove_constraint_check_named_mysql",
429        DatabaseBackend::MySql,
430        &["DROP CHECK `chk_age`"]
431    )]
432    #[case::remove_constraint_check_named_sqlite(
433        "remove_constraint_check_named_sqlite",
434        DatabaseBackend::Sqlite,
435        &["CREATE TABLE \"users_temp\""]
436    )]
437    fn test_remove_constraint(
438        #[case] title: &str,
439        #[case] backend: DatabaseBackend,
440        #[case] expected: &[&str],
441    ) {
442        let constraint = if title.contains("primary_key") {
443            TableConstraint::PrimaryKey {
444                columns: vec!["id".into()],
445                auto_increment: false,
446            }
447        } else if title.contains("unique") {
448            TableConstraint::Unique {
449                name: Some("uq_email".into()),
450                columns: vec!["email".into()],
451            }
452        } else if title.contains("foreign_key") {
453            TableConstraint::ForeignKey {
454                name: Some("fk_user".into()),
455                columns: vec!["user_id".into()],
456                ref_table: "users".into(),
457                ref_columns: vec!["id".into()],
458                on_delete: None,
459                on_update: None,
460            }
461        } else {
462            TableConstraint::Check {
463                name: "chk_age".into(),
464                expr: "age > 0".into(),
465            }
466        };
467
468        // For SQLite, we need to provide current schema with the constraint to be removed
469        let current_schema = vec![TableDef {
470            name: "users".into(),
471            description: None,
472            columns: if title.contains("check") {
473                vec![
474                    ColumnDef {
475                        name: "id".into(),
476                        r#type: ColumnType::Simple(SimpleColumnType::Integer),
477                        nullable: false,
478                        default: None,
479                        comment: None,
480                        primary_key: None,
481                        unique: None,
482                        index: None,
483                        foreign_key: None,
484                    },
485                    ColumnDef {
486                        name: "age".into(),
487                        r#type: ColumnType::Simple(SimpleColumnType::Integer),
488                        nullable: true,
489                        default: None,
490                        comment: None,
491                        primary_key: None,
492                        unique: None,
493                        index: None,
494                        foreign_key: None,
495                    },
496                ]
497            } else if title.contains("foreign_key") {
498                vec![
499                    ColumnDef {
500                        name: "id".into(),
501                        r#type: ColumnType::Simple(SimpleColumnType::Integer),
502                        nullable: false,
503                        default: None,
504                        comment: None,
505                        primary_key: None,
506                        unique: None,
507                        index: None,
508                        foreign_key: None,
509                    },
510                    ColumnDef {
511                        name: "user_id".into(),
512                        r#type: ColumnType::Simple(SimpleColumnType::Integer),
513                        nullable: true,
514                        default: None,
515                        comment: None,
516                        primary_key: None,
517                        unique: None,
518                        index: None,
519                        foreign_key: None,
520                    },
521                ]
522            } else {
523                // primary key / unique cases
524                vec![ColumnDef {
525                    name: "id".into(),
526                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
527                    nullable: false,
528                    default: None,
529                    comment: None,
530                    primary_key: None,
531                    unique: None,
532                    index: None,
533                    foreign_key: None,
534                }]
535            },
536            constraints: vec![constraint.clone()],
537        }];
538
539        let result =
540            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
541        let sql = result[0].build(backend);
542        for exp in expected {
543            assert!(
544                sql.contains(exp),
545                "Expected SQL to contain '{}', got: {}",
546                exp,
547                sql
548            );
549        }
550
551        with_settings!({ snapshot_suffix => format!("remove_constraint_{}", title) }, {
552            assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::<Vec<String>>().join("\n"));
553        });
554    }
555
556    #[test]
557    fn test_remove_constraint_primary_key_sqlite_table_not_found() {
558        // Test error when table is not found (line 25)
559        let constraint = TableConstraint::PrimaryKey {
560            columns: vec!["id".into()],
561            auto_increment: false,
562        };
563        let result = build_remove_constraint(
564            &DatabaseBackend::Sqlite,
565            "nonexistent_table",
566            &constraint,
567            &[], // Empty schema
568            &[],
569        );
570        assert!(result.is_err());
571        let err_msg = result.unwrap_err().to_string();
572        assert!(err_msg.contains("Table 'nonexistent_table' not found in current schema"));
573    }
574
575    #[rstest]
576    #[case::remove_primary_key_with_index_postgres(DatabaseBackend::Postgres)]
577    #[case::remove_primary_key_with_index_mysql(DatabaseBackend::MySql)]
578    #[case::remove_primary_key_with_index_sqlite(DatabaseBackend::Sqlite)]
579    fn test_remove_constraint_primary_key_with_index(#[case] backend: DatabaseBackend) {
580        // Test PrimaryKey removal with indexes
581        let constraint = TableConstraint::PrimaryKey {
582            columns: vec!["id".into()],
583            auto_increment: false,
584        };
585        let current_schema = vec![TableDef {
586            name: "users".into(),
587            description: None,
588            columns: vec![ColumnDef {
589                name: "id".into(),
590                r#type: ColumnType::Simple(SimpleColumnType::Integer),
591                nullable: false,
592                default: None,
593                comment: None,
594                primary_key: None,
595                unique: None,
596                index: None,
597                foreign_key: None,
598            }],
599            constraints: vec![
600                constraint.clone(),
601                TableConstraint::Index {
602                    name: Some("idx_id".into()),
603                    columns: vec!["id".into()],
604                },
605            ],
606        }];
607
608        let result =
609            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
610        let sql = result
611            .iter()
612            .map(|q| q.build(backend))
613            .collect::<Vec<String>>()
614            .join("\n");
615
616        if matches!(backend, DatabaseBackend::Sqlite) {
617            assert!(sql.contains("CREATE INDEX"));
618            assert!(sql.contains("ix_users__idx_id"));
619        }
620
621        with_settings!({ snapshot_suffix => format!("remove_primary_key_with_index_{:?}", backend) }, {
622            assert_snapshot!(sql);
623        });
624    }
625
626    #[rstest]
627    #[case::remove_primary_key_with_unique_constraint_postgres(DatabaseBackend::Postgres)]
628    #[case::remove_primary_key_with_unique_constraint_mysql(DatabaseBackend::MySql)]
629    #[case::remove_primary_key_with_unique_constraint_sqlite(DatabaseBackend::Sqlite)]
630    fn test_remove_constraint_primary_key_with_unique_constraint(#[case] backend: DatabaseBackend) {
631        // Test PrimaryKey removal with unique constraint
632        let constraint = TableConstraint::PrimaryKey {
633            columns: vec!["id".into()],
634            auto_increment: false,
635        };
636        let current_schema = vec![TableDef {
637            name: "users".into(),
638            description: None,
639            columns: vec![ColumnDef {
640                name: "id".into(),
641                r#type: ColumnType::Simple(SimpleColumnType::Integer),
642                nullable: false,
643                default: None,
644                comment: None,
645                primary_key: None,
646                unique: None,
647                index: None,
648                foreign_key: None,
649            }],
650            constraints: vec![
651                constraint.clone(),
652                TableConstraint::Unique {
653                    name: Some("uq_email".into()),
654                    columns: vec!["email".into()],
655                },
656            ],
657        }];
658
659        let result =
660            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
661        let sql = result
662            .iter()
663            .map(|q| q.build(backend))
664            .collect::<Vec<String>>()
665            .join("\n");
666
667        if matches!(backend, DatabaseBackend::Sqlite) {
668            // Unique constraint should be in the temp table definition
669            assert!(sql.contains("CREATE TABLE"));
670        }
671
672        with_settings!({ snapshot_suffix => format!("remove_primary_key_with_unique_constraint_{:?}", backend) }, {
673            assert_snapshot!(sql);
674        });
675    }
676
677    #[test]
678    fn test_remove_constraint_unique_sqlite_table_not_found() {
679        // Test error when table is not found (line 112)
680        let constraint = TableConstraint::Unique {
681            name: Some("uq_email".into()),
682            columns: vec!["email".into()],
683        };
684        let result = build_remove_constraint(
685            &DatabaseBackend::Sqlite,
686            "nonexistent_table",
687            &constraint,
688            &[], // Empty schema
689            &[],
690        );
691        assert!(result.is_err());
692        let err_msg = result.unwrap_err().to_string();
693        assert!(err_msg.contains("Table 'nonexistent_table' not found in current schema"));
694    }
695
696    #[rstest]
697    #[case::remove_unique_without_name_postgres(DatabaseBackend::Postgres)]
698    #[case::remove_unique_without_name_mysql(DatabaseBackend::MySql)]
699    #[case::remove_unique_without_name_sqlite(DatabaseBackend::Sqlite)]
700    fn test_remove_constraint_unique_without_name(#[case] backend: DatabaseBackend) {
701        // Test Unique removal without name (lines 134, 137, 210)
702        let constraint = TableConstraint::Unique {
703            name: None,
704            columns: vec!["email".into()],
705        };
706        let current_schema = vec![TableDef {
707            name: "users".into(),
708            description: None,
709            columns: vec![
710                ColumnDef {
711                    name: "id".into(),
712                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
713                    nullable: false,
714                    default: None,
715                    comment: None,
716                    primary_key: None,
717                    unique: None,
718                    index: None,
719                    foreign_key: None,
720                },
721                ColumnDef {
722                    name: "email".into(),
723                    r#type: ColumnType::Simple(SimpleColumnType::Text),
724                    nullable: true,
725                    default: None,
726                    comment: None,
727                    primary_key: None,
728                    unique: None,
729                    index: None,
730                    foreign_key: None,
731                },
732            ],
733            constraints: vec![constraint.clone()],
734        }];
735
736        let result =
737            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
738        let sql = result
739            .iter()
740            .map(|q| q.build(backend))
741            .collect::<Vec<String>>()
742            .join("\n");
743
744        // Should generate default constraint name
745        if !matches!(backend, DatabaseBackend::Sqlite) {
746            assert!(sql.contains("users_email_key") || sql.contains("email"));
747        }
748
749        with_settings!({ snapshot_suffix => format!("remove_unique_without_name_{:?}", backend) }, {
750            assert_snapshot!(sql);
751        });
752    }
753
754    #[rstest]
755    #[case::remove_unique_with_index_postgres(DatabaseBackend::Postgres)]
756    #[case::remove_unique_with_index_mysql(DatabaseBackend::MySql)]
757    #[case::remove_unique_with_index_sqlite(DatabaseBackend::Sqlite)]
758    fn test_remove_constraint_unique_with_index(#[case] backend: DatabaseBackend) {
759        // Test Unique removal with indexes
760        let constraint = TableConstraint::Unique {
761            name: Some("uq_email".into()),
762            columns: vec!["email".into()],
763        };
764        let current_schema = vec![TableDef {
765            name: "users".into(),
766            description: None,
767            columns: vec![
768                ColumnDef {
769                    name: "id".into(),
770                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
771                    nullable: false,
772                    default: None,
773                    comment: None,
774                    primary_key: None,
775                    unique: None,
776                    index: None,
777                    foreign_key: None,
778                },
779                ColumnDef {
780                    name: "email".into(),
781                    r#type: ColumnType::Simple(SimpleColumnType::Text),
782                    nullable: true,
783                    default: None,
784                    comment: None,
785                    primary_key: None,
786                    unique: None,
787                    index: None,
788                    foreign_key: None,
789                },
790            ],
791            constraints: vec![
792                constraint.clone(),
793                TableConstraint::Index {
794                    name: Some("idx_id".into()),
795                    columns: vec!["id".into()],
796                },
797            ],
798        }];
799
800        let result =
801            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
802        let sql = result
803            .iter()
804            .map(|q| q.build(backend))
805            .collect::<Vec<String>>()
806            .join("\n");
807
808        if matches!(backend, DatabaseBackend::Sqlite) {
809            assert!(sql.contains("CREATE INDEX"));
810            assert!(sql.contains("ix_users__idx_id"));
811        }
812
813        with_settings!({ snapshot_suffix => format!("remove_unique_with_index_{:?}", backend) }, {
814            assert_snapshot!(sql);
815        });
816    }
817
818    #[rstest]
819    #[case::remove_unique_with_other_unique_constraint_postgres(DatabaseBackend::Postgres)]
820    #[case::remove_unique_with_other_unique_constraint_mysql(DatabaseBackend::MySql)]
821    #[case::remove_unique_with_other_unique_constraint_sqlite(DatabaseBackend::Sqlite)]
822    fn test_remove_constraint_unique_with_other_unique_constraint(
823        #[case] backend: DatabaseBackend,
824    ) {
825        // Test Unique removal with another unique constraint
826        let constraint = TableConstraint::Unique {
827            name: Some("uq_email".into()),
828            columns: vec!["email".into()],
829        };
830        let current_schema = vec![TableDef {
831            name: "users".into(),
832            description: None,
833            columns: vec![
834                ColumnDef {
835                    name: "id".into(),
836                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
837                    nullable: false,
838                    default: None,
839                    comment: None,
840                    primary_key: None,
841                    unique: None,
842                    index: None,
843                    foreign_key: None,
844                },
845                ColumnDef {
846                    name: "email".into(),
847                    r#type: ColumnType::Simple(SimpleColumnType::Text),
848                    nullable: true,
849                    default: None,
850                    comment: None,
851                    primary_key: None,
852                    unique: None,
853                    index: None,
854                    foreign_key: None,
855                },
856            ],
857            constraints: vec![
858                constraint.clone(),
859                TableConstraint::Unique {
860                    name: Some("uq_name".into()),
861                    columns: vec!["name".into()],
862                },
863            ],
864        }];
865
866        let result =
867            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
868        let sql = result
869            .iter()
870            .map(|q| q.build(backend))
871            .collect::<Vec<String>>()
872            .join("\n");
873
874        if matches!(backend, DatabaseBackend::Sqlite) {
875            // The remaining unique constraint should be preserved
876            assert!(sql.contains("CREATE TABLE"));
877        }
878
879        with_settings!({ snapshot_suffix => format!("remove_unique_with_other_unique_constraint_{:?}", backend) }, {
880            assert_snapshot!(sql);
881        });
882    }
883
884    #[test]
885    fn test_remove_constraint_foreign_key_sqlite_table_not_found() {
886        // Test error when table is not found (line 236)
887        let constraint = TableConstraint::ForeignKey {
888            name: Some("fk_user".into()),
889            columns: vec!["user_id".into()],
890            ref_table: "users".into(),
891            ref_columns: vec!["id".into()],
892            on_delete: None,
893            on_update: None,
894        };
895        let result = build_remove_constraint(
896            &DatabaseBackend::Sqlite,
897            "nonexistent_table",
898            &constraint,
899            &[], // Empty schema
900            &[],
901        );
902        assert!(result.is_err());
903        let err_msg = result.unwrap_err().to_string();
904        assert!(err_msg.contains("Table 'nonexistent_table' not found in current schema"));
905    }
906
907    #[rstest]
908    #[case::remove_foreign_key_without_name_postgres(DatabaseBackend::Postgres)]
909    #[case::remove_foreign_key_without_name_mysql(DatabaseBackend::MySql)]
910    #[case::remove_foreign_key_without_name_sqlite(DatabaseBackend::Sqlite)]
911    fn test_remove_constraint_foreign_key_without_name(#[case] backend: DatabaseBackend) {
912        // Test ForeignKey removal without name (lines 260, 263, 329)
913        let constraint = TableConstraint::ForeignKey {
914            name: None,
915            columns: vec!["user_id".into()],
916            ref_table: "users".into(),
917            ref_columns: vec!["id".into()],
918            on_delete: None,
919            on_update: None,
920        };
921        let current_schema = vec![TableDef {
922            name: "posts".into(),
923            description: None,
924            columns: vec![
925                ColumnDef {
926                    name: "id".into(),
927                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
928                    nullable: false,
929                    default: None,
930                    comment: None,
931                    primary_key: None,
932                    unique: None,
933                    index: None,
934                    foreign_key: None,
935                },
936                ColumnDef {
937                    name: "user_id".into(),
938                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
939                    nullable: true,
940                    default: None,
941                    comment: None,
942                    primary_key: None,
943                    unique: None,
944                    index: None,
945                    foreign_key: None,
946                },
947            ],
948            constraints: vec![constraint.clone()],
949        }];
950
951        let result =
952            build_remove_constraint(&backend, "posts", &constraint, &current_schema, &[]).unwrap();
953        let sql = result
954            .iter()
955            .map(|q| q.build(backend))
956            .collect::<Vec<String>>()
957            .join("\n");
958
959        // Should generate default constraint name
960        if !matches!(backend, DatabaseBackend::Sqlite) {
961            assert!(sql.contains("posts_user_id_fkey") || sql.contains("user_id"));
962        }
963
964        with_settings!({ snapshot_suffix => format!("remove_foreign_key_without_name_{:?}", backend) }, {
965            assert_snapshot!(sql);
966        });
967    }
968
969    #[rstest]
970    #[case::remove_foreign_key_with_index_postgres(DatabaseBackend::Postgres)]
971    #[case::remove_foreign_key_with_index_mysql(DatabaseBackend::MySql)]
972    #[case::remove_foreign_key_with_index_sqlite(DatabaseBackend::Sqlite)]
973    fn test_remove_constraint_foreign_key_with_index(#[case] backend: DatabaseBackend) {
974        // Test ForeignKey removal with indexes
975        let constraint = TableConstraint::ForeignKey {
976            name: Some("fk_user".into()),
977            columns: vec!["user_id".into()],
978            ref_table: "users".into(),
979            ref_columns: vec!["id".into()],
980            on_delete: None,
981            on_update: None,
982        };
983        let current_schema = vec![TableDef {
984            name: "posts".into(),
985            description: None,
986            columns: vec![
987                ColumnDef {
988                    name: "id".into(),
989                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
990                    nullable: false,
991                    default: None,
992                    comment: None,
993                    primary_key: None,
994                    unique: None,
995                    index: None,
996                    foreign_key: None,
997                },
998                ColumnDef {
999                    name: "user_id".into(),
1000                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1001                    nullable: true,
1002                    default: None,
1003                    comment: None,
1004                    primary_key: None,
1005                    unique: None,
1006                    index: None,
1007                    foreign_key: None,
1008                },
1009            ],
1010            constraints: vec![
1011                constraint.clone(),
1012                TableConstraint::Index {
1013                    name: Some("idx_user_id".into()),
1014                    columns: vec!["user_id".into()],
1015                },
1016            ],
1017        }];
1018
1019        let result =
1020            build_remove_constraint(&backend, "posts", &constraint, &current_schema, &[]).unwrap();
1021        let sql = result
1022            .iter()
1023            .map(|q| q.build(backend))
1024            .collect::<Vec<String>>()
1025            .join("\n");
1026
1027        if matches!(backend, DatabaseBackend::Sqlite) {
1028            assert!(sql.contains("CREATE INDEX"));
1029            assert!(sql.contains("idx_user_id"));
1030        }
1031
1032        with_settings!({ snapshot_suffix => format!("remove_foreign_key_with_index_{:?}", backend) }, {
1033            assert_snapshot!(sql);
1034        });
1035    }
1036
1037    #[rstest]
1038    #[case::remove_foreign_key_with_unique_constraint_postgres(DatabaseBackend::Postgres)]
1039    #[case::remove_foreign_key_with_unique_constraint_mysql(DatabaseBackend::MySql)]
1040    #[case::remove_foreign_key_with_unique_constraint_sqlite(DatabaseBackend::Sqlite)]
1041    fn test_remove_constraint_foreign_key_with_unique_constraint(#[case] backend: DatabaseBackend) {
1042        // Test ForeignKey removal with unique constraint
1043        let constraint = TableConstraint::ForeignKey {
1044            name: Some("fk_user".into()),
1045            columns: vec!["user_id".into()],
1046            ref_table: "users".into(),
1047            ref_columns: vec!["id".into()],
1048            on_delete: None,
1049            on_update: None,
1050        };
1051        let current_schema = vec![TableDef {
1052            name: "posts".into(),
1053            description: None,
1054            columns: vec![
1055                ColumnDef {
1056                    name: "id".into(),
1057                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1058                    nullable: false,
1059                    default: None,
1060                    comment: None,
1061                    primary_key: None,
1062                    unique: None,
1063                    index: None,
1064                    foreign_key: None,
1065                },
1066                ColumnDef {
1067                    name: "user_id".into(),
1068                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1069                    nullable: true,
1070                    default: None,
1071                    comment: None,
1072                    primary_key: None,
1073                    unique: None,
1074                    index: None,
1075                    foreign_key: None,
1076                },
1077            ],
1078            constraints: vec![
1079                constraint.clone(),
1080                TableConstraint::Unique {
1081                    name: Some("uq_user_id".into()),
1082                    columns: vec!["user_id".into()],
1083                },
1084            ],
1085        }];
1086
1087        let result =
1088            build_remove_constraint(&backend, "posts", &constraint, &current_schema, &[]).unwrap();
1089        let sql = result
1090            .iter()
1091            .map(|q| q.build(backend))
1092            .collect::<Vec<String>>()
1093            .join("\n");
1094
1095        if matches!(backend, DatabaseBackend::Sqlite) {
1096            // Unique constraint should be preserved in the temp table
1097            assert!(sql.contains("CREATE TABLE"));
1098        }
1099
1100        with_settings!({ snapshot_suffix => format!("remove_foreign_key_with_unique_constraint_{:?}", backend) }, {
1101            assert_snapshot!(sql);
1102        });
1103    }
1104
1105    #[test]
1106    fn test_remove_constraint_check_sqlite_table_not_found() {
1107        // Test error when table is not found (line 346)
1108        let constraint = TableConstraint::Check {
1109            name: "chk_age".into(),
1110            expr: "age > 0".into(),
1111        };
1112        let result = build_remove_constraint(
1113            &DatabaseBackend::Sqlite,
1114            "nonexistent_table",
1115            &constraint,
1116            &[], // Empty schema
1117            &[],
1118        );
1119        assert!(result.is_err());
1120        let err_msg = result.unwrap_err().to_string();
1121        assert!(err_msg.contains("Table 'nonexistent_table' not found in current schema"));
1122    }
1123
1124    #[rstest]
1125    #[case::remove_check_with_index_postgres(DatabaseBackend::Postgres)]
1126    #[case::remove_check_with_index_mysql(DatabaseBackend::MySql)]
1127    #[case::remove_check_with_index_sqlite(DatabaseBackend::Sqlite)]
1128    fn test_remove_constraint_check_with_index(#[case] backend: DatabaseBackend) {
1129        // Test Check removal with indexes
1130        let constraint = TableConstraint::Check {
1131            name: "chk_age".into(),
1132            expr: "age > 0".into(),
1133        };
1134        let current_schema = vec![TableDef {
1135            name: "users".into(),
1136            description: None,
1137            columns: vec![
1138                ColumnDef {
1139                    name: "id".into(),
1140                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1141                    nullable: false,
1142                    default: None,
1143                    comment: None,
1144                    primary_key: None,
1145                    unique: None,
1146                    index: None,
1147                    foreign_key: None,
1148                },
1149                ColumnDef {
1150                    name: "age".into(),
1151                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1152                    nullable: true,
1153                    default: None,
1154                    comment: None,
1155                    primary_key: None,
1156                    unique: None,
1157                    index: None,
1158                    foreign_key: None,
1159                },
1160            ],
1161            constraints: vec![
1162                constraint.clone(),
1163                TableConstraint::Index {
1164                    name: Some("idx_age".into()),
1165                    columns: vec!["age".into()],
1166                },
1167            ],
1168        }];
1169
1170        let result =
1171            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
1172        let sql = result
1173            .iter()
1174            .map(|q| q.build(backend))
1175            .collect::<Vec<String>>()
1176            .join("\n");
1177
1178        if matches!(backend, DatabaseBackend::Sqlite) {
1179            assert!(sql.contains("CREATE INDEX"));
1180            assert!(sql.contains("idx_age"));
1181        }
1182
1183        with_settings!({ snapshot_suffix => format!("remove_check_with_index_{:?}", backend) }, {
1184            assert_snapshot!(sql);
1185        });
1186    }
1187
1188    #[rstest]
1189    #[case::remove_check_with_unique_constraint_postgres(DatabaseBackend::Postgres)]
1190    #[case::remove_check_with_unique_constraint_mysql(DatabaseBackend::MySql)]
1191    #[case::remove_check_with_unique_constraint_sqlite(DatabaseBackend::Sqlite)]
1192    fn test_remove_constraint_check_with_unique_constraint(#[case] backend: DatabaseBackend) {
1193        // Test Check removal with unique constraint
1194        let constraint = TableConstraint::Check {
1195            name: "chk_age".into(),
1196            expr: "age > 0".into(),
1197        };
1198        let current_schema = vec![TableDef {
1199            name: "users".into(),
1200            description: None,
1201            columns: vec![
1202                ColumnDef {
1203                    name: "id".into(),
1204                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1205                    nullable: false,
1206                    default: None,
1207                    comment: None,
1208                    primary_key: None,
1209                    unique: None,
1210                    index: None,
1211                    foreign_key: None,
1212                },
1213                ColumnDef {
1214                    name: "age".into(),
1215                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1216                    nullable: true,
1217                    default: None,
1218                    comment: None,
1219                    primary_key: None,
1220                    unique: None,
1221                    index: None,
1222                    foreign_key: None,
1223                },
1224            ],
1225            constraints: vec![
1226                constraint.clone(),
1227                TableConstraint::Unique {
1228                    name: Some("uq_age".into()),
1229                    columns: vec!["age".into()],
1230                },
1231            ],
1232        }];
1233
1234        let result =
1235            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
1236        let sql = result
1237            .iter()
1238            .map(|q| q.build(backend))
1239            .collect::<Vec<String>>()
1240            .join("\n");
1241
1242        if matches!(backend, DatabaseBackend::Sqlite) {
1243            // Unique constraint should be preserved in the temp table
1244            assert!(sql.contains("CREATE TABLE"));
1245        }
1246
1247        with_settings!({ snapshot_suffix => format!("remove_check_with_unique_constraint_{:?}", backend) }, {
1248            assert_snapshot!(sql);
1249        });
1250    }
1251
1252    #[rstest]
1253    #[case::remove_unique_with_other_constraints_postgres(DatabaseBackend::Postgres)]
1254    #[case::remove_unique_with_other_constraints_mysql(DatabaseBackend::MySql)]
1255    #[case::remove_unique_with_other_constraints_sqlite(DatabaseBackend::Sqlite)]
1256    fn test_remove_constraint_unique_with_other_constraints(#[case] backend: DatabaseBackend) {
1257        // Test Unique removal with other constraint types (line 137)
1258        let constraint = TableConstraint::Unique {
1259            name: Some("uq_email".into()),
1260            columns: vec!["email".into()],
1261        };
1262        let current_schema = vec![TableDef {
1263            name: "users".into(),
1264            description: None,
1265            columns: vec![
1266                ColumnDef {
1267                    name: "id".into(),
1268                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1269                    nullable: false,
1270                    default: None,
1271                    comment: None,
1272                    primary_key: None,
1273                    unique: None,
1274                    index: None,
1275                    foreign_key: None,
1276                },
1277                ColumnDef {
1278                    name: "email".into(),
1279                    r#type: ColumnType::Simple(SimpleColumnType::Text),
1280                    nullable: true,
1281                    default: None,
1282                    comment: None,
1283                    primary_key: None,
1284                    unique: None,
1285                    index: None,
1286                    foreign_key: None,
1287                },
1288            ],
1289            constraints: vec![
1290                TableConstraint::PrimaryKey {
1291                    columns: vec!["id".into()],
1292                    auto_increment: false,
1293                },
1294                constraint.clone(),
1295                TableConstraint::Check {
1296                    name: "chk_email".into(),
1297                    expr: "email IS NOT NULL".into(),
1298                },
1299            ],
1300        }];
1301
1302        let result =
1303            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
1304        let sql = result
1305            .iter()
1306            .map(|q| q.build(backend))
1307            .collect::<Vec<String>>()
1308            .join("\n");
1309
1310        // Should still work with other constraint types present
1311        assert!(sql.contains("DROP") || sql.contains("CREATE TABLE"));
1312
1313        with_settings!({ snapshot_suffix => format!("remove_unique_with_other_constraints_{:?}", backend) }, {
1314            assert_snapshot!(sql);
1315        });
1316    }
1317
1318    #[rstest]
1319    #[case::remove_foreign_key_with_other_constraints_postgres(DatabaseBackend::Postgres)]
1320    #[case::remove_foreign_key_with_other_constraints_mysql(DatabaseBackend::MySql)]
1321    #[case::remove_foreign_key_with_other_constraints_sqlite(DatabaseBackend::Sqlite)]
1322    fn test_remove_constraint_foreign_key_with_other_constraints(#[case] backend: DatabaseBackend) {
1323        // Test ForeignKey removal with other constraint types (line 263)
1324        let constraint = TableConstraint::ForeignKey {
1325            name: Some("fk_user".into()),
1326            columns: vec!["user_id".into()],
1327            ref_table: "users".into(),
1328            ref_columns: vec!["id".into()],
1329            on_delete: None,
1330            on_update: None,
1331        };
1332        let current_schema = vec![TableDef {
1333            name: "posts".into(),
1334            description: None,
1335            columns: vec![
1336                ColumnDef {
1337                    name: "id".into(),
1338                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1339                    nullable: false,
1340                    default: None,
1341                    comment: None,
1342                    primary_key: None,
1343                    unique: None,
1344                    index: None,
1345                    foreign_key: None,
1346                },
1347                ColumnDef {
1348                    name: "user_id".into(),
1349                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1350                    nullable: true,
1351                    default: None,
1352                    comment: None,
1353                    primary_key: None,
1354                    unique: None,
1355                    index: None,
1356                    foreign_key: None,
1357                },
1358            ],
1359            constraints: vec![
1360                TableConstraint::PrimaryKey {
1361                    columns: vec!["id".into()],
1362                    auto_increment: false,
1363                },
1364                constraint.clone(),
1365                TableConstraint::Unique {
1366                    name: Some("uq_user_id".into()),
1367                    columns: vec!["user_id".into()],
1368                },
1369                TableConstraint::Check {
1370                    name: "chk_user_id".into(),
1371                    expr: "user_id > 0".into(),
1372                },
1373            ],
1374        }];
1375
1376        let result =
1377            build_remove_constraint(&backend, "posts", &constraint, &current_schema, &[]).unwrap();
1378        let sql = result
1379            .iter()
1380            .map(|q| q.build(backend))
1381            .collect::<Vec<String>>()
1382            .join("\n");
1383
1384        // Should still work with other constraint types present
1385        assert!(sql.contains("DROP") || sql.contains("CREATE TABLE"));
1386
1387        with_settings!({ snapshot_suffix => format!("remove_foreign_key_with_other_constraints_{:?}", backend) }, {
1388            assert_snapshot!(sql);
1389        });
1390    }
1391
1392    #[rstest]
1393    #[case::remove_check_with_other_constraints_postgres(DatabaseBackend::Postgres)]
1394    #[case::remove_check_with_other_constraints_mysql(DatabaseBackend::MySql)]
1395    #[case::remove_check_with_other_constraints_sqlite(DatabaseBackend::Sqlite)]
1396    fn test_remove_constraint_check_with_other_constraints(#[case] backend: DatabaseBackend) {
1397        // Test Check removal with other constraint types (line 357)
1398        let constraint = TableConstraint::Check {
1399            name: "chk_age".into(),
1400            expr: "age > 0".into(),
1401        };
1402        let current_schema = vec![TableDef {
1403            name: "users".into(),
1404            description: None,
1405            columns: vec![
1406                ColumnDef {
1407                    name: "id".into(),
1408                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1409                    nullable: false,
1410                    default: None,
1411                    comment: None,
1412                    primary_key: None,
1413                    unique: None,
1414                    index: None,
1415                    foreign_key: None,
1416                },
1417                ColumnDef {
1418                    name: "age".into(),
1419                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
1420                    nullable: true,
1421                    default: None,
1422                    comment: None,
1423                    primary_key: None,
1424                    unique: None,
1425                    index: None,
1426                    foreign_key: None,
1427                },
1428            ],
1429            constraints: vec![
1430                TableConstraint::PrimaryKey {
1431                    columns: vec!["id".into()],
1432                    auto_increment: false,
1433                },
1434                TableConstraint::Unique {
1435                    name: Some("uq_age".into()),
1436                    columns: vec!["age".into()],
1437                },
1438                constraint.clone(),
1439            ],
1440        }];
1441
1442        let result =
1443            build_remove_constraint(&backend, "users", &constraint, &current_schema, &[]).unwrap();
1444        let sql = result
1445            .iter()
1446            .map(|q| q.build(backend))
1447            .collect::<Vec<String>>()
1448            .join("\n");
1449
1450        // Should still work with other constraint types present
1451        assert!(sql.contains("DROP") || sql.contains("CREATE TABLE"));
1452
1453        with_settings!({ snapshot_suffix => format!("remove_check_with_other_constraints_{:?}", backend) }, {
1454            assert_snapshot!(sql);
1455        });
1456    }
1457
1458    #[test]
1459    fn test_remove_constraint_primary_key_postgres_direct() {
1460        // Direct non-rstest coverage for PK drop on Postgres (lines 53-54)
1461        let constraint = TableConstraint::PrimaryKey {
1462            columns: vec!["id".into()],
1463            auto_increment: false,
1464        };
1465        let schema = vec![TableDef {
1466            name: "orders".into(),
1467            description: None,
1468            columns: vec![ColumnDef {
1469                name: "id".into(),
1470                r#type: ColumnType::Simple(SimpleColumnType::Integer),
1471                nullable: false,
1472                default: None,
1473                comment: None,
1474                primary_key: None,
1475                unique: None,
1476                index: None,
1477                foreign_key: None,
1478            }],
1479            constraints: vec![constraint.clone()],
1480        }];
1481        let result = build_remove_constraint(
1482            &DatabaseBackend::Postgres,
1483            "orders",
1484            &constraint,
1485            &schema,
1486            &[],
1487        )
1488        .unwrap();
1489        assert_eq!(result.len(), 1);
1490        let sql = result[0].build(DatabaseBackend::Postgres);
1491        assert!(sql.contains("ALTER TABLE \"orders\" DROP CONSTRAINT \"orders_pkey\""));
1492    }
1493
1494    #[test]
1495    fn test_remove_constraint_primary_key_mysql_direct() {
1496        // Direct non-rstest coverage for PK drop on MySQL (lines 53-54)
1497        let constraint = TableConstraint::PrimaryKey {
1498            columns: vec!["id".into()],
1499            auto_increment: false,
1500        };
1501        let schema = vec![TableDef {
1502            name: "orders".into(),
1503            description: None,
1504            columns: vec![ColumnDef {
1505                name: "id".into(),
1506                r#type: ColumnType::Simple(SimpleColumnType::Integer),
1507                nullable: false,
1508                default: None,
1509                comment: None,
1510                primary_key: None,
1511                unique: None,
1512                index: None,
1513                foreign_key: None,
1514            }],
1515            constraints: vec![constraint.clone()],
1516        }];
1517        let result =
1518            build_remove_constraint(&DatabaseBackend::MySql, "orders", &constraint, &schema, &[])
1519                .unwrap();
1520        assert_eq!(result.len(), 1);
1521        let sql = result[0].build(DatabaseBackend::MySql);
1522        assert!(sql.contains("ALTER TABLE `orders` DROP PRIMARY KEY"));
1523    }
1524
1525    #[rstest]
1526    #[case::remove_index_with_custom_inline_name_postgres(DatabaseBackend::Postgres)]
1527    #[case::remove_index_with_custom_inline_name_mysql(DatabaseBackend::MySql)]
1528    #[case::remove_index_with_custom_inline_name_sqlite(DatabaseBackend::Sqlite)]
1529    fn test_remove_constraint_index_with_custom_inline_name(#[case] backend: DatabaseBackend) {
1530        // Test Index removal with a custom name from inline index field
1531        // This tests the scenario where index: "custom_idx_name" is used
1532        let constraint = TableConstraint::Index {
1533            name: Some("custom_idx_email".into()),
1534            columns: vec!["email".into()],
1535        };
1536
1537        let schema = vec![TableDef {
1538            name: "users".to_string(),
1539            description: None,
1540            columns: vec![ColumnDef {
1541                name: "email".to_string(),
1542                r#type: ColumnType::Simple(SimpleColumnType::Text),
1543                nullable: true,
1544                default: None,
1545                comment: None,
1546                primary_key: None,
1547                unique: None,
1548                index: Some(vespertide_core::StrOrBoolOrArray::Str(
1549                    "custom_idx_email".into(),
1550                )),
1551                foreign_key: None,
1552            }],
1553            constraints: vec![],
1554        }];
1555
1556        let result = build_remove_constraint(&backend, "users", &constraint, &schema, &[]);
1557        assert!(result.is_ok());
1558        let sql = result
1559            .unwrap()
1560            .iter()
1561            .map(|q| q.build(backend))
1562            .collect::<Vec<String>>()
1563            .join("\n");
1564
1565        // Should use the custom index name
1566        assert!(sql.contains("custom_idx_email"));
1567
1568        with_settings!({ snapshot_suffix => format!("remove_index_custom_name_{:?}", backend) }, {
1569            assert_snapshot!(sql);
1570        });
1571    }
1572}