Skip to content

Make prefer-robust-stmts aware of alembic version update transactions - #1311

Merged
kodiakhq[bot] merged 4 commits into
sbdchd:masterfrom
DylanGriffith:DylanGriffith/better-alembic-transaction-detection
Aug 30, 2026
Merged

Make prefer-robust-stmts aware of alembic version update transactions#1311
kodiakhq[bot] merged 4 commits into
sbdchd:masterfrom
DylanGriffith:DylanGriffith/better-alembic-transaction-detection

Conversation

@DylanGriffith

@DylanGriffith DylanGriffith commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Today with Alembic you can write a migration like:

def upgrade() -> None:
    op.add_column('t', sa.Column('test_column', sa.Text(), nullable=True))

    with op.get_context().autocommit_block():
        op.execute("SELECT 1")

def downgrade() -> None:
    """Downgrade schema."""
    pass

This results in the following SQL:

BEGIN;
ALTER TABLE t ADD COLUMN test_column TEXT;
COMMIT;

SELECT 1;

BEGIN;
UPDATE alembic_version SET version_num='bbc810e82b46' WHERE alembic_version.version_num = 'b4d17e0c93aa';
COMMIT;

And it passes the "prefer-robust-stmts" check. But it shouldn't because as soon as you add this autocommit_block anywhere in your migration you are effectively breaking the wrapping transaction for the whole alembic migration.

And slightly surprisingly Alembic still decides to put a transaction at the start and the end of the migration.

This is quite weird behaviour from Alembic. But it is resulting in this prefer-robust-stmts lint rule assuming that everything is "OK" because it sees that the ALTER TABLE t is in "some transaction". But it happens to be the wrong transaction and can definitely be partially failed.

This PR makes a targetted implementation for alembic that looks more closely to work out whether or not the statement is actually in the same statement as the alembic_version update which is our main safety against partial failures.

For now this code only handles alembic but if similar quirky behaviour is possible with other migration frameworks (or we want to protect against users putting explicitly BEGIN ... COMMIT in their migrations) then we might want to extend this implementation to find the relevant markers from the other frameworks.

This PR also removes a comment about IF NOT EXISTS not being supported by alembic but this is out of date as you can see from
https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.add_column .

@netlify

netlify Bot commented Aug 28, 2026

Copy link
Copy Markdown

👷 Deploy request for squawkhq pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit ed5fc8e

Today with Alembic you can write a migration like:

```py
def upgrade() -> None:
    op.add_column('t', sa.Column('test_column', sa.Text(), nullable=True))

    with op.get_context().autocommit_block():
        op.execute("SELECT 1")

def downgrade() -> None:
    """Downgrade schema."""
    pass
```

This results in the following SQL:

```sql
BEGIN;
ALTER TABLE t ADD COLUMN test_column TEXT;
COMMIT;

SELECT 1;

BEGIN;
UPDATE alembic_version SET version_num='bbc810e82b46' WHERE alembic_version.version_num = 'b4d17e0c93aa';
COMMIT;
```

And it passes the "prefer-robust-stmts" check. But it shouldn't because
as soon as you add this `autocommit_block` anywhere in your migration
you are effectively breaking the wrapping migration for the whole
alembic migration.

And slightly surprisingly Alembic still decides to put a transaction at
the start and the end of the migration.

This is quite weird behaviour from Alembic. But it is resulting in this
prefer-robust-stmts lint rule assuming that everything is "OK" because
it sees that the `ALTER TABLE t` is in "some transaction". But it
happens to be the wrong transaction and can definitely be partially
failed.

This PR makes a targetted implementation for alembic that looks more
closely to work out whether or not the statement is actually in the same
statement as the `alembic_version` update which is our main safety
against partial failures.

For now this code only handles alembic but if similar quirky behaviour
is possible with other migration frameworks (or we want to protect
against users putting explicitl `BEGIN ... COMMIT` in their migrations)
then we might want to extend this implementation to find the relevant
markers from the other frameworks.

This PR also removes a comment about `IF NOT EXISTS` not being
supported by alembic but this is definitely out of date as you can see
from
https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.add_column
.
@DylanGriffith
DylanGriffith force-pushed the DylanGriffith/better-alembic-transaction-detection branch from f57e71d to eea2049 Compare August 28, 2026 12:17
ast::Stmt::Begin(_) => tx_start = Some(i),
ast::Stmt::Commit(_) | ast::Stmt::Rollback(_) => {
if let Some(start) = tx_start.take()
&& stmts[start..=i].iter().any(is_alembic_version_update)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

wondering if we can avoid this search by keeping track of the alembic_version_updates when we look through on line 26?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK I code-golfed it down a little to only check it once now. I'm not totally sure it's more readable but the logic is the same.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also it wasn't so easy to change it exactly the way you suggested because I would have had to keep a list of indexes in that line and then loop over them and see if any were inside this range and it seemed a little more awkward than this approach.

@sbdchd sbdchd Aug 30, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Oh gotcha, thanks for the PR :D

Comment thread crates/squawk_linter/src/rules/prefer_robust_stmts.rs

@sbdchd sbdchd left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

looks good, left a couple comments!

@DylanGriffith
DylanGriffith requested a review from sbdchd August 29, 2026 07:28
@DylanGriffith

Copy link
Copy Markdown
Contributor Author

Thanks @sbdchd for the quick review. I applied both of your suggestions.

@sbdchd sbdchd left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Ty!

@sbdchd sbdchd added the automerge automerge with kodiak label Aug 30, 2026
@kodiakhq
kodiakhq Bot merged commit 4d07b54 into sbdchd:master Aug 30, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge automerge with kodiak

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants