MP091warning

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

migrationpilot analyze migration.sql
⚠ [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:

.migrationpilotrc.yml
rules:
  MP091: false

# or keep it, and downgrade it
rules:
  MP091:
    severity: warning

Try 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

Related rules