use crate::checks::pg_helpers::{
NodeEnum, alter_table_cmds, cmd_def_as_column_def, column_type_name, is_serial_pattern,
};
use crate::checks::{Check, Config, MigrationContext};
use crate::violation::Violation;
pub struct AddSerialColumnCheck;
impl Check for AddSerialColumnCheck {
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| {
let col = cmd_def_as_column_def(cmd)?;
if !is_serial_pattern(col) {
return None;
}
let column_name = &col.colname;
let type_name = column_type_name(col);
Some(Violation::new(
"ADD COLUMN with SERIAL",
format!(
"Adding column '{column_name}' with SERIAL type on table '{table_name}' requires a full table rewrite to populate sequence values for existing rows, \
which acquires an ACCESS EXCLUSIVE lock and blocks all operations. Duration depends on table size and number of indexes."
),
format!(r"1. Create a sequence:
CREATE SEQUENCE {table_name}_{column_name}_seq;
2. Add the column WITHOUT default (fast, no rewrite):
ALTER TABLE {table_name} ADD COLUMN {column_name} {type_name};
3. Backfill existing rows in batches (outside migration):
UPDATE {table_name} SET {column_name} = nextval('{table_name}_{column_name}_seq') WHERE {column_name} IS NULL;
4. Set default for future inserts only:
ALTER TABLE {table_name} ALTER COLUMN {column_name} SET DEFAULT nextval('{table_name}_{column_name}_seq');
5. Set NOT NULL if needed (Postgres 11+: safe if all values present):
ALTER TABLE {table_name} ALTER COLUMN {column_name} SET NOT NULL;
6. Set sequence ownership:
ALTER SEQUENCE {table_name}_{column_name}_seq OWNED BY {table_name}.{column_name};"
),
))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_allows, assert_detects_violation};
#[test]
fn test_detects_add_column_with_serial() {
assert_detects_violation!(
AddSerialColumnCheck,
"ALTER TABLE users ADD COLUMN id SERIAL;",
"ADD COLUMN with SERIAL"
);
}
#[test]
fn test_detects_add_column_with_bigserial() {
assert_detects_violation!(
AddSerialColumnCheck,
"ALTER TABLE users ADD COLUMN id BIGSERIAL;",
"ADD COLUMN with SERIAL"
);
}
#[test]
fn test_detects_add_column_with_smallserial() {
assert_detects_violation!(
AddSerialColumnCheck,
"ALTER TABLE users ADD COLUMN id SMALLSERIAL;",
"ADD COLUMN with SERIAL"
);
}
#[test]
fn test_allows_add_column_with_integer() {
assert_allows!(
AddSerialColumnCheck,
"ALTER TABLE users ADD COLUMN count INTEGER;"
);
}
#[test]
fn test_allows_create_table_with_serial() {
assert_allows!(
AddSerialColumnCheck,
"CREATE TABLE users (id SERIAL PRIMARY KEY);"
);
}
#[test]
fn test_ignores_other_statements() {
assert_allows!(
AddSerialColumnCheck,
"CREATE INDEX idx_users_email ON users(email);"
);
}
}