pgsafe 0.11.0

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

use super::Rule;
use crate::RuleHit;

pub struct DropNotNull;

impl Rule for DropNotNull {
    fn id(&self) -> &'static str {
        "drop-not-null"
    }
    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::AtDropNotNull)
            ) {
                out.push(RuleHit {
                    message: "ALTER COLUMN ... DROP NOT NULL silently removes a not-null invariant that \
                              application code, ORMs, and the query planner may rely on."
                        .into(),
                    guidance: "Confirm nothing depends on the column being non-null — planner assumptions, \
                               ORM models, application code — before dropping the constraint."
                        .into(),
                    fix: None,
                });
            }
        }
    }
}

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

    fn findings(sql: &str) -> Vec<crate::Finding> {
        lint_sql(sql, &LintOptions::default()).unwrap()
    }

    #[test]
    fn flags_drop_not_null() {
        let f = findings("ALTER TABLE t ALTER COLUMN a DROP NOT NULL")
            .into_iter()
            .find(|f| f.rule_id == "drop-not-null")
            .expect("rule must fire");
        assert_eq!(f.severity, Severity::Warning);
    }

    #[test]
    fn silent_on_set_not_null() {
        assert!(findings("ALTER TABLE t ALTER COLUMN a SET NOT NULL")
            .iter()
            .all(|f| f.rule_id != "drop-not-null"));
    }
}