RS-DEPLOY-002 — Changing REPLICA IDENTITY changes what downstream consumers can identify
Namespace: RS-DEPLOY · Code: RS-DEPLOY-002
What it means
Section titled “What it means”REPLICA IDENTITY controls what a logical decoding stream can identify a changed row BY. Changing it raises no error and does not affect queries, so nothing about applying the migration reveals a problem — but logical replication subscribers, CDC pipelines and analytics mirrors can silently start receiving UPDATE and DELETE events they cannot match to a row. NOTHING is the extreme case: those events carry no old-row identity at all. FULL is the safe-but-costly direction, writing the entire old row into WAL on every UPDATE and DELETE.
Why it matters
Section titled “Why it matters”Nothing fails where you can see it. A logical replication subscriber or CDC pipeline can start rejecting or skipping updates and deletes it can no longer match to a row, and the first symptom is data drift somewhere downstream.
Example
Section titled “Example”Production shape: public.events has 20,000,000 rows; whether anything consumes its changes is not something a fixture records. This migration produces RS-DEPLOY-002 against it:
ALTER TABLE public.events REPLICA IDENTITY NOTHING;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: 20000000, confidence: exact} columns: id: {type: bigint, nullable: false, unique: {value: true, confidence: exact, via: probe}} note: {type: text, nullable: false} created_at: {type: timestamp with time zone, nullable: false} indexes: [{name: events_note_idx, columns: [note], bytes: 400000000}]How to fix
Section titled “How to fix”Check what consumes this table’s changes before changing its identity: SELECT * FROM pg_publication_tables WHERE tablename = ‘<table>’; and check for active replication slots with SELECT * FROM pg_replication_slots. If you are dropping the index that backs REPLICA IDENTITY USING INDEX, set a new identity BEFORE dropping it, not after. If you need FULL, size the WAL increase first — it is proportional to update volume, not table size.
For the example above. Check what consumes the table’s changes before changing its identity, on the production database.
-- Run these first, against production:-- SELECT pubname FROM pg_publication_tables WHERE schemaname = 'public' AND tablename = 'events';-- SELECT slot_name, plugin, active FROM pg_replication_slots;-- Only if nothing consumes the table (or every consumer is ready):ALTER TABLE public.events REPLICA IDENTITY NOTHING;Related codes
Section titled “Related codes”RS-INDEX-003— DROP INDEX without CONCURRENTLY takes ACCESS EXCLUSIVE on the tableRS-DEPLOY-001— Renaming a column or table breaks running code that still uses the old nameRS-DEPLOY-003— Dropping NOT NULL withdraws a guarantee running code relies on
This page is generated from the same catalog rowshape explain RS-DEPLOY-002 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-DEPLOY-002 and the fix must not fail.