Skip to content

feat(lint): flag a CONCURRENTLY operation inside a transaction block - #222

Open
aparajon wants to merge 3 commits into
mainfrom
armand/lint-concurrent-in-transaction
Open

aparajon wants to merge 3 commits into
mainfrom
armand/lint-concurrent-in-transaction

Conversation

@aparajon

Copy link
Copy Markdown
Collaborator

Why

PostgreSQL refuses every CONCURRENTLY form inside an explicit transaction block. Many schema change tools wrap each file in BEGIN … COMMIT, so a file that adds an index the safe way still fails at that statement and rolls back. Lint flagged BEGIN and COMMIT as unrecognized, but said nothing about the statement that actually breaks.

What

  • Lint tracks the block a script opens and reports each CONCURRENTLY operation inside it as concurrent-in-transaction, an error. That covers CREATE INDEX, DROP INDEX, REINDEX, and DETACH PARTITION.
  • A savepoint, or a COMMIT AND CHAIN that opens the next block at once, keeps the script inside its block. COMMIT, ROLLBACK, and PREPARE TRANSACTION close it.
  • statement.TransactionControlOf reads those statements from the parse tree, so lint never matches on text.
  • The lint report moves to format_version 3, since a new code is a contract change. docs/lint-report.md and the reference doc describe the code.
  • An integration test runs each flagged form inside a block against a real server and requires it to be refused with active_sql_transaction.
New text output
$ pg-sprite lint changes.sql
changes.sql:2:1:
  CREATE INDEX CONCURRENTLY i ON t (c);

error[concurrent-in-transaction]:
  CREATE INDEX i — PostgreSQL refuses CONCURRENTLY inside a transaction
  block, so the script fails here and the block rolls back

help:
  run this statement on its own, outside the BEGIN … COMMIT block

docs:
  https://github.com/block/pg-sprite/blob/main/docs/postgres-online-ddl-reference.md#concurrent-in-transaction

Before this change the same statement printed no finding; only BEGIN and COMMIT were flagged, as unrecognized operations.

🤖 Generated with Claude Code

PostgreSQL refuses every CONCURRENTLY form inside an explicit block, so
a script that wraps one in BEGIN ... COMMIT fails at that statement and
rolls back. Lint now tracks the block a script opens and reports the
operation as concurrent-in-transaction, an error, with how to fix it.
statement.TransactionControlOf reads BEGIN, COMMIT, and their variants
from the parse tree. The lint report moves to format_version 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Explicitly disabled REINDEX concurrency incorrectly triggers the new transaction error.

1 open issue
What changed in this PR

Adds script-level transaction tracking to pg-sprite lint so concurrent operations inside explicit transaction blocks produce an error.

Changes:

  • Parses transaction controls and emits concurrent-in-transaction findings.
  • Adds unit, renderer, and PostgreSQL integration tests.
  • Updates the lint report to version 3, documentation, and demo assertions.
File Description
pkg/​statement/​transaction.go Parses transaction-control effects.
pkg/​statement/​transaction_test.go Tests transaction-control classification.
pkg/​lint/​transaction_integration_test.go Checks PostgreSQL refusal of concurrent operations.
pkg/​lint/​lint.go Tracks blocks and emits the new diagnostic.
pkg/​lint/​lint_test.go Tests block tracking and report version.
internal/​cli/​lint_text.go Renders diagnostic explanation and help.
internal/​cli/​lint_test.go Tests diagnostic text output.
docs/​postgres-online-ddl-reference.md Explains the new diagnostic.
docs/​lint-report.md Documents version 3 and the new code.
docs/​cli-output-examples.md Updates the JSON example version.
demo/​tour.sh Expects lint report version 3.

Review effort: Balanced

Comment thread pkg/lint/lint.go
REINDEX (CONCURRENTLY false) and (CONCURRENTLY 0) rebuild with the
blocking lock, which PostgreSQL runs inside a transaction block, but the
parser counted any CONCURRENTLY option as concurrent. Lint flagged them
as concurrent-in-transaction, and the concurrent rewrite appended a
second CONCURRENTLY option the server refuses as conflicting.

The option's value is now read as the server's defGetBoolean reads it,
through one helper that replaces the duplicate in ops.go, and the
rewrite replaces a disabled option instead of adding one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aparajon
aparajon marked this pull request as ready for review October 17, 2026 16:13
…t-in-transaction

# Conflicts:
#	pkg/lint/lint.go
#	pkg/lint/lint_test.go
#	pkg/statement/rewrite.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants