use crate::checks::pg_helpers::{
NodeEnum, ObjectType, concurrent_safe_alternative, drop_object_names,
};
use crate::checks::{Check, Config, MigrationContext, if_exists_clause};
use crate::violation::Violation;
pub struct DropIndexCheck;
impl Check for DropIndexCheck {
fn check(&self, node: &NodeEnum, _config: &Config, ctx: &MigrationContext) -> Vec<Violation> {
let NodeEnum::DropStmt(drop_stmt) = node else {
return vec![];
};
if drop_stmt.remove_type != ObjectType::ObjectIndex as i32 {
return vec![];
}
let if_exists_str = if_exists_clause(drop_stmt.missing_ok);
if !drop_stmt.concurrent {
return drop_object_names(&drop_stmt.objects)
.into_iter()
.map(|name| {
let suggestion = format!(
r#"Use CONCURRENTLY to drop the index without blocking queries:
DROP INDEX CONCURRENTLY{if_exists_str} {name};
Note: CONCURRENTLY requires Postgres 9.2+.
Considerations:
- Takes longer to complete than regular DROP INDEX
- Allows concurrent SELECT, INSERT, UPDATE, DELETE operations
- If it fails, the index may be marked "invalid" and should be dropped again
- Cannot be rolled back (no transaction support)"#,
);
let safe_alternative = concurrent_safe_alternative(suggestion, ctx);
Violation::new(
"DROP INDEX without CONCURRENTLY",
format!(
"Dropping index '{name}'{if_exists_str} without CONCURRENTLY acquires an ACCESS EXCLUSIVE lock, blocking all \
queries (SELECT, INSERT, UPDATE, DELETE) on the table until complete. Duration depends on system load and concurrent transactions."
),
safe_alternative,
)
})
.collect();
}
if !ctx.run_in_transaction {
return vec![];
}
drop_object_names(&drop_stmt.objects)
.into_iter()
.map(|name| {
Violation::new(
"DROP INDEX CONCURRENTLY inside a transaction",
format!(
"Dropping index '{name}'{if_exists_str} with CONCURRENTLY cannot run inside a transaction block. \
PostgreSQL will raise an error at runtime."
),
ctx.no_transaction_hint,
)
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checks::test_utils::parse_sql;
use crate::{
assert_allows, assert_allows_with_context, assert_detects_n_violations,
assert_detects_violation, assert_detects_violation_with_context,
};
#[test]
fn test_detects_drop_index() {
assert_detects_violation!(
DropIndexCheck,
"DROP INDEX idx_users_email;",
"DROP INDEX without CONCURRENTLY"
);
}
#[test]
fn test_detects_drop_index_if_exists() {
assert_detects_violation!(
DropIndexCheck,
"DROP INDEX IF EXISTS idx_users_email;",
"DROP INDEX without CONCURRENTLY"
);
}
#[test]
fn test_detects_drop_index_cascade() {
assert_detects_violation!(
DropIndexCheck,
"DROP INDEX idx_users_email CASCADE;",
"DROP INDEX without CONCURRENTLY"
);
}
#[test]
fn test_detects_drop_index_restrict() {
assert_detects_violation!(
DropIndexCheck,
"DROP INDEX idx_users_email RESTRICT;",
"DROP INDEX without CONCURRENTLY"
);
}
#[test]
fn test_detects_drop_multiple_indexes() {
assert_detects_n_violations!(
DropIndexCheck,
"DROP INDEX idx1, idx2, idx3;",
3,
"DROP INDEX without CONCURRENTLY"
);
}
#[test]
fn test_detects_drop_index_if_exists_cascade() {
assert_detects_violation!(
DropIndexCheck,
"DROP INDEX IF EXISTS idx_users_email CASCADE;",
"DROP INDEX without CONCURRENTLY"
);
}
#[test]
fn test_allows_drop_index_concurrently_outside_transaction() {
assert_allows_with_context!(
DropIndexCheck,
"DROP INDEX CONCURRENTLY idx_users_email;",
MigrationContext {
run_in_transaction: false,
..MigrationContext::default()
}
);
}
#[test]
fn test_detects_concurrent_in_transaction() {
assert_detects_violation_with_context!(
DropIndexCheck,
"DROP INDEX CONCURRENTLY idx_users_email;",
"DROP INDEX CONCURRENTLY inside a transaction",
MigrationContext {
run_in_transaction: true,
..MigrationContext::default()
}
);
}
#[test]
fn test_allows_concurrent_outside_transaction() {
assert_allows_with_context!(
DropIndexCheck,
"DROP INDEX CONCURRENTLY idx_users_email;",
MigrationContext {
run_in_transaction: false,
..MigrationContext::default()
}
);
}
#[test]
fn test_safe_alternative_includes_transaction_hint_when_in_transaction() {
let stmt = parse_sql("DROP INDEX idx_users_email;");
let violations = DropIndexCheck.check(
&stmt,
&Config::default(),
&MigrationContext {
run_in_transaction: true,
no_transaction_hint: "Create `metadata.toml` with `run_in_transaction = false`.",
},
);
assert_eq!(violations.len(), 1);
assert!(
violations[0]
.safe_alternative
.contains("Create `metadata.toml` with `run_in_transaction = false`."),
"Expected transaction hint in safe_alternative"
);
assert!(
violations[0]
.safe_alternative
.contains("CONCURRENTLY cannot run inside a transaction block"),
"Expected transaction note in safe_alternative"
);
}
#[test]
fn test_safe_alternative_omits_transaction_hint_when_outside_transaction() {
let stmt = parse_sql("DROP INDEX idx_users_email;");
let violations = DropIndexCheck.check(
&stmt,
&Config::default(),
&MigrationContext {
run_in_transaction: false,
no_transaction_hint: "Create `metadata.toml` with `run_in_transaction = false`.",
},
);
assert_eq!(violations.len(), 1);
assert!(
!violations[0]
.safe_alternative
.contains("Create `metadata.toml` with `run_in_transaction = false`."),
"Expected no transaction hint in safe_alternative"
);
}
#[test]
fn test_ignores_other_drop_statements() {
assert_allows!(DropIndexCheck, "DROP TABLE users;");
}
#[test]
fn test_ignores_other_statements() {
assert_allows!(
DropIndexCheck,
"CREATE INDEX idx_users_email ON users(email);"
);
}
#[test]
fn test_sqlx_framework_safe_alternative_message() {
let stmt = parse_sql("DROP INDEX CONCURRENTLY idx_users_email;");
let violations = DropIndexCheck.check(
&stmt,
&Config::default(),
&MigrationContext {
run_in_transaction: true,
no_transaction_hint: "Add `-- no-transaction` as the first line of the migration file.",
},
);
assert_eq!(violations.len(), 1);
assert!(
violations[0].safe_alternative.contains("-- no-transaction"),
"Expected SQLx safe alternative message"
);
}
#[test]
fn test_diesel_framework_safe_alternative_message() {
let stmt = parse_sql("DROP INDEX CONCURRENTLY idx_users_email;");
let violations = DropIndexCheck.check(
&stmt,
&Config::default(),
&MigrationContext {
run_in_transaction: true,
no_transaction_hint: "Create `metadata.toml` in the migration directory with `run_in_transaction = false`.",
},
);
assert_eq!(violations.len(), 1);
assert!(
violations[0].safe_alternative.contains("metadata.toml"),
"Expected Diesel safe alternative message"
);
}
}