require-partition-key-in-pk
Partitioned table primary key must include all partition key columns. PostgreSQL rejects it otherwise.
- operation
- Partitions
- lock taken
- no table lock
- remediation
- Manual rewrite
- category
- Partitioning
- Breaks a rolling deploy
What triggers it
A CreateStmt that has a non-empty partspec.partParams, has tableElts, declares a primary key somewhere in those elements, and whose partition key columns are not all present in that key. Partition keys come from partParams[].PartitionElem.name; primary key columns come from column-level CONSTR_PRIMARY constraints (contributing that column name) and table-level Constraint.keys[].String.sval. The comparison is a plain case-sensitive Array.includes between the two name lists.
What does not
Non-partitioned CREATE TABLE, which has no partspec. A partitioned table with no primary key at all — the hasPK flag stays false and the rule stands down, leaving that case to MP045. A partition key made entirely of expressions, for example PARTITION BY RANGE ((a + b)), because a PartitionElem holding an expr has no name and gets filtered out, emptying the key list. ALTER TABLE ... ADD PRIMARY KEY on an existing partitioned table, which is not a CreateStmt.
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
There is nothing to lock. This is a hard-error rule: PostgreSQL rejects the statement at parse-analysis time with "unique constraint on partitioned table must include all partitioning columns", so the migration fails on the spot rather than doing damage. The value of catching it statically is that it fails in CI instead of mid-deploy.
Why it matters
PostgreSQL requires that the primary key (and all unique constraints) on a partitioned table include all partition key columns. Uniqueness can only be enforced per-partition, so the partition key must be part of the constraint. If omitted, the CREATE TABLE will fail at runtime.
Unsafe, and safe
Flagged
CREATE TABLE events ( id bigint PRIMARY KEY, created_at timestamptz NOT NULL, data jsonb ) PARTITION BY RANGE (created_at); -- ERROR: insufficient columns in PRIMARY KEY
Safe alternative
CREATE TABLE events ( id bigint NOT NULL, created_at timestamptz NOT NULL, data jsonb, PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (created_at);
What it assumes
Only the primary key is checked. PostgreSQL applies the identical requirement to every UNIQUE constraint on a partitioned table, and whyItMatters even says so, but CONSTR_UNIQUE is never inspected — so a partitioned table with a bad UNIQUE and a good PK passes clean and then fails at deploy. Name matching is exact and case-sensitive, so a quoted mixed-case partition key compared against an unquoted key column can produce a spurious "missing" report. Mixed expression-and-column partition keys are partly checked: the expression components are silently dropped from the comparison.
What backs this rule
Every rule is a claim about PostgreSQL, so it carries what the claim rests on: a handbook chapter that cites the manual, the incidents that put it there, and the version it was last checked against.
Partition maintenance is usually automated — a nightly job adding tomorrow's partition and dropping last month's — which means it runs unattended, against a busy table, at whatever hour you picked.
- verified against
- PostgreSQL 17.10
- last checked
- 2026-08-11
- confidence
- High
Public incidents and write-ups
What the CLI prints
✗ [MP049] CRITICAL (line 1) Partitioned table "events" primary key does not include partition key column(s): created_at. PostgreSQL requires all partition key columns in the primary key. Safe alternative: -- Include partition key columns in the primary key: -- PRIMARY KEY (id, created_at) Why: PostgreSQL requires that the primary key (and all unique constraints) on a partitioned table include all partition key columns. If omitted, the CREATE TABLE will fail at runtime. This ensures uniqueness can be enforced per-partition. Docs: https://migrationpilot.dev/rules/mp049
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 MP049 CREATE TABLE events (
For the whole project, in .migrationpilotrc.yml — by name or by id:
rules:
MP049: false
# or keep it, and downgrade it
rules:
MP049:
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 MP049 in the playground