Dropping a composite attribute causes data integrity violations

Started by Nikhil Sontakke7 days ago2 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

appliessuccessCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253439
psql -h localhost -U postgres

Built from patchset v2 (message #2), August 23, 2026 at 05:52 PM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t253439_2 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t253439_2 && git checkout t253439_2

Patchset v2 (message #2) is on t253439_2

Jump to latest
#1Nikhil Sontakke
nikhil@planetscale.com

Hi,

A composite datum records the OID and typmod of its row type and nothing
about the shape it was built with, so every value is read back against
whatever the type looks like currently at read time. Nothing prevents
the below for example:

CREATE TYPE ct AS (a int, b int);
CREATE TABLE t (v ct);
CREATE UNIQUE INDEX t_v ON t (v);
INSERT INTO t VALUES (ROW(1, 2)::ct), (ROW(1, 3)::ct); -- accepted

ALTER TYPE ct DROP ATTRIBUTE b; -- accepted today! this violates UNIQUE

SELECT count(DISTINCT v) FROM t;
1

The rows are on disk and were committed before the ALTER; no cursor or
plan is involved, and the incorrect reading survives a restart.

INSERT INTO t VALUES (ROW(1)::ct);
ERROR: duplicate key value violates unique constraint "t_v"
DETAIL: Key (v)=((1)) already exists.

REINDEX INDEX t_v;
ERROR: could not create unique index "t_v"
DETAIL: Key (v)=((1)) is duplicated.

Worse, a plain pg_dump of that database fails to restore for the
same reason.

The check for this exists. On the same table, before the drop, retyping
the attribute is refused:

ALTER TYPE ct ALTER ATTRIBUTE b TYPE bigint;
ERROR: cannot alter type "ct" because column "t.v" uses it

but the call is gated on a rewrite being queued,

if (tab->newvals != NIL || tab->rewrite > 0)
find_composite_type_dependencies(...);

and a drop produces neither, so it walks past.

The same applies to ALTER TABLE ... DROP COLUMN, since a table's row type
is a composite type too, and there too the retyping form is already
refused.

The attached patch calls the existing function from the drop path, after
the column is checked for droppability and before recursion to
inheritance children, so both calls and every level are covered.
Regression, isolation and pg_upgrade suites pass.

I added new tests and had to modify a couple of existing tests because of
this
behavior change.

Regards,
Nikhil
---
Nikhil Sontakke
PlanetScale Postgres Core Team

Attachments:

t253439_1
0001-Refuse-to-drop-a-column-whose-row-rowtype-drop-guard.patchapplication/octet-stream; name=0001-Refuse-to-drop-a-column-whose-row-rowtype-drop-guard.patchDownload+95-9
#2Nikhil Sontakke
nikhil@planetscale.com
In reply to: Nikhil Sontakke (#1)
Re: Dropping a composite attribute causes data integrity violations

Hi,

I added this to the open commitfest at
https://commitfest.postgresql.org/patch/7169/

Also, please find attached v2, which addresses additional required test
changes.

Regards,
Nikhil

On Mon, Aug 17, 2026 at 12:33 PM Nikhil Sontakke <nikhil@planetscale.com>
wrote:

Show quoted text

Hi,

A composite datum records the OID and typmod of its row type and nothing
about the shape it was built with, so every value is read back against
whatever the type looks like currently at read time. Nothing prevents
the below for example:

CREATE TYPE ct AS (a int, b int);
CREATE TABLE t (v ct);
CREATE UNIQUE INDEX t_v ON t (v);
INSERT INTO t VALUES (ROW(1, 2)::ct), (ROW(1, 3)::ct); -- accepted

ALTER TYPE ct DROP ATTRIBUTE b; -- accepted today! this violates UNIQUE

SELECT count(DISTINCT v) FROM t;
1

The rows are on disk and were committed before the ALTER; no cursor or
plan is involved, and the incorrect reading survives a restart.

INSERT INTO t VALUES (ROW(1)::ct);
ERROR: duplicate key value violates unique constraint "t_v"
DETAIL: Key (v)=((1)) already exists.

REINDEX INDEX t_v;
ERROR: could not create unique index "t_v"
DETAIL: Key (v)=((1)) is duplicated.

Worse, a plain pg_dump of that database fails to restore for the
same reason.

The check for this exists. On the same table, before the drop, retyping
the attribute is refused:

ALTER TYPE ct ALTER ATTRIBUTE b TYPE bigint;
ERROR: cannot alter type "ct" because column "t.v" uses it

but the call is gated on a rewrite being queued,

if (tab->newvals != NIL || tab->rewrite > 0)
find_composite_type_dependencies(...);

and a drop produces neither, so it walks past.

The same applies to ALTER TABLE ... DROP COLUMN, since a table's row type
is a composite type too, and there too the retyping form is already
refused.

The attached patch calls the existing function from the drop path, after
the column is checked for droppability and before recursion to
inheritance children, so both calls and every level are covered.
Regression, isolation and pg_upgrade suites pass.

I added new tests and had to modify a couple of existing tests because of
this
behavior change.

Regards,
Nikhil
---
Nikhil Sontakke
PlanetScale Postgres Core Team

Attachments:

t253439_2
v2-0001-Refuse-to-drop-a-column-whose-row-type-is-stored-.patchapplication/octet-stream; name=v2-0001-Refuse-to-drop-a-column-whose-row-type-is-stored-.patchDownload+101-12