warn-privilege-drift
GRANT/REVOKE mixed into a DDL migration makes access-control changes invisible to review and impossible to audit in one place.
- operation
- Privileges and RLS
- lock taken
- no table lock
- remediation
- Manual rewrite
- category
- Privileges & RLS
What triggers it
A GrantStmt or GrantRoleStmt in a migration file where at least one other statement is DDL (per isDDL), reported once — on the first privilege statement in the file, found by checking that no earlier statement in ctx.allStatements was itself a GrantStmt/GrantRoleStmt.
What does not
Non-privilege statements. A migration containing only GRANT/REVOKE with no DDL at all. A migration containing only DDL with no privilege statements. And every privilege statement after the first one in the same file — those don't generate a second violation.
Where it applies
Applies to every PostgreSQL version MigrationPilot targets. It works on the SQL text alone — no database connection needed.
The lock, and what it blocks
None — the concern is auditability, not locking.
Why it matters
A GRANT in the middle of a schema migration is a permanent access-control change being reviewed as though it were a schema change — the reviewer is checking whether the column type is right, and the privilege line goes past in the same diff. The durable problem is auditing: "who can read this table, and who approved that" has no answer short of replaying every migration in order. Rollback is asymmetric too, because reverting the schema change does not revert the grant.
Unsafe, and safe
Flagged
-- migrations/012_add_reports.sql CREATE TABLE reports (id BIGINT PRIMARY KEY, body TEXT); CREATE INDEX idx_reports_created ON reports (created_at); GRANT SELECT ON reports TO analyst; -- Access decision buried in a schema diff
Safe alternative
-- migrations/012_add_reports.sql — schema only CREATE TABLE reports (id BIGINT PRIMARY KEY, body TEXT); CREATE INDEX idx_reports_created ON reports (created_at); -- The GRANT moves to 013_grant_reports_access.sql, so a search for -- GRANT across the migrations directory returns something meaningful.
What it assumes
Assumes any privilege statement sharing a file with DDL is worth separating, regardless of how small either side is — a single narrowly-scoped GRANT next to a trivial schema tweak is flagged the same as a sweeping access change buried in a large migration.
What the CLI prints
⚠ [MP091] WARNING (line 4) GRANT on "reports" sits in a migration that also changes schema. Access-control changes reviewed as schema changes drift out of sight. Move privileges into their own migration. Safe alternative: -- Split the file so each change is reviewed as what it is: -- migrations/012_add_reports_table.sql (DDL only) -- migrations/013_grant_reports_access.sql (GRANT/REVOKE only) Why: A GRANT in the middle of a schema migration is a permanent access-control change reviewed as though it were a schema change. Nobody can answer "who can read this table and who approved that" without replaying every migration in order, because that history is the only record. Rolling back the schema change does not roll back the privilege either: DDL rollbacks restore structure, not the grants that came with it. Keeping privileges in their own migrations gives the access model one reviewable home. Docs: https://migrationpilot.dev/rules/mp091
Generated by running the CLI's own formatter over the flagged example above, so it is the text the tool actually produces. A real run also reports the other rules that fire on the same statement; those blocks are left out here.
Turning it off
For one statement, put a comment on the line before it:
-- migrationpilot-disable MP091 CREATE TABLE reports (id BIGINT PRIMARY KEY, body TEXT);
For the whole project, in .migrationpilotrc.yml — by name or by id:
rules:
MP091: false
# or keep it, and downgrade it
rules:
MP091:
severity: warningTry it
Open this rule's flagged example in the playground. It runs in your browser — edit it and watch the finding appear and disappear.
Run MP091 in the playground