use crate::checks::pg_helpers::{AlterTableType, NodeEnum, alter_table_cmds};
use crate::checks::{Check, Config, MigrationContext};
use crate::violation::Violation;
use regex::Regex;
use std::sync::LazyLock;
static PRIMARY_KEY_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)((_pkey|_pk)$|^pk_|_primary_key|primarykey)")
.expect("Invalid primary key regex pattern")
});
pub struct DropPrimaryKeyCheck;
impl DropPrimaryKeyCheck {
fn is_likely_primary_key(constraint_name: &str) -> bool {
PRIMARY_KEY_PATTERN.is_match(constraint_name)
}
}
impl Check for DropPrimaryKeyCheck {
fn check(&self, node: &NodeEnum, _config: &Config, _ctx: &MigrationContext) -> Vec<Violation> {
let Some((table_name, cmds)) = alter_table_cmds(node) else {
return vec![];
};
cmds.iter()
.filter_map(|cmd| {
if cmd.subtype != AlterTableType::AtDropConstraint as i32 {
return None;
}
let constraint_name_str = &cmd.name;
if !Self::is_likely_primary_key(constraint_name_str) {
return None;
}
Some(Violation::new(
"DROP PRIMARY KEY",
format!(
"Dropping primary key constraint '{constraint_name_str}' from table '{table_name}' requires an ACCESS EXCLUSIVE lock, blocking all operations. \
More critically, this breaks foreign key relationships in other tables and removes the uniqueness constraint."
),
format!(r"Consider the following before dropping a primary key:
1. Identify all foreign key dependencies:
SELECT
tc.table_name, kcu.column_name, rc.constraint_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.referential_constraints rc ON tc.constraint_name = rc.unique_constraint_name
WHERE tc.table_name = '{table_name}' AND tc.constraint_type = 'PRIMARY KEY';
2. If you must change the primary key:
- Create the new primary key constraint FIRST
- Update all foreign keys to reference the new key
- Then drop the old primary key
3. If migrating to a different key strategy:
- Consider using a transition period with both keys
- Update application code gradually
- Drop the old key only after full migration
Note: This check uses naming pattern detection (e.g., '{constraint_name_str}' matches '*_pkey' pattern) and may not catch all cases.
Future versions will support database connections for accurate constraint type verification.
If this is a false positive, use a safety-assured block."
),
))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_allows, assert_detects_violation};
#[test]
fn test_detects_drop_primary_key_pkey_suffix() {
assert_detects_violation!(
DropPrimaryKeyCheck,
"ALTER TABLE users DROP CONSTRAINT users_pkey;",
"DROP PRIMARY KEY"
);
}
#[test]
fn test_detects_drop_primary_key_pk_suffix() {
assert_detects_violation!(
DropPrimaryKeyCheck,
"ALTER TABLE users DROP CONSTRAINT users_pk;",
"DROP PRIMARY KEY"
);
}
#[test]
fn test_detects_drop_primary_key_pk_prefix() {
assert_detects_violation!(
DropPrimaryKeyCheck,
"ALTER TABLE users DROP CONSTRAINT pk_users;",
"DROP PRIMARY KEY"
);
}
#[test]
fn test_detects_drop_primary_key_primary_key_in_name() {
assert_detects_violation!(
DropPrimaryKeyCheck,
"ALTER TABLE users DROP CONSTRAINT users_primary_key;",
"DROP PRIMARY KEY"
);
}
#[test]
fn test_allows_drop_unique_constraint() {
assert_allows!(
DropPrimaryKeyCheck,
"ALTER TABLE users DROP CONSTRAINT users_email_key;"
);
}
#[test]
fn test_allows_drop_foreign_key_constraint() {
assert_allows!(
DropPrimaryKeyCheck,
"ALTER TABLE posts DROP CONSTRAINT posts_user_id_fkey;"
);
}
#[test]
fn test_allows_drop_check_constraint() {
assert_allows!(
DropPrimaryKeyCheck,
"ALTER TABLE users DROP CONSTRAINT users_age_check;"
);
}
#[test]
fn test_ignores_add_constraint() {
assert_allows!(
DropPrimaryKeyCheck,
"ALTER TABLE users ADD CONSTRAINT users_pkey PRIMARY KEY (id);"
);
}
#[test]
fn test_ignores_other_statements() {
assert_allows!(
DropPrimaryKeyCheck,
"CREATE TABLE users (id SERIAL PRIMARY KEY);"
);
}
}