What goes wrong with a table that has no primary key?
Table created without a primary key
Note: this works and blocks nothing, but a performance regression is likely.
What happens
A table without a primary key (or any unique constraint) works until it doesn't: logical replication rejects UPDATE/DELETE without a replica identity, targeted row operations degrade to full scans, and maintenance tooling that addresses rows by key can't help you. Adding a key later on a populated table is exactly the migration this tool exists to warn about.
Why it is dangerous on a populated table
Nothing blocks, but the cost scales with the table and shows up as a slower query plan or a longer maintenance window rather than an outage.
Fires on
CREATE TABLE audit_log (entry text, created_at timestamptz);The safe pattern
Give every table a primary key at creation, when it costs nothing, a bigint identity column is the default answer. It provides the replica identity logical replication requires, and spares you the ADD PRIMARY KEY migration on a populated table later.
CREATE TABLE audit_log (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
entry text,
created_at timestamptz NOT NULL DEFAULT now()
);Fixtures
The rule ships with these files and the test suite runs them on every change: the first set must fire, the second must stay silent.
Fires (1)
CREATE TABLE audit_log (entry text, created_at timestamptz);Stays silent (7)
CREATE TABLE admins () INHERITS (users);CREATE TABLE audit_log (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, entry text);CREATE TABLE audit_log (id bigint, entry text);
ALTER TABLE audit_log ADD CONSTRAINT audit_pk PRIMARY KEY (id);CREATE TABLE users_archive (LIKE users INCLUDING ALL);CREATE TABLE events_2026_09 PARTITION OF events FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');CREATE TABLE audit_log (tenant_id bigint, seq bigint, entry text, PRIMARY KEY (tenant_id, seq));CREATE TEMP TABLE staging_rows (entry text, created_at timestamptz);How to check locally
This rule runs in the hosted service on Startup and above: add --remote with a team token, or use the GitHub Action. No install, nothing leaves your machine:
npx bolvrk check migration.sql --remote