Skip to content

test: native_truncate gains its pytest twin, and its GUC-off arm stops passing on a refusal (#1341) - #1513

Merged
jdatcmd merged 1 commit into
mainfrom
test/1341-native-truncate-twin
Oct 17, 2026
Merged

jdatcmd merged 1 commit into
mainfrom
test/1341-native-truncate-twin

Conversation

@OffgridwithJD

@OffgridwithJD OffgridwithJD commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

@jdatcmd This adds the twin for native_truncate, and fixes a shell arm that was passing on an error.

The GUC-off arm passed on a refusal

psql_run "SET pgcolumnar.enable_end_truncation = off; SELECT pgcolumnar.truncate('n');" sends both statements as one -c string. That runs as an implicit transaction block, and IsTransactionBlock() is true there, so truncate refuses. At head the suite log shows the call failing with pgcolumnar.truncate() cannot run inside a transaction block. The arm then compares the size of a file nothing touched, and passes.

mutant (PG18) suite as on main this PR
pgcolumnar_enable_end_truncation && removed (the GUC not consulted) 17/17 green red: GUC off: truncate does nothing, GUC on: truncate reclaims

The call is now its own -c in the same session. The arm checks size|answer against size_before|0, so a refusal can't pass it again. The twin always sent the two statements separately, and it went red on the first run, which is how this surfaced.

The indexed lookup

It's forced through n_id with the custom scan off, uses count(v) so the plan fetches rows, and has a premise naming the index. The old lookup already read the index: it's red under "index fetch finds no row". So this is a guard against a planner change, not a fix.

The twin

It covers all 18 checks by the shell's name, in three tests. Under each mutant on PG18, the shell and the twin redden the same set:

mutant reds (shell = twin)
none 0
IsTransactionBlock refusal removed truncate refused inside a transaction block
GUC not consulted the two GUC arms above
truncate returns 0 without truncating truncate returned blocks, file shrank, GUC on: truncate reclaims, GUC on: file shrank
index fetch finds no row indexed lookup after truncate

file unchanged after refused in-txn truncate stays green even with the refusal removed, so it arrives never.

Ledger

native_truncate seeded with 18 rows: 4397 → 4415, never 3055 → 3066, dated 1342 → 1349 (restated over #1516), suites_not_covered 190 → 189. It's 18/18 on each of 15–19. The gate returns rc 0 and orphan-scan finds 0. cluster_tests goes 751 → 754 (over #1514), TESTS.md gets §176, and the stem leaves unported_suites.txt (#1509). The grader passes the pair, and the NO_CLUSTER guards pass 440/440.

🤖 Generated with Claude Code

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

Approve, at b3ec495.

My own mutant, in the dangerous direction: truncate cuts one block into live data (truncBlock -= 1 after it's computed from liveEnd, in columnar_vacuum.c). PG17:

  • shell, 10 of 18 red: every parity arm (QUERY_ERROR), row count after truncate: got [30000] want [15000], indexed lookup after truncate, truncate returned blocks, file shrank, …;
  • pytest: red by name first (truncate before any delete is a no-op: got 'error: None'), then the backend is gone (connection failed). The server seems to fall over reading the cut-off blocks.

A truncate that loses data can't pass this suite.

The GUC-off arm: your finding (two statements in one -c run as an implicit transaction block, so truncate refused and the arm compared an untouched file) is the same class as my pipefail one on #1483: a harness behaviour reading as a pass. Splitting the -c plus checking size|answer is right.

Merge order with #1511: whichever is green first. You've said you'll restate the other.

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

@jdatcmd rebased onto d3b237e (#1511 and #1512 in), now f4e30fba. Only the counts moved, restated over #1511's gaps:

  • ledger rows 4396 + 18 = 4414, never 3055 + 11 = 3066, dated 1341 + 7 = 1348 (checks_never_observed_red 3066); suites_not_covered 190 -> 189 unchanged.
  • cluster_tests 750 -> 753 (by collection on this tree).

Gate rc 0 against origin/main with the five head logs; guards 439 passed. No suite or twin change.

🤖 Generated with Claude Code

@OffgridwithJD
OffgridwithJD force-pushed the test/1341-native-truncate-twin branch from b3ec495 to f4e30fb Compare October 16, 2026 23:12

@jdatcmd jdatcmd 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-approving at f4e30fba, the restate of b3ec495e (which I approved) onto main d3b237ec.

  • Unchanged. Six of eight files have byte-identical patches to the approved head (per-file md5 of every ^[-+] line): check_ledger.tsv, native_truncate.sh, TESTS.md, test_compare_to_bash.py, test_native_truncate.py, unported_suites.txt.
  • Restated bookkeeping, checked against the files:
    • check_ledger.tsv counts 4414 rows: 3066 never and 1348 dated. That equals the budget's 4396 + 18, 3055 + 11 and 1341 + 7.
    • suites_not_covered goes from 190 to 189.
    • expected_tests.txt holds exactly one cluster_tests line (753), and collection on this tree is 753.
  • Harness files on this tree: test_harness_deps.py, test_mutation_ledger.py and test_compare_to_bash.py: 105 passed. The one failure, test_a_cluster_test_still_needs_the_driver, fails identically on a clean checkout of main in my container, so it isn't this PR.

I'll merge once its CI is green.

@jdatcmd

jdatcmd commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

@OffgridwithJD, two things on f4e30fba:

  1. It now conflicts with main. test: native_agg's pytest twin covers every shell arm (#1341) #1514, test: the loop-coverage sweep's premise reads what the sweep saw (#1339) #1515 and test: latest_reading holds the GROUP BY recipe to reading the table once (#1339) #1516 merged since your restate, so it needs another one. My re-approval at f4e30fba stands for the six unchanged behaviour files, and I'll re-check the bookkeeping after the restate.

  2. suites (PG 18) was red, in a suite this PR doesn't touch. The failing arm was native_backend_crash "crashB: the INSERT had written pages before the kill (the relation grew): got [f] want [t]" (job). It comes from your test: native_backend_crash gets its pytest twin, and a mid-write kill must follow a write (#1341 item 25) #1444, at native_backend_crash.sh:145.

    • What the log shows. crashB is the sleep phase with 100,000 rows. Its backend did reach PgSleep (that arm passed), yet pg_relation_size('c') was not above size0. size0 is read after crashA's crash and recovery.
    • What I measured.
      • The suite went 5/5 green on PG18 here, so I can't reproduce it on an idle machine.
      • None of the last 60 failed CI runs shows this arm.
      • My first hypothesis was that crashB reuses pages an abandoned write left. With a ROLLBACK standing in for the crash, it is refuted: the later write still grew the file (188416 → 253952).
    • Open question. After a real crash, does the writer's next-block position come back below the physical end crashA left? If it does, crashB's 8-odd pages would fit inside crashA's leftovers whenever crashA wrote enough before its kill. That depends on timing. I haven't verified it.

    If you agree it's a premise flake rather than a product failure, I'd count it as unrelated to this PR.

@OffgridwithJD
OffgridwithJD force-pushed the test/1341-native-truncate-twin branch from f4e30fb to 0e9d204 Compare October 16, 2026 23:31
OffgridwithJD pushed a commit that referenced this pull request Oct 16, 2026
…the index (#1341)

The shell's "indexed lookup after gap" counted with the defaults, which plan
a columnar scan for count(*) over the range, so it never had to read the
index built over the gapped layout. It is now forced through n_id with the
custom scan off and count(v), and a premise asserts the plan, as
native_truncate's lookup is. The old lookup still went red under an index
fetch that finds no row (bitmap path), so this is a guard, not a fix.

The twin covers all 9 checks by the shell's name, in two tests, and the
file-growth arm is an at_least on the measured growth rather than yes/no.
Four runs (none, gap fill removed as before 6a16582, the test hook
advancing nothing, index fetch finding no row) redden the same names in
both: 0, 6, 1 and 1.

Ledger: native_gap seeded, 4415 -> 4424 rows, never 3066 -> 3069, dated
1349 -> 1355, suites_not_covered 189 -> 188. cluster_tests 754 -> 756,
TESTS.md §177, and the stem leaves unported_suites.txt. Stacked on #1513.

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

Copy link
Copy Markdown
Collaborator Author

@jdatcmd restated the stack onto 4490755 (#1514–#1516 in). Only the counts moved:

PR head ledger rows never dated cluster_tests
#1513 0e9d204 4397 → 4415 3055 → 3066 1342 → 1349 751 → 754
#1517 6efdae5 4415 → 4424 3066 → 3069 1349 → 1355 754 → 756
#1518 5be44b2 4424 → 4433 3069 → 3071 1355 → 1362 756 → 758

At each tip:

  • the gate is rc 0 against the commit below it, using that suite's five head logs;
  • guards pass 440/440;
  • the collection count matches cluster_tests.

#1518's loop-reader list keeps #1514's native_agg.

🤖 Generated with Claude Code

@jdatcmd jdatcmd 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-approving at 0e9d2047 (restated onto 44907550).

  • Unchanged. Six files have byte-identical patches to f4e30fba (per-file md5 of every ^[-+] line).
  • Changed: only check_ledger_budget.txt and expected_tests.txt.
  • Checked on this tree:
    • the tsv counts 4415 rows, 3066 never and 1349 dated. That equals main's 4397, 3055 and 1342, plus 18, 11 and 7;
    • suites_not_covered is 189;
    • there is one cluster_tests line (754), and collection is 754;
    • test_harness_deps.py, test_compare_to_bash.py and test_mutation_ledger.py: 105 passed.

I'll merge it once CI is green. If native_backend_crash crashB reddens again, I'll treat it as the flake described above, rerun that job, and not merge on a red.

…s passing on a refusal (#1341)

The shell's GUC-off arm sent "SET pgcolumnar.enable_end_truncation = off;
SELECT pgcolumnar.truncate('n')" as one -c string. That is an implicit
transaction block, which truncate refuses, so at head the call failed with
"cannot run inside a transaction block" and the arm passed on an unchanged
file. With the GUC not consulted at all, the suite as on main passes 17/17.
The call is now its own statement in the same session, and the arm checks
its answer as well as the size. Under the same mutant it goes red, with the
GUC-on reclaim after it.

The indexed lookup is forced to fetch every row through n_id (custom scan,
seqscan and bitmap scans off, count(v)), and a premise asserts the plan.
By default it is an Index Only Scan, which still fetches each row while
the visibility map is unset (so the old lookup was red under an index
fetch that finds no row) but answers from the index alone once a VACUUM
marks the pages. A guard, not a fix.

The twin covers all 18 checks by the shell's name, in three tests. Five
mutants (none, txn refusal removed, GUC ignored, truncate returning 0,
index fetch finding no row) redden the same names in both.

Ledger: native_truncate seeded, 4397 -> 4415 rows, never 3055 -> 3066,
dated 1342 -> 1349, suites_not_covered 190 -> 189. cluster_tests 751 ->
754, TESTS.md §176, and the stem leaves unported_suites.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@OffgridwithJD
OffgridwithJD force-pushed the test/1341-native-truncate-twin branch from 0e9d204 to cde85e4 Compare October 16, 2026 23:41
OffgridwithJD pushed a commit that referenced this pull request Oct 16, 2026
…the index (#1341)

The shell's "indexed lookup after gap" counted with the defaults, which
plan an Index Only Scan. That fetches each row only while the visibility
map is unset (so the old suite was already red under an index fetch that
finds no row) and answers from the index alone once a VACUUM marks the
pages. It is now forced to fetch every row through n_id (custom scan,
seqscan and bitmap scans off, count(v)), and a premise asserts the plan,
as native_truncate's lookup is. A guard, not a fix.

The twin covers all 9 checks by the shell's name, in two tests over a
fixture that builds the gapped layout, so each stands alone, and the
file-growth arm is an at_least on the measured growth rather than
yes/no. Four runs (none, gap fill removed as before 6a16582, the test
hook advancing nothing, index fetch finding no row) redden the same
names in both: 0, 6, 1 and 1.

Ledger: native_gap seeded, 4415 -> 4424 rows, never 3066 -> 3069, dated
1349 -> 1355, suites_not_covered 189 -> 188. cluster_tests 754 -> 756,
TESTS.md §177, and the stem leaves unported_suites.txt. Stacked on #1513.

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

Copy link
Copy Markdown
Collaborator Author

@jdatcmd heads-up: I changed this PR after your approval, now at cde85e4. The lookup comment (shell, twin, commit message) said the default plan was a columnar scan. It is an Index Only Scan: Index Only Scan using n_id on n on PG17 and PG18, measured on main's suite. You caught the same claim on #1517. It is corrected, and the twin's premise now pins Index Scan using n_id. Shell 18/18, twin 18/18 on PG18, guards 440. No check name or count changed. Please re-approve if it reads right.

🤖 Generated with Claude Code

@jdatcmd jdatcmd 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-approving at cde85e4e.

  • The diff from 0e9d2047 touches two files: the lookup comment in native_truncate.sh and test_native_truncate.py, and the twin's premise string, now "Index Scan using n_id". No check name or count moves.
  • The new comment, measured. With an EXPLAIN before the lookup in this PR's shell, the default count(*) plans Index Only Scan using n_id on n on both PG17 and PG18. The shell is 18/18 on both.

@jdatcmd

jdatcmd commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

@OffgridwithJD merging #1513 now at cde85e4e: approved on this head, 15/15 checks, CLEAN.

@jdatcmd
jdatcmd merged commit bbda341 into main Oct 17, 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