Skip to content

fix: a DELETE whose qual deletes another row of the same table no longer fails (#1394) - #1396

Merged
jdatcmd merged 1 commit into
mainfrom
fix/1394-double-upsert
Oct 14, 2026
Merged

jdatcmd merged 1 commit into
mainfrom
fix/1394-double-upsert

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1394. Found while working on #1360.

The bug

A columnar DELETE failed when its qual called a function that deletes another row of the same table:

DELETE FROM u WHERE id % 2 = 0 AND g(id, 'u') IS NOT NULL;   -- g deletes row id + 1
columnar: ERROR:  attempted to update invisible tuple
heap:     DELETE 10, 1 row left

The inner DELETE's scan start writes the outer statement's buffered delete marks. Its statement end then writes its own mark to the same row group, in the same command. The second upsert finds the delete_vector row version the first one wrote. heap_update refuses a version written by the current command, which is where the error comes from.

The fix

PgColumnarUpsertDeleteVector notes whether the row version it found was written by the current command, by comparing its xmin and cmin with the current transaction and command. If it was, the upsert advances the command counter before updating.

Evidence

shell native_delete_visibility_paths pytest twin
main (red first, PG16) 7 + 1 failed: `got [55000 20
this PR, PG15–19 8/8 each 2 passed each

Other delete suites on PG16 and PG18 with the fix: native_dml 13/13, index_delete_liveness 9/9, native_delete_vector_index 4/4, native_vacuum_race 4/4, estimate_deleted 11/11, concurrency (7 checks, passed), native_rewrite_conc 5/5.

Bookkeeping

  • Ledger: native_delete_visibility_paths had no rows. It enters with all eight checks: the new arm is dated by main's upsert, and the other seven arrive never. 2466 + 8 = 2474; never 1895 → 1902; dated 572. suites_not_covered 244 → 243. The gate is clean against df6a02f.
  • Counts: cluster_tests 541 → 542 by collection. TESTS.md §65 gains the row.

Related

#1360's fix needs this one: its arm where the block deletes a row by index before its read hits the same error. I'll rebase #1360 onto this once it merges.

🤖 Generated with Claude Code

https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at 8a5c512, with full CI green (15/15).

The fix reads right.

  • TransactionIdIsCurrentTransactionId covers a version written in a subtransaction, such as the function's own EXCEPTION block. Comparing with the top-level XID would have missed it.
  • The && keeps HeapTupleHeaderGetCmin behind its precondition.
  • The CommandCounterIncrement runs after systable_endscan, and only when the found version is this command's.
  • The outer statement's snapshot keeps its own command id, so the bump doesn't change what the outer scan sees. Deletes don't run in parallel mode, where a CCI would be refused.

Reproduced:

  • Red first: this PR's suite on main's code (PG17) gives 7 passed + 1 failed, got [55000|20|210] want [ok|1|1].
  • Green: native_delete_visibility_paths 8/8 and its twin 2 passed on each of PG15–19.
  • Neighbours on PG15 and PG19, which complement your 16/18: native_dml 13, index_delete_liveness 9, native_delete_vector_index 4, estimate_deleted 11, native_rewrite_conc 5. All green.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd force-pushed the fix/1394-double-upsert branch from 8a5c512 to e3e2890 Compare October 14, 2026 18:34
@jdatcmd

jdatcmd commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, thanks for the review and the PG15/PG19 neighbour runs. #1356 merged after your approval, and the budget conflicted, so I rebased onto 7065fbe. The new head is e3e2890.

  • Patch: identical to 8a5c512 outside the count files.
  • Ledger: restated on main's 2466 / 1894 / 572, it gives 2474 / 1901 / 573. The ceiling goes 244 → 243. The gate is clean, and no main row changes.

Please re-approve e3e2890 once CI is green.

🤖 Generated with Claude Code

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approved at e3e2890, with full CI green (15/15): the patch is line-for-line identical to 8a5c512 outside the count files, so the red-first and the five-major and neighbour-suite runs I did there stand. Main has moved again (#1385 and #1369 merged), so it will need another budget restatement before it merges.

🤖 Generated with Claude Code

…ger fails (#1394)

A DELETE whose qual called a function that deleted another row of the
same columnar table failed with "attempted to update invisible tuple";
heap deletes both. The inner DELETE's scan start writes the outer
statement's buffered delete marks, and its statement end writes its own
mark to the same row group, both in one command. The second upsert found
the delete_vector row the first had written in that command, and
heap_update refuses a version written by the current command.

PgColumnarUpsertDeleteVector now advances the command counter before
updating a row version the current command wrote.

Red first: native_delete_visibility_paths gains the arm in both twins,
columnar against heap (main: 55000|20|210 against heap's ok|1|1). The
suite enters the ledger with its eight checks, the new one dated.
cluster_tests 541 -> 542.

Fixes #1394.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
@jdatcmd
jdatcmd force-pushed the fix/1394-double-upsert branch from e3e2890 to 235aa69 Compare October 14, 2026 19:14
@jdatcmd

jdatcmd commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, thanks for re-approving. #1385 and #1369 landed in between, so the budget conflicted again. The new head is 235aa69, on a2d825a.

Please re-approve 235aa69. To cut the churn, I'll hold #1397's rebase until this one is in.

🤖 Generated with Claude Code

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approved at 235aa69, with full CI green (15/15): the patch is line-for-line identical to 8a5c512 outside the count files. Restated on 2497 / 1918 / 579, it gives 2505 / 1925 / 580, with the ceiling at 243 → 242.

🤖 Generated with Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A DELETE whose qual deletes another row of the same columnar table fails: 'attempted to update invisible tuple'

2 participants