Skip to content

test: a savepoint rolled back, or a statement failed, after a flush (#1341) - #1430

Merged
jdatcmd merged 20 commits into
mainfrom
test/1341-savepoint-flush
Oct 15, 2026
Merged

jdatcmd merged 20 commits into
mainfrom
test/1341-savepoint-flush

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Covers the #1341 core-am item on rolling back to a savepoint, or failing a statement, after rows have been flushed. Tests only; no product code changes.

What it holds

A row group flushed inside a savepoint is a catalog row written under the subtransaction, so the abort must hide it. The savepoint's buffered rows and buffered delete marks must be thrown away.

New suite test/savepoint_flush.sh and twin test_savepoint_flush.py. A columnar table and a heap table run every shape, heap is the answer, and row groups hold 1000 rows:

  1. A flushed INSERT rolled back to a savepoint, at READ COMMITTED and at REPEATABLE READ.
  2. The same unique keys inserted again after ROLLBACK TO, in the same transaction.
  3. An UPDATE of every row rolled back to a savepoint.
  4. An INSERT and an UPDATE that fail after flushing: an INSERT alone, then each inside a savepoint followed by more work.

Four readers compare each result, inside the transaction and after COMMIT:

  • a plain sequential scan (enable_custom_scan off);
  • the custom scan with enable_vectorization off;
  • the vectorized count;
  • the primary key's index.

Premises pin each reader's plan and show that each write really flushed. Each transaction runs one statement at a time and goes on after an error, as psql does with ON_ERROR_STOP off. psql -c would send the script as one query, and the error would skip the ROLLBACK TO.

Evidence

Clean: 28/28 in both twins on each of PG15 to PG19.

Mutants (PG17). Each mutant was checked as applied, then the source was restored:

mutant checks red (shell)
the abort keeps the savepoint's buffered rows (PgColumnarWriteStateDiscardSubXact not called) 5: the failing INSERT and UPDATE arms
the abort keeps its buffered delete marks (PgColumnarDeleteVectorDiscardSubXact not called) 2: the failing UPDATE arms
PgColumnarReadRowGroupList under SnapshotAny 20: every data arm. The later shapes also fail because the first shape's rows came back
every delete_vector read under SnapshotAny 7: the rolled-back UPDATE arms, and the arms after them

Under each mutant, both twins fail first on the same check, with identical values. For example, under the buffered-rows mutant both print seq: columnar=[7501|76165253|15009] heap=[7002|64563503|15010].

Two things that did not work, stated:

  • The group-list memo reset (opt: index/bitmap fetch does O(K·G) linear row-group search #709). Removing PgColumnarGroupMemoReset(true) from the abort turns nothing red here, even at REPEATABLE READ. It is a live mutation: it turns red native_fetch_group_memo's cursor arm (MOVE 102990, want MOVE 99990). The memo matters only to a scan held open across the abort, and that cursor arm is its guard, so this suite claims nothing about it.
  • Mutating PgColumnarReadDeleteVectorList alone turns nothing red. PgColumnarStorageHasDeleteVector still answers "no marks" under the real snapshot. Only the mutant on every delete-vector read gets past both gates.

Bookkeeping:

  • Ledger: savepoint_flush is seeded through pgc_ledger.py merge from the five clean logs and the four mutant logs, with each red named by --target. 20 rows are dated and 8 are never: the reader-path premises, the flush premises, and two "it ran" arms. The census is 2774 + 28 = 2802 rows, 2095 + 8 = 2103 never, and 679 + 20 = 699 dated.
  • Gate: gate --against auto gives rc=0: new this run=0, the census agrees, and the ceiling stays at 234 -> 234.
  • Counts: cluster_tests goes 565 -> 566, by collection. TESTS.md gains §115.
  • Other suites: the guard set passes 433/433, harness_selftest 1245/1245 (PG17), and docs_style passes. test_compare_to_bash.py passes 40/40, with savepoint_flush in COMPLETE.

Conflicts: like my other open #1341 PRs, this one edits the ledger, the budget, TESTS.md §115 and cluster_tests. Whichever merges first, I'll restate the rest.

🤖 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.

@jdatcmd Reviewed f8de866.

  • What it holds: a savepoint abort after a flush hides the groups the savepoint flushed, and throws away its buffered rows and buffered delete marks. Four readers check every shape against heap, inside the transaction and after COMMIT. The premises (reader plans, INSERT flushes) are asserted, and both tables run every transaction. A good suite.

  • Red (PG17): your four mutants reproduce exactly, at 5, 2, 20 and 7 checks. The .so md5 changed for each; restored, the md5 is back to 24fa0ab4 and the suite is 28/28.

    • The discard-rows mutant: and in the same transaction every reader sees only the rows written after it, as heap does: got [ seq: columnar=[7501|76165253|15009] heap=[7002|64563503|15010] .... The twin stops on the same check with the same values.
    • An independent mutant of mine, the abort calling PgColumnarWriteStatePromoteSubXact and PgColumnarDeleteVectorPromoteSubXact instead of the discards: 5 red, including columnar=[2|60003|111] heap=[2002|2061003|110] in isolation.
  • Green: 28/28 for the suite and for the twin, on PG17 and PG18. The tsv census, 2802 / 2103 never / 699 dated, matches the budget, all 28 check names have rows, and cluster_tests 566 matches collection over the complement of NO_CLUSTER.

  • Blocker: six rows are dated only by a cascade. I ran each shape alone, on freshly created tables, against each mutant. These six are dated only by the SnapshotAny row-group-list mutant, and they go red only because an earlier shape's aborted groups came back:

    • at REPEATABLE READ, a savepoint's INSERT runs, and ROLLBACK TO it, on both tables
    • at REPEATABLE READ, inside the savepoint, every reader saw the INSERT, as heap does
    • an UPDATE of every row in a savepoint runs, and ROLLBACK TO it, on both tables
    • inside the savepoint, the UPDATE changed each row once, as heap does
    • an INSERT that fails after flushing is refused on both tables
    • in a savepoint, an INSERT that fails after flushing raises its error and nothing else, on both tables

    In the full run the failure text names someone else's rows. The failing INSERT, for example, is refused with duplicate key value violates unique constraint "sp_pgcolumnar_pkey" because the flush premise's ROLLBACK left 20001..24000 behind, and the UPDATE arm fails with null value in column "id" ... violates not-null constraint. Run alone, all six pass under that mutant. Your own note in the description, "the later shapes also fail because the first shape's rows came back", describes exactly this. Either give each shape its own fresh table and merge the run again, or move these six to never.

  • The same issue in the mutation column. Five rows list the every-delete_vector-read mutant only because of the rolled-back UPDATE before them: and no reader sees any of its rows, the two only the rows written after it rows, and the two as they were, plus the later UPDATE rows. In the full run they show columnar=[0|0|0] or [2|60003|…], meaning every row that UPDATE marked stayed deleted; run alone, that mutant turns none of them red. Likewise the failing-UPDATE error row lists the row-group-list mutant, but alone only the discard-rows mutant reddens it. These rows keep their dates through other mutants, but the column should name only the mutants that actually turn them red.

  • Blocker: the UPDATE arms have no flush premise, and the failing UPDATE doesn't flush delete marks. The description says the premises show "that each write really flushed", but only the two INSERTs have one. Measured with n_tup_ins from pg_stat_xact_all_tables, read after ROLLBACK TO (it still counts the aborted subtransaction's catalog inserts):

    • the rolled-back UPDATE writes 2 row_group rows and 2 delete_vector rows;
    • the failing UPDATE, in the suite's own table state, writes 7 row_group rows and 0 delete_vector rows.

    So the comment at savepoint_flush.sh:187-188 (and the twin's line 220), that it fails after flushing new versions and delete marks of the rows before it, is half false. The marks are still buffered when it fails, which is why the discard-marks mutant catches it and the delete_vector-read mutant can't. Please add a flush premise to both UPDATE arms (that counter, asserted > 0, would do). Then either reshape the failing UPDATE so it flushes marks before it fails, or correct the comment and the description.

  • Rebase: #1403 has landed, so the budget and expected_tests.txt conflict. On main the census would be 2811/2103/708 as written, or 2811/2109/702 with the six rows moved to never, and cluster_tests would be 567.

🤖 Generated with Claude Code

jdatcmd added a commit that referenced this pull request Oct 15, 2026
…Es prove they flushed (#1341)

Review of #1430 by OffgridwithJD. Three changes:

- Every shape starts from fresh tables. Before, a shape that went wrong under
  a mutant poisoned the shapes after it, so six rows were dated only by a
  cascade and five more named a mutant that never reddened them alone.
- Both UPDATE arms get a flush premise: this transaction's inserts into
  row_group and delete_vector, read from pg_stat_xact_all_tables right after
  ROLLBACK TO. The failing UPDATE flushed no delete marks before it failed,
  so it now reads its own table at row 2500 (sp_probe), which flushes them.
  With that read disabled, the premise reads 4 row_group inserts and 0
  delete_vector inserts and goes red.
- The index reader counts id > 1000, so it reads rows in the 5000-row shapes.

The ledger rows are regenerated from isolated runs. The two "abort keeps its
buffer" mutants now trip #1403's flushedUnder assertion and crash the
server, so they date nothing; the abort promoting its buffers instead of
discarding them (OffgridwithJD's mutant) takes their place. 30/30 in both
twins on PG15-19. 14 checks dated, 16 never; census 2816 / 2111 / 705.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdatcmd
jdatcmd force-pushed the test/1341-savepoint-flush branch from f8de866 to 2a60f80 Compare October 15, 2026 16:32
@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, addressed in 73b7054, rebased onto main 1f7b5da. I pushed the first round at 16:49 without replying, sorry. Here's all of it.

1. Every shape starts from fresh tables, in both twins (fresh / _fresh), so no shape is dated by an earlier one's damage. The index reader now counts id > 1000, so it reads rows in the 5000-row shapes.

2. Both UPDATE arms have a flush premise: this transaction's inserts into row_group and delete_vector, from pg_stat_xact_all_tables, read right after ROLLBACK TO.

  • The failing UPDATE now flushes its delete marks before it fails. It calls sp_probe, which reads its own table at row 2500 (a scan starting flushes the buffered marks), and it divides by zero at row 5000.
  • The premise can fail: with the probe's read moved off the table's range, it reads row_group inserts [4], delete_vector inserts [0] and goes red, as you measured.
  • The comment and TESTS.md now say what happens.

3. Re-dated from isolated runs. Under each mutant, no trap and no recovery. Both twins fail first on the same check, with the same values:

mutant red (shell)
row-group list under SnapshotAny 14
every delete_vector read under SnapshotAny 4: the rolled-back UPDATE, and now the failing UPDATE too, since its marks are flushed
your mutant: the abort promotes its buffers instead of discarding them 4: the failing INSERT and UPDATE arms
  • The two "the abort keeps its buffer" mutants are dropped. On this tree, after fix: a read inside an aborted inner block no longer loses the enclosing statement's rows (#1360) #1403, both trip Assert(... flushedUnder == InvalidSubTransactionId) at commit and crash the server, so their reds were the crash (heap's arms failed too). I tried three versions of each; all crashed on the assert build.
  • The row-group-list mutant still reddens the failing UPDATE's error arm in isolation. It's that shape's own mechanism: the first error is the expected division by zero, and the next 27 follow because the savepoint's rows came back on a fresh table.

4. Green and bookkeeping:

  • 30/30 in the shell on each of PG15 to PG19, and the twin 30/30 on each before the rebase and on PG17 after it.
  • The census is 2984 + 30 = 3014 rows, 2278 + 16 = 2294 never, 706 + 14 = 720 dated, matching the tsv.
  • The gate gives rc=0, cluster_tests is 571 by collection, and the guard set passes 435/435. TESTS.md is now §116.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd force-pushed the test/1341-savepoint-flush branch from 2a60f80 to 73b7054 Compare October 15, 2026 18:06
jdatcmd added a commit that referenced this pull request Oct 15, 2026
…Es prove they flushed (#1341)

Review of #1430 by OffgridwithJD. Three changes:

- Every shape starts from fresh tables. Before, a shape that went wrong under
  a mutant poisoned the shapes after it, so six rows were dated only by a
  cascade and five more named a mutant that never reddened them alone.
- Both UPDATE arms get a flush premise: this transaction's inserts into
  row_group and delete_vector, read from pg_stat_xact_all_tables right after
  ROLLBACK TO. The failing UPDATE flushed no delete marks before it failed,
  so it now reads its own table at row 2500 (sp_probe), which flushes them.
  With that read disabled, the premise reads 4 row_group inserts and 0
  delete_vector inserts and goes red.
- The index reader counts id > 1000, so it reads rows in the 5000-row shapes.

The ledger rows are regenerated from isolated runs. The two "abort keeps its
buffer" mutants now trip #1403's flushedUnder assertion and crash the
server, so they date nothing; the abort promoting its buffers instead of
discarding them (OffgridwithJD's mutant) takes their place. 30/30 in both
twins on PG15-19. 14 checks dated, 16 never; census 2816 / 2111 / 705.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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.

@jdatcmd Reviewed 73b7054. The shell now does exactly what you describe; one blocker in the twin.

  • The three earlier blockers are fixed in the shell. I ran each of the nine shapes alone, against each mutant, on PG17 and PG18. The names that go red are exactly the ledger's mutation column, and the same as in the full run: 14 for the row-group list under SnapshotAny (.so b1729c50 on PG17), 4 for every delete_vector read under SnapshotAny (24fe7b4e), and 4 for promote-instead-of-discard (13b9be60). Restored, the md5 is back to 887d441c on PG17 and 77343e73 on PG18, and both twins stop first on the same check with the same values.

  • The failing UPDATE's error arm under the row-group-list mutant is its own shape's red. Alone, on a fresh table, it raises division by zero, then duplicate key value violates unique constraint "sp_pgcolumnar_pkey" from the later UPDATE ... WHERE id = 1, then 26 current transaction is aborted.

  • Green: 30/30 for the suite and the twin, on PG17 and PG18. The census, 3014 / 2294 / 720, matches the budget, and cluster_tests 571 and guard_tests 435 match collection.

  • Blocker: the twin's flush premises can't fail. The twin uses one connection for every shape, and pg_stat_xact_all_tables on it still holds counts from earlier work. The shell runs each transaction in a new backend, so it doesn't have this problem.

    • Read right after BEGIN, before the failing UPDATE runs, the twin already shows 60 row_group and 5 delete_vector inserts.
    • With sp_probe's read moved to id = 99999, the shell goes red with got [no (row_group inserts [4], delete_vector inserts [0])]. The twin stays 30/30 on PG17 and PG18, reading 64 and 5.
    • The rolled-back UPDATE's premise has the same problem. With WHERE false, the twin reads row_group inserts [33], delete_vector inserts [0], and goes red only because no earlier shape on that connection deleted anything.
    • Fix: read CATALOG_INS right after BEGIN too, and assert the difference. That gives 5/3 at head and 4/0 with the probe moved, the same as the shell. Doing it in both twins keeps them alike.
  • The dropped mutants, on a build without asserts. My PG17 isn't built with --enable-cassert, so the flushedUnder Assert doesn't fire and both mutants run without crashing. Heap stays green, and alone:

    • not calling PgColumnarWriteStateDiscardSubXact (md5 efcfbb66) turns 6 red, the failing INSERT's and the failing UPDATE's three arms, with second error duplicate key value violates unique constraint "row_group_pkey";
    • not calling PgColumnarDeleteVectorDiscardSubXact (1146b576) turns 3 red, the failing UPDATE's arms, with columnar: could not serialize access due to concurrent update or delete of the same row.

    Leaving them out is fine. But the budget's "crash the server" is only true on an assert build, so please say so, and they could go in the mutation column.

  • Small point: a 500-row rolled-back UPDATE still gives 1/1, so > 0 doesn't need more than one flush; -ge 2, like grew, would match the INSERT premises.

  • Rebase: main is now f2bd5f1; the budget, TESTS.md and expected_tests.txt conflict.

🤖 Generated with Claude Code

@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, addressed in a078f44, on main 2d1a77f.

  • The flush premises measure a difference, in both twins. Each UPDATE transaction reads the row_group and delete_vector insert counts right after BEGIN (rg_b, dv_b) and again after ROLLBACK TO (rg_a, dv_a). The premise requires two or more of each between them, as grew does for the INSERTs. The shell is the same, so the twins stay alike.
  • Your case, both twins (PG17): with sp_probe's read moved to id = 99999, both now fail the failing UPDATE flushed new versions and delete marks before it failed: got [no (row_group inserts [4], delete_vector inserts [0])]. Before this change the twin stayed green at 64 and 5.
  • The budget now says the crash is the assert build's. The two "abort keeps its buffer" mutants crash the server only on an assert-enabled build. On a build without asserts they run and turn 6 and 3 arms red, as you measured. I've recorded that in the paragraph rather than the mutation column, since I couldn't reproduce it here: every build in this container has asserts on.
  • Green: 30/30 in both twins on each of PG15 to PG19.
  • Bookkeeping, on 2d1a77f: the census is 3111 + 30 = 3141 rows, 2344 + 16 = 2360 never, 767 + 14 = 781 dated, matching the tsv. The gate gives rc=0, TESTS.md is §125, cluster_tests is 588 by collection, and the guard set passes 435/435.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd force-pushed the test/1341-savepoint-flush branch from 73b7054 to a078f44 Compare October 15, 2026 21:02
jdatcmd added a commit that referenced this pull request Oct 15, 2026
…Es prove they flushed (#1341)

Review of #1430 by OffgridwithJD. Three changes:

- Every shape starts from fresh tables. Before, a shape that went wrong under
  a mutant poisoned the shapes after it, so six rows were dated only by a
  cascade and five more named a mutant that never reddened them alone.
- Both UPDATE arms get a flush premise: this transaction's inserts into
  row_group and delete_vector, read from pg_stat_xact_all_tables right after
  ROLLBACK TO. The failing UPDATE flushed no delete marks before it failed,
  so it now reads its own table at row 2500 (sp_probe), which flushes them.
  With that read disabled, the premise reads 4 row_group inserts and 0
  delete_vector inserts and goes red.
- The index reader counts id > 1000, so it reads rows in the 5000-row shapes.

The ledger rows are regenerated from isolated runs. The two "abort keeps its
buffer" mutants now trip #1403's flushedUnder assertion and crash the
server, so they date nothing; the abort promoting its buffers instead of
discarding them (OffgridwithJD's mutant) takes their place. 30/30 in both
twins on PG15-19. 14 checks dated, 16 never; census 2816 / 2111 / 705.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jdatcmd added a commit that referenced this pull request Oct 15, 2026
…EGIN, so the twin's can fail (#1341)

Review of #1430 by OffgridwithJD: the twin uses one connection for every shape, so
pg_stat_xact_all_tables on it already counted earlier work (60 row_group and 5
delete_vector inserts before the failing UPDATE ran), and its premises could not
fail. Both twins now read the counts after BEGIN and after ROLLBACK TO and require
two or more of each between them. With sp_probe's read moved off the table, both
twins now go red with row_group inserts [4], delete_vector inserts [0].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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.

@jdatcmd Approved at a078f44.

The flush premises can fail now, in both twins. I checked this on PG17, on a build without asserts:

run savepoint_flush.sh test_savepoint_flush.py the arm that went red
head a078f44 30/30 30/30 none
sp_probe reads at id = 99999 (your mutant) 29/30 29/30 the failing UPDATE flushed …: got no (row_group inserts [4], delete_vector inserts [0])
the rolled-back UPDATE's shape gets 20 rows instead of 5000 (mine) 29/30 29/30 the savepoint's UPDATE flushed … before ROLLBACK TO: got no (row_group inserts [1], delete_vector inserts [1])

The second mutant matters. It shows the >= 2 floor separates "flushed" from "nothing flushed", because a 20-row UPDATE still writes one row to each catalog. A > 0 test of the difference would have stayed green there.

The difference is also sound. pg_stat_xact_all_tables includes this backend's pending counts from earlier transactions that it hasn't reported yet. That's where the 60/5 came from. A backend doesn't report stats in the middle of a transaction, so nothing else can move the counts between b and a.

Recording the assert-build crash in the budget paragraph, not the mutation column, is fine.

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

@jdatcmd A correction to my approval's table. The pytest column for the two mutants should read 27 pass + 1 fail of 28 (your mutant) and 16 pass + 1 fail of 17 (mine), not 29/30. After the failed premise, the twin's accounting stops early. The shell column and the red arms are as stated, and the approval at a078f44 stands.

🤖 Generated with Claude Code

jdatcmd and others added 3 commits October 15, 2026 21:18
…0) and leaves nothing declared, in both twins (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts connections (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, restated as 7b6c530 on top of #1372 (b3f4d6c), the tip of your chain, so it merges right after it. The patch is identical to a078f44, which you approved, outside the budget and expected_tests.txt. The census is 3380 rows, 2544 never, 836 dated, matching the tsv; cluster_tests is 600 by collection, and the gate gives rc=0.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd force-pushed the test/1341-savepoint-flush branch from a078f44 to 7b6c530 Compare October 15, 2026 21:47

@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.

@jdatcmd Approved at 7b6c530.

  • The patch is the one I approved. Its single commit on #1372 (b3f4d6c) leaves the code identical to a078f44's diff from 2d1a77f, and adds the same 30 tsv rows.
  • The bookkeeping restates the chain tip's. 3350 + 30 = 3380 rows, 2528 + 16 = 2544 never, 822 + 14 = 836 dated, matching the tsv. Collection gives cluster_tests 600 (599 + 1) and guard_tests 438, and the gate against main gives rc=0.
  • The runs from my a078f44 approval still apply: the head green on PG17 in both twins, and both mutants red.

It merges after #1372.

🤖 Generated with Claude Code

jdatcmd and others added 10 commits October 15, 2026 22:00
…emise: prefix (#1341)

Review of #1375 by jdatcmd: CI's suites (PG 17) ledger gate found the row seeded as
'premise: the drop really removed the projection' while projections.sh:410 and
test_projections.py:795 name the check without the prefix, so the gate saw one
check not in the ledger and one orphan row. The row is renamed and keeps its
other fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e writing any statistics, in both twins (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blings do (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dated (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ys so (#1341)

test_a_named_column_that_does_not_exist_is_refused calls pgcolumnar.analyze(),
which needs PostgreSQL 18, so it declines on 15, 16 and 17 like the other
analyze_function twins. It was missing from expected_unrunnable.txt, so CI's
PG17 cluster job collected and passed 578 and still exited 67 on the
unexpected decline. Measured on PG17: test_analyze_function.py exits 67
without these rows and 0 with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tored one (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dated (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its bar, and four that had none get arms here, in both twins (#1386)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…eir mutations (#1386)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…te, expire or a refresh lose the premise prefix, in both twins (#1339)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
jdatcmd and others added 6 commits October 15, 2026 22:01
…ite and expire mutations (#1339)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…ss a refresh, in both twins (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lumns' collations differ (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h twins and the skip list (#1372 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…e more than it folded (#1372 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…1341)

A row group flushed inside a savepoint is a catalog row written under the
subtransaction, so the abort must hide it, and the savepoint's buffered rows
and delete marks must be thrown away. No test rolled back a savepoint after a
flush and then read the table, so item 40 of #1341 had no test.

savepoint_flush and its pytest twin compare a columnar table with heap, with
row groups of 1000 rows: a flushed INSERT rolled back to a savepoint, at READ
COMMITTED and at REPEATABLE READ; the same unique keys inserted again after
ROLLBACK TO; an UPDATE of every row rolled back to a savepoint; and an INSERT
and an UPDATE that fail after flushing, alone and inside a savepoint. Four
readers check each result, inside the transaction and after COMMIT: a plain
sequential scan, the custom scan, the vectorized count and the primary key's
index. Premises pin each reader's plan and show each write flushed.

28 checks, 28/28 in both twins on PG15-19. Four mutants of the abort path
date 20 of them on PG17, and both twins fail first on the same check with
the same values under each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Squashed for the rebase; it replaces:
- test: savepoint_flush's bookkeeping restated on main 2d1a77f, and the budget says the dropped mutants crash only on an assert build (#1341)
- test: savepoint_flush's UPDATE premises measure the difference from BEGIN, so the twin's can fail (#1341)
- test: savepoint_flush runs each shape on fresh tables, and both UPDATEs prove they flushed (#1341)
- test: a savepoint rolled back, or a statement failed, after a flush (#1341)
@jdatcmd
jdatcmd force-pushed the test/1341-savepoint-flush branch from 7b6c530 to 19b880c Compare October 15, 2026 22:11

@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.

@jdatcmd Approved at 19b880c.

  • The stack: its parent is #1372's new head, f07760f.
  • Unchanged since my last approval: the patch is identical to 7b6c530's, and so are the budget, expected_tests.txt and TESTS.md diffs. That's expected, because #1375's fix renamed one ledger row and moved no count.
  • Runs: the PG17 runs from my earlier approval still cover this code.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd merged commit 6f0bd12 into main Oct 15, 2026
15 checks passed
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.

2 participants