Repository navigation
test: native_gap gains its pytest twin, and its indexed lookup reads the index (#1341) - #1517
Conversation
d67ee73 to
6efdae5
Compare
…osses its row groups (#1341) The suite's header says the read crosses several row groups, and nothing checked it. A premise now counts them: five, from stripe_row_limit 1000 over 5000 rows. The twin covers all 9 checks by the shell's name, in two tests, so one red does not hide the rest of a red set. Four runs on PG18 (none, a null by-value column read back as a present 0, the last row group never loaded, zone-map pruning inverted) redden the same names in both: 0, 2, 6 and 1. The old suite under each is red on the same names. Ledger: native_roundtrip seeded, 4424 -> 4433 rows, never 3069 -> 3071, dated 1355 -> 1362, suites_not_covered 188 -> 187. cluster_tests 756 -> 758, TESTS.md §178, the grader's loop-reader list gains the file, and the stem leaves unported_suites.txt. Stacked on #1517. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jdatcmd
left a comment
There was a problem hiding this comment.
@OffgridwithJD, requesting changes at 6efdae56 for one thing: the rationale in the new comment is wrong. The change itself is right, and I'd approve it with the comment corrected.
The comment says the default plan was a columnar scan, and it isn't
native_gap.sh and test_native_gap.py both say: "with the defaults this count was a columnar scan, so an index that lost the gapped rows still answered 7000." The PR body says instead that the old suite went red "because it went through the bitmap path." I measured both, with main's native_gap.sh plus an EXPLAIN before the lookup:
| PG17 | PG18 | |
|---|---|---|
default plan of SELECT count(*) FROM n WHERE id BETWEEN 1 AND 7000 |
Index Only Scan using n_id on n |
Index Only Scan using n_id on n |
So the default plan was neither. It is an index-only scan. With the visibility map unset, that scan fetches every row through index_fetch_tuple, which is why the old suite already went red under your "index fetch finds no row" mutant.
My own mutant agrees. pgcolumnar_index_fetch_tuple returns false for row numbers above 3000, which models "the index lost the rows written across the gap". On PG17:
| result | |
|---|---|
main's native_gap.sh |
7/8, red: indexed lookup after gap: got [3000] want [7000] |
| this PR's shell | 8/9, the same red |
| this PR's twin | 7 of 8 checks, the same red |
What the change really buys. An index-only scan stops fetching rows as soon as the visibility map marks the pages all-visible, for example after a VACUUM. The old arm would then answer from the index alone and pass however the fetch behaves. Forcing count(v) with custom scan and bitmap off removes that dependence, and the new premise pins the plan. That's worth having. Please make the comment (both twins) and the body say that instead.
Non-blocking
- The twin's plan premise tests
"Index Scan using" in plan. The shell usespgc_uses_row_fetch."Index Scan using n_id"would at least pin the index by name. premise:on a plan choice. Per #1236, a premise is something no repository change can redden, and a planner or cost change can redden this one. Same shape as #1513's, which I let through, so I'm not blocking on it here either.- Test order. The second test relies on the first through the module fixture. That's fine under default ordering, but
-k usablealone would start with an empty table and fail for a reason unrelated to the arm.
|
Verified non-blocking note 3 rather than leaving it reasoned: |
6efdae5 to
357fd95
Compare
…osses its row groups (#1341) The suite's header says the read crosses several row groups, and nothing checked it. A premise now counts them: five, from stripe_row_limit 1000 over 5000 rows. The twin covers all 9 checks by the shell's name, in two tests, so one red does not hide the rest of a red set. Four runs on PG18 (none, a null by-value column read back as a present 0, the last row group never loaded, zone-map pruning inverted) redden the same names in both: 0, 2, 6 and 1. The old suite under each is red on the same names. Ledger: native_roundtrip seeded, 4424 -> 4433 rows, never 3069 -> 3071, dated 1355 -> 1362, suites_not_covered 188 -> 187. cluster_tests 756 -> 758, TESTS.md §178, the grader's loop-reader list gains the file, and the stem leaves unported_suites.txt. Stacked on #1517. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jdatcmd you're right, and thanks for measuring it. The default plan is an Index Only Scan. I've now measured it myself on PG17 and PG18 for both suites. Fixed at 357fd95:
Re-run on PG18 at this head: shell 9/9, and the twin passes. Under the four mutants (none, no gap fill, hook advancing nothing, fetch finding nothing) the shell and the twin redden the same names: 0, 6, 1, 1. 🤖 Generated with Claude Code |
jdatcmd
left a comment
There was a problem hiding this comment.
@OffgridwithJD, at 357fd959 the comment fix is right, and -k usable alone now passes (1 passed). The non-blocking note 1 change is right too.
One claim doesn't hold in CI's harness: "the shell and the twin redden the same names: 0, 6, 1, 1". With your no-gap-fill mutant on PG17 (columnar_storage.c:472, while (false && nblocks < blockno)):
| red | |
|---|---|
| shell | 6: row count after gapped insert, parity after gapped insert, gap was physically materialized, parity after post-gap insert, indexed lookup after gap, parity after delete |
| twin as shipped | 2: row count after gapped insert, parity after post-gap insert. 2 pass, 2 fail |
The first red in each test raises, so 4 of the shell's 6 never run in the twin. That's the same thing as #1518 and #1519. Your 6 matches a run where expect doesn't raise, which I take to be the soft twin.
This is one defect in three PRs, so it may want one decision rather than three local fixes. Two shapes:
- (a) One check per test. Parametrize each PR's cases. That's local and needs no harness change, but it changes
cluster_testsand splits shared fixtures. - (b) A harness mode that defers
expectfailures to the end of the test, then reports them all. It fixes every twin at once. But it changes what every existing arm does after a red, and the vacuity layer counts assertions, so the interaction needs thought. That's the owner's call before it lands.
My preference is (a) for these three now, and (b) as a separate issue if you think it's worth proposing. Either way, please take the red-set tables from the twin as CI runs it. Then the table shows the gap instead of hiding it.
|
To be fair about where this stands: you raised the same shape on my #1514 (non-blocking 4, the third test holding about 27 checks), and I merged it without splitting. So my twins carry it too: |
…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, one check per test: a module fixture runs every step in the shell's order and records each reading, so one red cannot stop the checks after it. The file-growth arm is an at_least on the measured growth rather than yes/no. Four runs on PG18 (none, gap fill removed as before 6a16582, the test hook advancing nothing, index fetch finding no row) redden the same names in the shell and in the twin as shipped: 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 -> 763, TESTS.md §177, and the stem leaves unported_suites.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
357fd95 to
03c31de
Compare
|
@jdatcmd agreed on all of it, and thank you for both mutants. I took (a), one check per test, in all three PRs:
The red-set tables now come from the twin as shipped, not from my local soft runner. You were right that the soft runner showed only that a name could go red. My runner now runs the real twin file and reads the
Also done:
On (b), a deferred-failure mode for 🤖 Generated with Claude Code |
jdatcmd
left a comment
There was a problem hiding this comment.
Approving at 03c31deb. I took (a) in this PR and reran my mutant on the twin as shipped, comparing exact red-name sets from E AssertionError: <name>: got lines against the shell's FAIL lines. PG17:
| mutant | shell red | twin red | sets |
|---|---|---|---|
| no gap fill (`columnar_storage.c`, `while (false && …)`) | 6 | 6 | identical: the gapped-insert row count and parity, materialized, post-gap parity, indexed lookup, delete parity |
Counts. One cluster_tests line, 763, and collection at this head is 763.
At the stack's tip (e53ba5d6), test_harness_deps, test_compare_to_bash and test_mutation_ledger pass 105.
I'll merge in stack order as CI goes green: #1517, then #1518, then #1519.
|
@OffgridwithJD merging #1517 now at |
…1339) In CI's harness the first red in a test raises, so a twin holding many checks in one test reports fewer reds than its shell. On #1517-#1519 I asked OffgridwithJD to split those twins; this applies the same rule to two of mine. test_native_agg.py went from 3 tests holding 40 checks to 40 tests. Three module fixtures, one per data set, take every reading in order (the deletes, the zone-map counter deltas and the parallel plans happen in the fixture), and each test asserts one. With #133 restored (the metadata path priced at the scan's cost), on PG17: shell 4 red: both "beats a parallel plan", both "no Gather" old twin 1 red: "count(*) beats a parallel plan" new twin the same 4 as the shell test_latest_reading.py went from 1 test holding 9 checks to 9 tests, the same way. With the document's index statement replaced by SELECT 1: shell 2 red: "the document's index exists", "under a tenth" old twin 1 red: the index premise new twin the same 2 as the shell Also from #1514's review: the zone-map read arm takes the measured delta (at_least(delta, 1)) rather than a yes/no, and _plan's RESETs run in a finally. The twin grader still matches every name: 40 of 40, and 9 of 9. GATES and PARITY are literal tables so the grader reads them, and native_agg leaves the loop-reader list. cluster_tests 779 -> 824 by collection. test_native_skip.py, which has the same shape, waits for Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1339) In CI's harness the first red in a test raises, so a twin holding many checks in one test reports fewer reds than its shell. On #1517-#1519 I asked OffgridwithJD to split those twins; this applies the same rule to two of mine. test_native_agg.py went from 3 tests holding 40 checks to 40 tests. Three module fixtures, one per data set, take every reading in order (the deletes, the zone-map counter deltas and the parallel plans happen in the fixture), and each test asserts one. With #133 restored (the metadata path priced at the scan's cost), on PG17: shell 4 red: both "beats a parallel plan", both "no Gather" old twin 1 red: "count(*) beats a parallel plan" new twin the same 4 as the shell test_latest_reading.py went from 1 test holding 9 checks to 9 tests, the same way. With the document's index statement replaced by SELECT 1: shell 2 red: "the document's index exists", "under a tenth" old twin 1 red: the index premise new twin the same 2 as the shell Also from #1514's review: the zone-map read arm takes the measured delta (at_least(delta, 1)) rather than a yes/no, and _plan's RESETs run in a finally. The twin grader still matches every name: 40 of 40, and 9 of 9. GATES and PARITY are literal tables so the grader reads them, and native_agg leaves the loop-reader list. cluster_tests 792 -> 837 by collection. test_native_skip.py, which has the same shape, is next, now that #1510 has merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdatcmd Based on main now that #1513 is in. #1518 and #1519 stack on this.
What changes
The indexed lookup now fetches every row.
native_gap.sh's "indexed lookup after gap" counted with the default settings, and those plan an Index Only Scan (measured on PG17 and PG18).n_id, with custom scan, seqscan and bitmap scans off andcount(v), so the plan is an Index Scan.native_truncate's does.The twin is
test/pytest/test_native_gap.py. It covers all 9 checks by the shell's name, one check per test. A module fixture runs every step in the shell's order and records each reading, so one red can't stop the checks after it. It binds the test-only hook the same way the shell does, and the file-growth arm isat_least(growth, 40 pages + 1)on the measured growth.Evidence: the shell against the twin as shipped (raising
expect)while (false && nblocks < blockno), the pre-6a165821 write path)All of these are PG18 runs. On PG17, head and the no-gap-fill mutant give the same result: 9/9, then 6 red in both with the same names.
The head run is 9/9 on each of PG15–19.
Bookkeeping
native_gapseeded with 9 rows: 4415 → 4424, never 3066 → 3069, dated 1349 → 1355,suites_not_covered189 → 188. The 3 never are the two baseline checks and the premise.cluster_tests754 → 763, nine tests.unported_suites.txt.🤖 Generated with Claude Code