Skip to content

test: native_decode_gating gets its pytest twin, and its filter counters can see a double count and a deleted row (#1341) - #1456

Merged
jdatcmd merged 11 commits into
mainfrom
test/1341-native-decode-gating-twin
Oct 16, 2026
Merged

jdatcmd merged 11 commits into
mainfrom
test/1341-native-decode-gating-twin

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

#1341, the twin-less suites: native_decode_gating.sh (#452 phase 2) had no pytest twin. This PR adds one, every check by the shell's name, and seeds the suite into the ledger. The port found that two of the filter counters' guarantees couldn't fail, and that the multi-group fixture depended on the gating it was built to test. Both are fixed in both twins.

Stacked on #1455, at the top of my chain. This branch is #1455's head plus two commits.

What changes

  • test/pytest/test_native_decode_gating.py, the twin: two tests in a private schema. The width gate (Decode gating (#452 phase 2) is unconditional; gate it on selectivity so it stops regressing less-selective scans #595) is set off for the session, where the shell pins it off for its cluster; it's PGC_USERSET. compare_to_bash.py grades it as covering all 29 names, and it joins COMPLETE.
  • Rejections inside a surviving vector weren't tested. Every row of the needle's surviving vector matched, so the producer's re-test of that vector rejected nothing. A producer that counted those rejections a second time therefore read exactly right.
    • Measured: with the non-counting filter disabled, the suite stayed 26/26.
    • The fix: a qual that half of vector 0 passes (tag LIKE '%needle%' AND p1 % 2 = 0, still no skip predicate) asserts its rejections are counted once, with a premise for its 512 survivors in one vector.
  • The counters were checked only on the table without deletes.
    • Measured: with the evaluator's deleted-row test removed, the suite stayed 26/26.
    • The fix: Rows Removed by Filter is now checked on the multi-group table with deletes, against heap's live count.
  • The fixture depended on the gating under test. Its DELETE … WHERE p1 % 17 = 0 is an unprunable qual too, so a gating mutant broke the fixture, and with it the premise "rows really were deleted", rather than the arms. pgc_ledger.py gate then refused, rightly, a premise row with a red observation.
    • The fix: the DELETE now runs with the width gate out of reach (100000, the setting's maximum), and the premise counts the survivors with count(*).
  • Ledger: native_decode_gating is seeded with 29 rows from five-major runs, and 11 of the 29 rows dated. suites_not_covered goes 215 → 214, and the census is 3691 + 29 = 3720 rows, 2705 + 18 = 2723 never and 986 + 11 = 997 dated, matching the tsv. The gate gives rc=0 against origin/main, and no premise is dated.

Red first

Each mutation of src/columnar_reader.c was run one at a time on PG18, through both twins, with the .so md5 printed every time.

mutation native_decode_gating.sh test_native_decode_gating.py the arms that went red
none (the head) 29/29 29/29 none
phase 2's per-vector evaluation never runs 25/29 1 of 2 tests fails the decode counts (got [192] want [32]/[37]) and the skipped-vector counts
the evaluator counts a deleted row as filtered 28/29 (26/26 before this PR) 1 fails rows removed by filter across deleted rows count only live rows: got [49140] want [46250]
every vector marked skipped 22/29 2 fail the control's decode count, the needle's counts, and every correctness arm (got [EMPTY])
the producer counts a gated group's rejections a second time 27/29 (26/26 before this PR) 2 fail rejections inside a surviving vector are counted once: got [32768] want [32256], and the deleted-rows counter

Never red (18):

  • the thirteen premises;
  • the observable's presence;
  • the full scan across every group;
  • the nomatch query's empty answer;
  • two exact counters that no single mutation here moves.

Runs

major native_decode_gating.sh test_native_decode_gating.py
15 29/29 29/29
16 29/29 29/29
17 29/29 29/29
18 29/29 29/29
19 29/29 29/29

Also measured: the guard half (438 tests), harness_selftest 1245/1245 and docs_style. cluster_tests goes 674 → 676 by collection, and TESTS.md gets §148.

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

Approving 51b5aa3.

Measured on PG17: native_decode_gating.sh 29/29; test_native_decode_gating.py 29 checks over 2 tests.

The deleted-row mutant, run myself: if (deleted) continue; removed from the phase-2 evaluator in columnar_reader.c, so a deleted row is counted and asked like a live one. Both twins go red on the new arm, and only on it: rows removed by filter across deleted rows count only live rows, got [49140] want [46250]. Your table says the same, and before this PR the counters were checked only on the table without deletes.

I took the other three mutants from your table. This goes on top of the stack I approved (#1450 → #1455); the stack's restate onto 0edb5ae covers it too.

jdatcmd and others added 11 commits October 16, 2026 05:01
…es ALTER EXTENSION UPDATE (#1341 item 66)

Between installing a new library and running ALTER EXTENSION pgcolumnar UPDATE, a
database runs the 1.0-alpha5 library over its 1.0-alpha4 catalog. The library deletes
from pgcolumnar.retired_group, which that catalog lacks, so DROP TABLE, TRUNCATE and
pgcolumnar.vacuum() on a columnar table fail there with 42P01 'columnar metadata
table "pgcolumnar.retired_group" does not exist', which names neither the cause nor
the remedy. Measured: each refusal changes nothing, reads and writes work throughout,
and after the update all three succeed on the same rows.

open_columnar_table's error now carries a hint naming the update.
docs/installation.md's "If you skipped step 3" shows the error and the hint.

Red first, both twins, PG18: native_upgrade_converge gains the window's arms in a
database at the previous release's catalog (the newest fixture), and the three
refusal arms fail on main with `got [42P01|no-hint]`. test_native_upgrade_converge.py
is their partial twin, declared INCOMPLETE; it stages the previous release's base
script from test/fixtures as the shell does. Green on 15-19 in both twins.

cluster_tests 660 -> 661 by collection; TESTS.md gets section 143.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… still be set to a row store (#1341)

test_fk_referencing.py asserts every shell check by name; compare_to_bash grades it as
covering all 28 (27 checks on PG17+, and the pre-17 branch's own), and it joins
COMPLETE.

The conversion guard's over-rejection control converted a table with no foreign key
back to heap, so it could not see a guard that refused every SET ACCESS METHOD on a
referenced table whatever the target: with the guard's columnar-target test removed,
the suite stayed 26/26. A new arm sets the referenced table itself to heap; under that
mutant both twins now fail it.

cluster_tests 661 -> 666 by collection; TESTS.md gets section 144.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tations of the foreign-key guards (#1341)

28 rows from five-major runs (27/27 on 17-19, 26/26 on 15-16). Dated on PG18 by:
the referenced-side refusal removed (5), the guard keyed on the referencing table
(8), the conversion refusal removed (8), and the conversion refused whatever the
target (1). suites_not_covered 219 -> 218; the census is 3647 rows, 2686 never,
961 dated, restated on main db56168.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_truncate_cleanup.py asserts every shell check by name; compare_to_bash grades it
as covering all 25, and it joins COMPLETE. It runs in a private database, since the
snapshot counts every pgcolumnar catalog row and the shared one holds every other
test's. The shell suite needed no change: each of its arms already goes red under a
mutation of the path it names.

cluster_tests 666 -> 672 by collection; TESTS.md gets section 145.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mutations of the truncate paths (#1341)

25 rows from five-major runs (25/25 on each of 15-19). Dated on PG18 by a rewrite
that keeps the retired storage's rows (7) or its stale write state (1), an in-place
truncate that skips the projections' storages (8), and a storage-tree delete that
skips them (1). suites_not_covered 218 -> 217; the census is 3672 rows, 2695 never,
977 dated, restated on main db56168.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…zone-map answer that ignores deletes (#1341)

test_native_dml.py asserts every shell check by name; compare_to_bash grades it as
covering all 15, and it joins COMPLETE.

The min/max arm could not fail as its comment claimed. The deletes (id % 7 = 0)
removed neither the smallest nor the largest id, so min and max were the same
whether or not the deleted rows counted; and measured with groups that hold deletes
left on the zone-map path, min/max(id) still came out right while min/max(k) came
out wrong (10|50000 for 20|49990). The suite stayed 13/13. The deletes now include
ids 1 and 5000, a premise asserts on heap that min and max moved, and the arm takes
min/max over k as well as id; under that mutant both twins fail it.

The index arms gain a premise that the lookups take the primary-key index, so they
test the index fetch's delete check rather than the scan.

cluster_tests 672 -> 673 by collection; TESTS.md gets section 146.

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

15 rows from five-major runs (15/15 on each of 15-19). Dated on PG18 by: a row group
with deletes still answering min/max from zone maps (1), count(*) from metadata not
subtracting deleted rows (3), and the index fetch ignoring the delete vector (5).
suites_not_covered 217 -> 216; the census is 3687 rows, 2702 never, 985 dated, restated on main db56168.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…longer pass its survival arm (#1341)

test_native_dict_underfill.py asserts every shell check by name; compare_to_bash
grades it as covering all 4, and it joins COMPLETE. It turns the post-codec encoding
choice off for its own session, where the shell sets it for its database.

The survival arm was a fresh SELECT 1, which a crashed backend passes once crash
recovery has finished, and the suite computed a crash flag it never checked. It now
counts "terminated by signal" in the server log. The rejection arm matched
'does not match|corrupt|invalid|out of' in any message, which "out of memory"
satisfies too; it now requires XX001. A premise asserts the inflation rewrote the
chunk's descriptor.

cluster_tests 673 -> 674 by collection; TESTS.md gets section 147.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 rows from five-major runs (4/4 on each of 15-19). Dated on PG18 by the DICT fill
check removed (1) and replaced by a crash (2, one shared). suites_not_covered
216 -> 215; the census is 3691 rows, 2704 never, 987 dated, restated on main db56168.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ers can see a double count and a deleted row (#1341)

test_native_decode_gating.py asserts every shell check by name; compare_to_bash
grades it as covering all 29, and it joins COMPLETE. It sets the width gate off for
its session, where the shell pins it off for its cluster.

Two of the filter counters' guarantees could not fail:
- every row of the needle's surviving vector matched, so the producer's re-test of
  that vector rejected nothing, and a producer counting its rejections a second time
  read exactly right: with the non-counting filter disabled the suite stayed 26/26.
  A qual that half of vector 0 passes now asserts its rejections are counted once.
- the counters were checked only on the table without deletes, so with the
  evaluator's deleted-row test removed the suite stayed 26/26. Rows Removed by Filter
  is now checked on the multi-group table with deletes, against heap's live count.

The multi-group fixture's DELETE ran through the gating it was built to test (its
WHERE is an unprunable qual too), so a gating mutant broke the fixture rather than
the arms. It now runs with the width gate out of reach, and its premise counts the
surviving rows with count(*) instead of a gated WHERE.

cluster_tests 674 -> 676 by collection; TESTS.md gets section 148.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…our mutations of phase 2 (#1341)

29 rows from five-major runs (29/29 on each of 15-19). Dated on PG18 by: the
per-vector evaluation never run (4), a deleted row counted as filtered (1), every
vector skipped (7), and a double count in the producer (2). No premise is dated: the
fixture no longer depends on the gating under test. suites_not_covered 215 -> 214;
the census is 3720 rows, 2722 never, 998 dated, restated on main db56168.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@OffgridwithJD
OffgridwithJD force-pushed the test/1341-native-decode-gating-twin branch from 51b5aa3 to fb1f6e2 Compare October 16, 2026 05:01

@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 fb1f6e2, the restate onto main db56168. I compared every commit's patch with the approved stack using git range-diff. The test and source commits are unchanged, and the ledger-seed commits changed only by re-anchoring on #1470. The census at this head matches its budget paragraph, and the counts step consistently. At #1467's head: the guards pass (438), 687 collected (matching the file), and harness_selftest is 1245/1245.

@jdatcmd
jdatcmd merged commit 6ec1759 into main Oct 16, 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