pgsafe 0.9.0

Static safety linter for PostgreSQL DDL migrations — catches unsafe schema changes before they lock or break production.
Documentation
use pg_query::protobuf::AlterTableType;
use pg_query::NodeEnum;

use super::Rule;
use crate::{RuleHit, Severity};

pub struct SetNotNull;

impl Rule for SetNotNull {
    fn id(&self) -> &'static str {
        "set-not-null"
    }
    fn severity(&self) -> Severity {
        Severity::Error
    }

    fn check(&self, node: &NodeEnum, out: &mut Vec<RuleHit>) {
        for cmd in super::alter_table_cmds(node) {
            if matches!(
                AlterTableType::try_from(cmd.subtype),
                Ok(AlterTableType::AtSetNotNull)
            ) {
                out.push(RuleHit {
                    message: "ALTER COLUMN ... SET NOT NULL scans the entire table under an ACCESS \
                              EXCLUSIVE lock."
                        .into(),
                    guidance: "On PG12+, first add `CHECK (col IS NOT NULL) NOT VALID`, run VALIDATE \
                               CONSTRAINT, then SET NOT NULL (it reuses the validated check and skips \
                               the scan). Drop the helper CHECK afterward if you like."
                        .into(),
                    fix: None,
                });
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::{lint_sql, LintOptions};

    #[test]
    fn flags_set_not_null() {
        let findings = lint_sql(
            "ALTER TABLE t ALTER COLUMN a SET NOT NULL",
            &LintOptions::default(),
        )
        .unwrap();
        assert!(findings.iter().any(|f| f.rule_id == "set-not-null"));
    }

    #[test]
    fn ignores_drop_not_null() {
        let findings = lint_sql(
            "ALTER TABLE t ALTER COLUMN a DROP NOT NULL",
            &LintOptions::default(),
        )
        .unwrap();
        assert!(findings.iter().all(|f| f.rule_id != "set-not-null"));
    }
}