Skip to content

In-place TRUNCATE leaves projection row groups behind: row_group_pkey collision on the next write #896

Description

@OffgridwithJD

TRUNCATE followed by INSERT in the same transaction fails on any columnar table that has a projection, and rolls the whole transaction back.

Found while adding arms for #892. It is not caused by #892 — it reproduces on e42c80d with that branch nowhere in sight.

Reproduction

CREATE TABLE t (id int, a int, b text) USING pgcolumnar;
INSERT INTO t SELECT g, g%5, 'x'||g FROM generate_series(1,100) g;
SELECT pgcolumnar.add_projection('t','pp',ARRAY['a','b'],ARRAY['a']);

BEGIN;
  TRUNCATE t;
  INSERT INTO t SELECT g, g%5, 'y'||g FROM generate_series(1,50) g;
COMMIT;

Observed on PostgreSQL 18.4, assert build, at e42c80d:

ERROR:  duplicate key value violates unique constraint "row_group_pkey"
DETAIL:  Key (storage_id, group_number)=(10000000004, 2) already exists.

The transaction rolls back, so the table is left as it was. It fails loudly rather than losing data, which is the good outcome — but TRUNCATE then reload in one transaction is the ordinary way to refresh a table, and it is unavailable on any table carrying a projection.

Three controls, which isolate it to the projection

case result
A. same statements, no projection declared succeeds, 50 rows
B. with a projection, all in one transaction row_group_pkey violation, rolled back
C. with a projection, table created in an earlier transaction row_group_pkey violation, rolled back
D. with a projection, TRUNCATE but no INSERT after it succeeds

So it needs a projection, and it needs a write after the TRUNCATE in the same transaction. Whether the table pre-exists makes no difference.

Where I think it is, stated as a hypothesis rather than a finding

pgcolumnar_relation_set_new_filelocator already handles exactly this shape for the base relation, and src/columnar_tableam.c says so in its own words:

a transaction that writes, truncates and writes again COMMITS into storage nothing reads: the second insert reuses the stale state, flushes into the retired storage id [...] Before the delete above existed, the retired storage's catalog rows survived and the stale flush collided with them on the primary key, so the transaction ERRORed and rolled back.

That is this error, and the fix there was PgColumnarForgetWriteStateForRelation(RelationGetRelid(rel)). A projection has its own storage and its own write state, keyed separately, and I do not see an equivalent forget for it. That would explain why case A passes and case B does not, and why the symptom is the collision the comment describes rather than silent loss.

I have not confirmed that by instrumenting the write state, so treat the location as unverified. The reproduction and the four cases above are measured.

Note on severity

The comment quoted above records that the base-relation version of this bug was, at one point, silent data loss rather than an error — the collision was the only thing making it safe. If a future change deletes the retired projection rows without also forgetting the projection's write state, this becomes the silent version. That is the reason to fix it rather than document it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions