use crate::checks::pg_helpers::{NodeEnum, range_var_name};
use crate::checks::{Check, Config, MigrationContext};
use crate::violation::Violation;
pub struct TruncateTableCheck;
impl Check for TruncateTableCheck {
fn check(&self, node: &NodeEnum, _config: &Config, _ctx: &MigrationContext) -> Vec<Violation> {
let NodeEnum::TruncateStmt(truncate) = node else {
return vec![];
};
truncate
.relations
.iter()
.filter_map(|rel_node| {
if let Some(NodeEnum::RangeVar(rv)) = &rel_node.node {
let table_name_str = range_var_name(rv);
Some(Violation::new(
"TRUNCATE TABLE",
format!(
"TRUNCATE TABLE on '{table_name_str}' acquires an ACCESS EXCLUSIVE lock, blocking \
all reads and writes. Unlike DELETE, it cannot be batched or throttled. \
This is safe for empty/small tables or non-production environments, but \
dangerous on large production tables."
),
format!(
r#"If this table can be large in production, prefer batched DELETE:
1. Delete rows in small batches:
DELETE FROM {table_name_str} WHERE id IN (
SELECT id FROM {table_name_str} LIMIT 1000
);
2. Repeat until all rows are removed.
3. (Optional) Reset sequences:
ALTER SEQUENCE {table_name_str}_id_seq RESTART WITH 1;
4. (Optional) Reclaim space:
VACUUM {table_name_str};
If TRUNCATE is intentional (e.g. lookup table, test/staging environment,
or table is known to be small), silence this check:
Per-statement — wrap in a safety-assured block:
-- safety-assured:start
-- Safe because: lookup table, always small
TRUNCATE TABLE {table_name_str};
-- safety-assured:end
Project-wide as a warning (reported but non-blocking):
# diesel-guard.toml
warn_checks = ["TruncateTableCheck"]
Project-wide silenced:
# diesel-guard.toml
disable_checks = ["TruncateTableCheck"]"#
),
))
} else {
None
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_allows, assert_detects_n_violations, assert_detects_violation};
#[test]
fn test_detects_truncate_table() {
assert_detects_violation!(
TruncateTableCheck,
"TRUNCATE TABLE users;",
"TRUNCATE TABLE"
);
}
#[test]
fn test_detects_truncate_multiple_tables() {
assert_detects_n_violations!(
TruncateTableCheck,
"TRUNCATE TABLE users, orders;",
2,
"TRUNCATE TABLE"
);
}
#[test]
fn test_detects_truncate_with_cascade() {
assert_detects_violation!(
TruncateTableCheck,
"TRUNCATE TABLE users CASCADE;",
"TRUNCATE TABLE"
);
}
#[test]
fn test_ignores_delete_statement() {
assert_allows!(TruncateTableCheck, "DELETE FROM users;");
}
#[test]
fn test_ignores_drop_table() {
assert_allows!(TruncateTableCheck, "DROP TABLE users;");
}
}