pgsafe 0.9.1

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

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

pub struct AddIndexNonConcurrent;

impl Rule for AddIndexNonConcurrent {
    fn id(&self) -> &'static str {
        "add-index-non-concurrent"
    }
    fn severity(&self) -> Severity {
        Severity::Error
    }

    fn check(&self, node: &NodeEnum, out: &mut Vec<RuleHit>) {
        if let NodeEnum::IndexStmt(stmt) = node {
            if !stmt.concurrent {
                out.push(RuleHit {
                    message: "CREATE INDEX without CONCURRENTLY takes a lock that blocks writes \
                              to the table for the entire build."
                        .into(),
                    guidance:
                        "Use CREATE INDEX CONCURRENTLY (outside a transaction block). A failed \
                               CONCURRENTLY build leaves an INVALID index: drop it with DROP INDEX \
                               CONCURRENTLY and retry, or rebuild with REINDEX INDEX CONCURRENTLY."
                            .into(),
                    fix: Some(crate::fix::FixDraft {
                        title: "Add CONCURRENTLY",
                        edits: vec![crate::fix::FixDraftEdit {
                            anchor: crate::fix::FixAnchor::AfterKeyword("INDEX"),
                            replacement: " CONCURRENTLY".into(),
                        }],
                    }),
                });
            }
        }
    }
}

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

    #[test]
    fn flags_plain_create_index() {
        let findings = lint_sql("CREATE INDEX idx ON t (col)", &LintOptions::default()).unwrap();
        assert!(findings
            .iter()
            .any(|f| f.rule_id == "add-index-non-concurrent"));
    }

    #[test]
    fn ignores_concurrent_create_index() {
        let findings = lint_sql(
            "CREATE INDEX CONCURRENTLY idx ON t (col)",
            &LintOptions::default(),
        )
        .unwrap();
        assert!(findings
            .iter()
            .all(|f| f.rule_id != "add-index-non-concurrent"));
    }

    #[test]
    fn emits_a_concurrently_fix() {
        use crate::fix::apply;
        let sql = "CREATE INDEX idx ON t (col);";
        let fs = lint_sql(sql, &LintOptions::default()).unwrap();
        let f = fs
            .iter()
            .find(|f| f.rule_id == "add-index-non-concurrent")
            .unwrap();
        let fix = f.fix.as_ref().expect("fix present");
        assert_eq!(fix.title, "Add CONCURRENTLY");
        let fixed = apply(sql, &fix.edits);
        assert_eq!(fixed, "CREATE INDEX CONCURRENTLY idx ON t (col);");
        // Applying it clears the finding.
        assert!(lint_sql(&fixed, &LintOptions::default())
            .unwrap()
            .iter()
            .all(|f| f.rule_id != "add-index-non-concurrent"));
    }
}