Skip to main content

lockcheck

Finds Alembic migrations that will lock a Postgres table, before they run.

CI Python License Dependencies

pip install lockcheck
lockcheck

The line that takes the site down

op.create_index("ix_clicks_clicked_at", "clicks", ["clicked_at"])

One line, no obvious cost, reviewed and approved. On the developer's machine the table has forty rows and it finishes instantly. In production it has forty million, Postgres holds a lock that blocks every write to clicks until the index is built, and everything writing to that table stops — for as long as it takes.

Nobody finds out at review time. They find out during the deploy.

alembic/versions/0002_click_rollups.py
    47  LC002  CREATE INDEX without CONCURRENTLY blocks writes for the whole build
            The table takes a SHARE lock until the index finishes, so every write waits
            -- minutes on a large table, and the queue behind it outlives the migration.
            Pass postgresql_concurrently=True and run it outside the transaction (see
            LC003).

1 problem found.

Exit code 1, so CI stops. Every finding names the fix, because a linter that reports a problem and leaves the answer as an exercise gets silenced by the first person in a hurry.

The rules

LC001 adding a NOT NULL column with no default rewrites the table, or fails
LC002 CREATE INDEX without CONCURRENTLY blocks writes for the whole build
LC003 CREATE INDEX CONCURRENTLY inside a transaction, which cannot work
LC004 SET NOT NULL scans the whole table under ACCESS EXCLUSIVE
LC005 changing a column type rewrites the entire table
LC006 a foreign key or check constraint added without NOT VALID
LC007 a unique constraint, which builds its index the blocking way

lockcheck --list-rules prints them; each finding prints the full explanation.

What it does not report

This matters more than the list above. A linter is judged on its false positives, and the first thing this one did against a real project was produce ten findings that were all noise — indexes on tables created three lines earlier, empty and invisible to anything else.

So a table created or dropped in the same migration is not locked, and operations on it are not reported. create_table is never flagged for the same reason: a table nothing has touched yet has nothing to block.

A table named by anything other than a literal string gets the cautious answer and is treated as pre-existing. Reporting something harmless costs a moment; staying quiet about a real lock costs an outage.

What it cannot see

Raw SQL. op.execute("CREATE INDEX ...") is a string as far as Python is concerned. Half-parsing SQL by hand would be a worse lie than not parsing it, so this says so instead.

Deploy ordering. drop_column and rename_table take no meaningful lock and will still break the application running against the old schema. That is a real problem and a different one, about the order of deploys rather than the shape of a migration.

Your data. Every rule here is about what Postgres would do to a large table. On a table with a hundred rows, all of them are noise — which is what --ignore and the # lockcheck: ignore comment are for.

Silencing

op.create_index("ix", "settings", ["key"])  # lockcheck: ignore[LC002]

Bare # lockcheck: ignore silences the line; the bracketed form names rules. The comment counts anywhere in a multi-line call, which is where anyone would write it — beside the argument that made it safe.

Across a whole project, --ignore LC002,LC004.

In CI

- run: pip install lockcheck
- run: lockcheck

With no arguments it checks alembic/versions. Give it paths for anywhere else.

How it reads a migration

The file is parsed, not searched. A regular expression finds op.create_index( and then has no idea whether the call three lines down passed postgresql_concurrently=True, whether it sits inside an autocommit block, or whether the whole thing is inside a docstring. Each of those changes the answer, and the calls this exists to catch are exactly the ones long enough to wrap across lines.

Nothing is imported or executed. Reading a migration should not be able to run one.

Requirements

Python 3.10 or newer. No dependenciesast and argparse from the standard library. A linter that drags a dependency tree into someone's CI is one more thing for them to resolve, pin and upgrade, and this needs none of it.

Alembic itself is not required. It is not imported.

Tests

pytest

No database, no services, no fixtures to stand up. It is a static analyser, so the whole suite runs in about a tenth of a second.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lockcheck-0.1.0.tar.gz (16.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lockcheck-0.1.0-py3-none-any.whl (12.9 kB view details)

Uploaded Python 3

File details

Details for the file lockcheck-0.1.0.tar.gz.

File metadata

  • Download URL: lockcheck-0.1.0.tar.gz
  • Upload date:
  • Size: 16.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for lockcheck-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4058ea0904e37bfce571100c74734cc69991ce45c38baa887fef50874baff198
MD5 f0ae7fe92e5a9c8ffb737f534c943190
BLAKE2b-256 944679ed3cfa2e9e5872f1bfcd885ed575b2ce094a7c1f5e57385e44479cb33e

See more details on using hashes here.

Provenance

The following attestation bundles were made for lockcheck-0.1.0.tar.gz:

Publisher: release.yml on Nappuccino-tlg/lockcheck

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file lockcheck-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: lockcheck-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 12.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for lockcheck-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2b5745e01b8c4509f4310869b14e135c36119a624e95c8fb9fcfd8bc43655f99
MD5 f122132036fa5e4b7915804f435bef6a
BLAKE2b-256 9e33d73e504ee33289e18561d6fcd4efe5d4ef5744b2a7d838995b8ff2145f62

See more details on using hashes here.

Provenance

The following attestation bundles were made for lockcheck-0.1.0-py3-none-any.whl:

Publisher: release.yml on Nappuccino-tlg/lockcheck

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page