squawk-linter 2.49.0

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

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

pub(crate) fn ban_concurrent_index_creation_in_transaction(
    ctx: &mut Linter,
    parse: &Parse<SourceFile>,
) {
    let mut in_transaction = ctx.settings.assume_in_transaction;
    let file = parse.tree();
    let mut errors = vec![];
    let mut stmt_count = 0;
    for stmt in file.stmts() {
        stmt_count += 1;
        match stmt {
            ast::Stmt::Begin(_) => {
                in_transaction = true;
            }
            ast::Stmt::Commit(_) => {
                in_transaction = false;
            }
            ast::Stmt::CreateIndex(create_index) => {
                if in_transaction {
                    if let Some(concurrently) = create_index.concurrently_token() {
                        errors.push(Violation::for_range(
                            Rule::BanConcurrentIndexCreationInTransaction,
                            "While regular index creation can happen inside a transaction, this is not allowed when the `CONCURRENTLY` option is used.".into(),
                            concurrently.text_range(),
                        ).help("Build the index outside any transactions."));
                    }
                }
            }
            _ => (),
        }
    }
    if stmt_count > 1 {
        for error in errors {
            ctx.report(error);
        }
    }
}

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

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

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

    fn lint_errors_with(sql: &str, settings: LinterSettings) -> String {
        crate::test_utils::lint_errors_with(
            sql,
            settings,
            Rule::BanConcurrentIndexCreationInTransaction,
        )
    }

    #[test]
    fn ban_concurrent_index_creation_in_transaction_err() {
        let sql = r#"
        -- instead of
        BEGIN;
        CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name");
        COMMIT;
        "#;
        assert_snapshot!(lint_errors(
            sql,
            Rule::BanConcurrentIndexCreationInTransaction
        ));
    }

    #[test]
    fn ban_concurrent_index_creation_in_transaction_ok() {
        let sql = r#"
  -- run outside a transaction
  CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name");
        "#;
        lint_ok(sql, Rule::BanConcurrentIndexCreationInTransaction);
    }

    #[test]
    fn assuming_in_transaction_err() {
        let sql = r#"
  -- instead of
  CREATE UNIQUE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name");
  ALTER TABLE "table_name" ADD CONSTRAINT "field_name_id" UNIQUE USING INDEX "field_name_idx";
    "#;
        assert_snapshot!(lint_errors_with(
            sql,
            LinterSettings {
                assume_in_transaction: true,
                ..Default::default()
            },
        ));
    }

    #[test]
    fn assuming_in_transaction_ok() {
        let sql = r#"
  -- run index creation in a standalone migration
  CREATE UNIQUE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name");
        "#;
        lint_ok_with(
            sql,
            LinterSettings {
                assume_in_transaction: true,
                ..Default::default()
            },
        );
    }

    #[test]
    fn assuming_in_transaction_ok_also() {
        let sql = r#"
  -- the following will work too
  COMMIT;
  CREATE UNIQUE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name");
  BEGIN;
  ALTER TABLE "table_name" ADD CONSTRAINT "field_name_id" UNIQUE USING INDEX "field_name_idx";
    "#;
        lint_ok_with(
            sql,
            LinterSettings {
                assume_in_transaction: true,
                ..Default::default()
            },
        );
    }
}