Skip to content

RS-LOCK-003 — ATTACH PARTITION validates the incoming rows under a lock on the parent

Namespace: RS-LOCK · Code: RS-LOCK-003

ATTACH PARTITION takes ACCESS EXCLUSIVE on the PARENT table — blocking every query against every partition — and then scans the incoming table to prove every row satisfies the partition bound, unless a matching CHECK constraint already exists. On a large incoming partition that scan is the outage, and it happens while the whole partitioned table is locked. Non-concurrent DETACH takes the same lock on the parent.

The lock is on the parent, so queries against every partition wait, not only queries on the new one, and the scan that checks the bound runs while they wait.

Production shape: public.events is partitioned by month on created_at, and public.events_2026_10 is an ordinary 20,000,000-row table about to become October’s partition. This migration produces RS-LOCK-003 against it:

ALTER TABLE public.events ATTACH PARTITION public.events_2026_10
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
The fixture this example is checked against
rowshape_fixture: "1"
meta: {id: example, engine: {name: postgres, version: "16"}, privacy: standard, profile: {mode: fast, escalated: []}}
tables:
public.events:
rows: {value: 200000000, confidence: exact}
partitions: {count: 12, strategy: range, key: created_at}
columns:
id: {type: bigint, nullable: false}
created_at: {type: timestamp with time zone, nullable: false}
public.events_2026_10:
rows: {value: 20000000, confidence: exact}
columns:
id: {type: bigint, nullable: false}
created_at: {type: timestamp with time zone, nullable: false, range: {min: "2026-10-01T00:00:00Z", max: "2026-10-31T23:59:59Z"}}

Add a CHECK constraint matching the partition bound to the incoming table BEFORE attaching, and validate it separately: ALTER TABLE incoming ADD CONSTRAINT c CHECK (<bound>) NOT VALID; then VALIDATE CONSTRAINT c; then ATTACH. With the constraint already proven, ATTACH skips the scan and the lock is brief. On PostgreSQL 14+, prefer DETACH PARTITION CONCURRENTLY for the reverse direction.

For the example above. Prove the bound on the incoming table first with a CHECK that matches it, validated without the exclusive lock. ATTACH then skips its scan, so the lock on the parent is brief.

Migration 1
SET lock_timeout = '3s';
ALTER TABLE public.events_2026_10 ADD CONSTRAINT events_2026_10_bound
CHECK (created_at >= '2026-10-01' AND created_at < '2026-11-01') NOT VALID;
Migration 2
ALTER TABLE public.events_2026_10 VALIDATE CONSTRAINT events_2026_10_bound;
Migration 3
SET lock_timeout = '3s';
ALTER TABLE public.events ATTACH PARTITION public.events_2026_10
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
  • RS-CONSTRAINT-020 — ADD CONSTRAINT without NOT VALID scans the whole table under ACCESS EXCLUSIVE
  • RS-LOCK-010 — The migration takes ACCESS EXCLUSIVE without setting lock_timeout

This page is generated from the same catalog rowshape explain RS-LOCK-003 reads, so the remediation here is byte-identical to the one a verdict carries — they cannot drift. An agent can read it with the explain_finding MCP tool. The example is run against PostgreSQL 16 by rowshape’s test suite: the first migration must produce RS-LOCK-003 and the fix must not fail.