squawk-linter 2.50.0

Linter for Postgres migrations & SQL
Documentation
use squawk_syntax::{
    Parse, SourceFile,
    ast::{self, AstNode},
    identifier::Identifier,
};

use crate::{Edit, Fix, Linter, Rule, Violation};

use super::constraint_missing_not_valid::tables_created_in_transaction;

fn concurrently_fix(create_index: &ast::CreateIndex) -> Option<Fix> {
    let index_token = create_index.index_token()?;
    let at = index_token.text_range().end();
    let edit = Edit::insert(" concurrently", at);
    Some(Fix::new("Add `concurrently`", vec![edit]))
}

pub(crate) fn require_concurrent_index_creation(ctx: &mut Linter, parse: &Parse<SourceFile>) {
    let file = parse.tree();
    let tables_created = tables_created_in_transaction(ctx.settings.assume_in_transaction, &file);
    for stmt in file.stmts() {
        if let ast::Stmt::CreateIndex(create_index) = stmt {
            if let Some(table_name) = create_index
                .relation_name()
                .and_then(|x| x.path())
                .and_then(|x| x.segment())
                .and_then(|x| x.name_ref())
            {
                if create_index.concurrently_token().is_none()
                    && !tables_created.contains(&Identifier::new(&table_name.text()))
                {
                    let fix = concurrently_fix(&create_index);

                    ctx.report(Violation::for_node(
                        Rule::RequireConcurrentIndexCreation,
                "During normal index creation, table updates are blocked, but reads are still allowed.".into(),
                        create_index.syntax(),
                    )
                    .help("Use `concurrently` to avoid blocking writes.")
                    .fix(fix));
                }
            }
        }
    }
}

#[cfg(test)]
mod test {
    use insta::assert_snapshot;

    use crate::{
        LinterSettings, Rule,
        test_utils::{fix_sql, lint_errors, lint_ok},
    };

    fn lint_ok_with(sql: &str, settings: LinterSettings) {
        crate::test_utils::lint_ok_with(sql, settings, Rule::RequireConcurrentIndexCreation);
    }

    fn fix(sql: &str) -> String {
        fix_sql(sql, Rule::RequireConcurrentIndexCreation)
    }

    #[test]
    fn fix_add_concurrently_named_index() {
        assert_snapshot!(fix("CREATE INDEX i ON t (c);"), @"CREATE INDEX concurrently i ON t (c);");
    }

    #[test]
    fn fix_add_concurrently_unnamed_index() {
        assert_snapshot!(fix("
CREATE INDEX ON t (a);
"), @"CREATE INDEX concurrently ON t (a);");
    }

    /// ```sql
    /// -- instead of
    /// CREATE INDEX "field_name_idx" ON "table_name" ("field_name");
    /// -- use CONCURRENTLY
    /// CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name");
    /// ```
    #[test]
    fn adding_index_non_concurrently_err() {
        let sql = r#"
-- instead of
CREATE INDEX "field_name_idx" ON "table_name" ("field_name");
        "#;
        assert_snapshot!(lint_errors(sql, Rule::RequireConcurrentIndexCreation));
    }

    #[test]
    fn adding_index_concurrently_ok() {
        let sql = r#"
-- use CONCURRENTLY
CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name");
        "#;
        lint_ok(sql, Rule::RequireConcurrentIndexCreation);
    }

    #[test]
    fn new_table_ok() {
        let sql = r#"
BEGIN;
CREATE TABLE "core_foo" (
"id" serial NOT NULL PRIMARY KEY,
"tenant_id" integer NULL
);
CREATE INDEX "core_foo_tenant_id_4d397ef9" ON "core_foo" ("tenant_id");
COMMIT;
        "#;
        lint_ok(sql, Rule::RequireConcurrentIndexCreation);
    }

    #[test]
    fn new_table_in_assume_transaction_ok() {
        let sql = r#"
CREATE TABLE "core_foo" (
"id" serial NOT NULL PRIMARY KEY,
"tenant_id" integer NULL
);
CREATE INDEX "core_foo_tenant_id_4d397ef9" ON "core_foo" ("tenant_id");
        "#;
        lint_ok_with(
            sql,
            LinterSettings {
                assume_in_transaction: true,
                ..Default::default()
            },
        );
    }
}