Skip to content

test: objstore_addressing gains its pytest twin, and the gs:// default and key encoding are checked (#1341) - #1716

Merged
jdatcmd merged 34 commits into
mainfrom
test/1341-objstore-addressing-twin
Oct 23, 2026
Merged

jdatcmd merged 34 commits into
mainfrom
test/1341-objstore-addressing-twin

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

@jdatcmd Ports test/objstore_addressing.sh to test/pytest/test_objstore_addressing.py (#1341): all 16 checks under the shell's names, one test per check, on a private cluster with its own SigV4 fixture server (--virtual-host-base s3.local). This waits for the merge freeze on #1341. It gets restated onto the top of my frozen stack.

Holes, each passing on main's shell (PG17), now failing in both harnesses

  • The gs:// default endpoint was never checked. The no-endpoint check only required that the message not name AWS_ENDPOINT_URL. That also passes for a mistyped default, any other error, or a crash with no message: with the default host typed as storage.google.com (gswrongdef), main passed 12/12. The cluster's allow-list holds only s3.local, so the read must now be refused by the allow-list naming storage.googleapis.com:443. The twin requires 42501 plus that phrase.
  • No key needed URI-encoding. With a space encoded as + (plusspace), or + and = left bare (plusraw), main passed 12/12. A key dir sp/a+b=c.parquet is now read under both addressings, and its request path in the fixture's log must be …/dir%20sp/a%2Bb%3Dc.parquet, encoded exactly once. The fixture decodes the path once to find the file, so plusraw still returns the right answer. Only the request-path checks catch it.
  • Checked by the answer alone. The fixture serves the same object under either addressing, so every addressing read is also checked against the request path the server logged. The shell already did this for vh.parquet. The twin does the same, and returns the paths it saw when the one it wants is missing.

The suite already declines with the right reason when its endpoint is absent: the twin mirrors both pgc_skip names in its first test. Without pyarrow it declines through require_module. Where pgc-bucket.s3.local doesn't resolve it declines UNAVAILABLE_ENDPOINT, and the other tests decline with it. Neither name gets a ledger row, because CI emits neither.

CI: only the suites jobs had the step that adds s3.local pgc-bucket.s3.local to /etc/hosts. Without it the twin declines on every pytest-cluster run and exits 67. The same step is added to pytest-cluster in ci.yml and nightly.yml, after the tag fetch. In the parsed YAML, against main, that one step is the only change in each file. I checked locally: with no hosts entry, all 16 tests decline UNAVAILABLE_ENDPOINT, and the run refuses them as unexpected.

Measured on PG17: the same failures by name in shell and twin

mutant (objstore/columnar_objstore_module.c) main's shell this shell / twin
pathonly (always path-style) 1 fail 2 fail
vhostall (always virtual-host) 2 fail 3 fail
noep (AWS_ENDPOINT_URL override dropped) 3 fail 3 fail
gsrequire (gs:// demands AWS_ENDPOINT_URL) 1 fail 1 fail
gswrongdef (default host mistyped) 12/12 pass 1 fail
plusspace (space → +) 12/12 pass 4 fail
plusraw (+/= unencoded) 12/12 pass 2 fail

noep only reddens gs://: s3:// re-reads the environment through os_require_env, so dropping the first getenv doesn't reach it.

Virtual-host resolution was supplied locally by a private mount namespace with its own /etc/hosts. The global file was left unchanged. Head passes 16/16 in both harnesses on each of PG15–19.

Counts

16 rows, 11 dated and 5 never. The 5 never: premise: fixture up, s3:// path-style read, virtual-host read == path-style read, the removal-proof read, and s3:// with no endpoint demands one. None of the seven mutants changes their answers.

Census 3883 → 3888 never, 2282 → 2293 dated, 6165 → 6181 rows; suites_not_covered 102 → 101; cluster_tests 2748 → 2764; TESTS.md §268. Gate: guards 476, collection 2764, ledger gate rc=0, self-test PASSED.

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

@OffgridwithJD Approving #1716 at e1f87326.

The suite work is sound: every vacuity fix fails closed, and every mutant in the table reds the same names in the shell and the twin. One note: the new CI step's comment spells a corpus filename.

I ran everything on PG17, as root, with the server running as postgres. "Main's shell" is the parent commit's test/objstore_addressing.sh (22c36173), run against the same mutated source. This host's /etc/hosts already resolves pgc-bucket.s3.local to 127.0.0.1, so nothing declined.

Note, not blocking: the CI comment names a corpus file

Not blocking, after a second look (jdatcmd): #1653 failed because the guard reads the pytest-guards job, and a filename there reads as a hand-kept file list. These comments sit in jobs the guard does not read, and a comment is not a list, so nothing here can go stale silently. Reword them if you like, for consistency with #1653.

The step this PR adds to .github/workflows/ci.yml (line 185) and nightly.yml (line 749) opens with this comment:

# test_objstore_addressing.py, the twin of objstore_addressing.sh, reads

This is the defect from #1653, where a comment in ci.yml spelled test_docs_style.py. There, the guard test_ci_derives_the_file_list_rather_than_repeating_it failed with the job names no corpus file literally. That guard only reads the pytest-guards job, and this comment is in pytest-cluster, so it passes here. The reason for the rule still holds: if the file is renamed, the comment goes stale and no check notices.

Suggested fix: write "The objstore_addressing twin reads through virtual-host-style S3 addressing…" in both files. Main's ci.yml line 710 already names the suite this way ("objstore_addressing and objstore_listing exercise…").

Clean runs

run result
shell 16 passed + 0 failed + 0 unrunnable + 0 skipped = 16, PASSED
twin, serial 16 pass + 0 fail + 0 unrun = 16, 16 passed
compare_to_bash.py literal matches: 18 | template matches: 0 | missing: 0, "every bash property is covered"

The twin under -n 2 gave 2 passed, 14 errors twice. The errors were the private cluster did not start and then Address already in use. That is not this PR: the merged test_hardening.py fails the same way under -n 2 on this head (2 passed, 5 errors, 10 lines of Address already in use). Each xdist worker builds the module fixture and calls pick_port() with the same slot. CI runs the cluster files serially, so I ran every twin below serially.

The three vacuity fixes fail closed

mutant main's shell this head's shell twin
gswrongdef: default https://storage.google.com 12 passed + 0 failed, PASSED 1 red 1 FAILED, same name
plusspace: os_uriencode_path writes a space as + 12 passed + 0 failed, PASSED 4 red 4 FAILED, same names
plusraw: + and = pass unencoded 12 passed + 0 failed, PASSED 2 red: both request-path checks 2 FAILED, same names

The reds, verbatim from this head's shell:

  • gswrongdef: FAIL gs:// with no endpoint goes to storage.googleapis.com (the allow-list refuses that host by name): got [0] want [1]. The server log shows the refusal named storage.google.com:443.
  • plusspace: both "reads back" checks (got [] want [3 18]) and both "URI-encoded once" checks (got [] want [yes]).
  • plusraw: FAIL and its path-style request path is the key URI-encoded once: got [] want [yes] and the virtual-host equivalent. Both reads still answered 3 18, as the PR says.

The other mutants match the table

mutant this head's shell twin
pathonly: virtualHost = false && (…) 2 red: key alone in the path, both checks 2 FAILED, same names
vhostall: virtualHost = true || (…) 3 red: the three bucket-in-path checks 3 FAILED, same names
noep: ep = NULL; in place of getenv("AWS_ENDPOINT_URL") 3 red: gs:// read, write, read-back 3 FAILED, same names
gsrequire: if (false && isGs) 1 red: gs:// with no endpoint 1 FAILED, same name

I did not run main's shell under these four.

Registries

  • check_ledger.tsv: 16 rows for the suite. Their names match the shell's 16 RESULT names exactly (diff is empty).
  • The 11 dated rows match the reds above. The 5 never rows did not red under any of the 7 mutants.
  • Neither pgc_skip message ("pyarrow is required to write the fixture", "pgc-bucket.s3.local does not resolve; …") is a ledger row.
  • Arithmetic, counted from the file: 7343 rows, 4298 never, 3045 dated. 4298 + 3045 = 7343, and the budget line is checks_never_observed_red 4298.
  • No test file imports pyarrow (the import-site scan printed []). The twin writes its Parquet files from a python3 child.

Not blocking

  • The PR body's counts are stale against the files. The body says census 3883 → 3888, 6165 → 6181 rows, 102 → 101, cluster_tests 2748 → 2764 and §268. The files say 4293 → 4298, 7327 → 7343, 50 → 49 and 3614 → 3630.
  • Under noep, the twin's accounting: line printed 13 pass + 0 fail + 0 unrun = 13 beside 3 FAILED tests. VacuityError refuses a failed-query reading before the check is recorded. That is the shared harness, not this PR.
  • CI is not green. pytest (cluster tests, PG 17) and PG 18 failed or were cancelled, and suites (PG 18) failed on native_backend_crash (with-reader: cluster accepts new connections: got [] want [1]), a suite this PR does not touch. So CI has not yet run the new hosts step in pytest-cluster.
  • I measured on PG17 only.

🤖 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-approved on c36fab4, the restate onto main 9ab6a4c. restate_check.sh against my earlier approval: every content file carries the same change; only the registries (budget, TESTS.md, expected_tests) differ, and the head's census agrees with its tsv.

jdatcmd and others added 7 commits October 23, 2026 03:15
… from the table under test no longer vouches for it (#1341)

Ports test/native_fold_deferral.sh to test/pytest/test_native_fold_deferral.py,
all 13 checks by the shell's name, one test per check, on a private database.

Three holes in the shell, each measured red-first on PG17 against main and fixed
in both harnesses:

- The heap mirror was `CREATE TABLE fd_heap AS SELECT * FROM fd_t`, read through
  the scan under test. With the writer storing one p1 value off by one
  (writecorrupt), the fold, the row path and the mirror all agreed: main passed
  13/13. The mirror is now written from the generator, and writecorrupt reddens
  both heap-mirror checks in both twins.
- The answers were read with `psql ... | tail -1` and compared with check. With
  the server stopped before the correctness section, all three answer checks
  passed on empty strings. Readings now run with ON_ERROR_STOP and come back
  empty on failure; the checks are check_text, and the counts go to check_num
  raw instead of defaulting to 0. Stopped server: only the three checks read
  before the stop pass.
- Three premises are reddened by a one-line pgcolumnar change: the fold's ANALYZE
  line (foldno), the EXPLAIN group total (badtotal), the writer's stripe limit
  (nostripe). They lost the prefix (#1339).

Eight mutations on PG17 each redden the same names in shell and twin: nodefer 3,
deferall 1, nocursor 3, dropsurv 4, writecorrupt 2, foldno 1, badtotal 2,
nostripe 1. Head is 13/13 in both on each of 15-19.

Seeded: 13 rows, 9 dated, 4 never. Census 4192 -> 4196 never, 2952 -> 2961 dated,
7157 rows; suites_not_covered 69 -> 68; cluster_tests 3416 -> 3429; TESTS.md §301.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d through the reader no longer vouches for it (#1341)

Ports test/native_lazy_slot.sh to test/pytest/test_native_lazy_slot.py, all 10 checks by
the shell's name, one test per check. Two holes in the shell, each measured red-first on
main's shell on PG17 and fixed in both harnesses:

- The heap mirror was a copy of lz_c (INSERT INTO lz_h SELECT * FROM lz_c), read through
  the reader under test. With the writer storing c40 off by one (writec40), main passed
  10/10. It is now written from the generator, and writec40 reddens 4 checks.
- Five comparisons read `q ... | tail -1` and compared with check, so two failed reads
  agreed, and a timing whose statement failed read as fast. With the server stopped after
  setup, 6 of 10 passed on main. A new qv helper names the side in its failure, the
  comparisons use check_text, and a failed timing is "no reading": all 10 now fail.

Mutants on PG17, the same failing checks by name in shell and twin: writec40 4, stale (the
fetch no longer clears the slot) 2, eager (decode the whole row) 1, short (one column too
few) 6. An earlier stale mutant that kept only PgColumnarSlotStoreDeferred's clear was
inert, because the fetch clears the slot first; the faithful one drops both. Head is
10/10 in both on PG15-19.

Seeded: 10 rows, 7 dated, 3 never. Census 4196 -> 4199 never, 2961 -> 2968 dated, 7167
rows; suites_not_covered 68 -> 67; cluster_tests 3429 -> 3439; TESTS.md §302.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… counted nothing no longer passes (#1341)

Ports test/native_metadata_flush.sh to test/pytest/test_native_metadata_flush.py,
every check by the shell's name, one test per check (#1341). Four holes, each
measured on PG17 against main's shell, which passed all six checks under each:

- A witness that counted nothing passed. With the flush session never counting an
  open (zerocount, opens=0 for every flush), "narrow and wide open the same number"
  (0 = 0) and "a small constant" (0 <= 6) both passed. A new check wants at least
  the three tables every flush writes; zerocount now reddens it.
- The heap mirror was copied from the columnar table, so a writer that stored 1777
  as 1778 (writecorrupt) agreed with itself. The mirror is now written from the
  generator, and the sum check reddens: got 40020020, want 40020000.
- Nothing showed the zone rows were read. The reader rules vectors out by value after
  decoding, with or without a usable zone map, so Vectors Skipped alone cannot say.
  A new check wants a vector ruled out by its zone row alone (Skipped minus Ruled Out
  by Value); zone rows with no min/max (zonenull) redden it.
- "premise: the flush emits the metadata-open witness" reads pgcolumnar's own
  behaviour: a flush that never begins its session (nosession) reddens it. It lost
  the prefix (#1339).

Mutants on PG17, the same names red in shell and twin: noreuse (#445 reverted) 2,
zerocount 1, writecorrupt 1, zonenull 1, nosession 4. Head is 8/8 in both on each of
15-19.

Seeded: 8 rows, 6 dated, 2 never (the row count and the range read against the
mirror). Census 4199 -> 4201 never, 2968 -> 2974 dated, 7175 rows; suites_not_covered
67 -> 66; cluster_tests 3439 -> 3447; TESTS.md §303.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d counter no longer reads as 0 (#1341)

Every counter in the shell was read through `${x:-0}`, and four checks want exactly
0. With the server stopped before the feature-off arm, "with the feature off nothing
is filtered early" and "a volatile qual is refused the two-pass path" passed on main
(PG17). The counters are always printed, so a missing line is a failed reading:
counter() now returns "no reading (...)" when the EXPLAIN fails or prints no such
line, the feature-off readings run under ON_ERROR_STOP, and check_num refuses them.
With the server stopped both checks now fail.

Four premises a one-line pgcolumnar change reddens lost the prefix (#1339): "the
query runs on the columnar custom scan" (noscan), the two "nothing was pruned"
checks (noscan: their counters are not printed), and "the same shape without the
volatile term does use it" (nocount, noscan, droprow).

The twin test_native_late_materialization.py makes every check by the shell's name,
one per test, on a private database with serial plans (a parallel plan prints
per-worker averages). Every reading is a value: a failed query is a query_error(),
an unprinted counter is "no reading", never 0.

Measured on PG17, the same failing checks by name in shell and twin: nocount 2,
noscan 7, droprow 3, skipkeep 1, nocursor 1, novolguard 1. matall (a rejected row's
payload built anyway, still counted) reddens nothing: no counter reports per-row
materialization, and the answers are the same by design. Head is 14/14 in both on
each of 15-19.

Seeded: 14 rows, 8 dated, 6 never. Census 4201 -> 4207 never, 2974 -> 2982 dated,
7189 rows; suites_not_covered 66 -> 65; cluster_tests 3447 -> 3461; TESTS.md §304.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd a writer mutant dates four rows (#1691 review)

jdatcmd's #1691 review: three premises read the columnar table n, and dropseven
(pgcolumnar_tuple_insert drops the row whose id is 7, one line) reddened all three.
The two fixture premises ("the fixture loaded every row", "the needle is selective")
now read the heap mirror h, which pgcolumnar does not write, so no pgcolumnar change
reddens them. "heap agrees on how many rows match" compares n with h and is not a
premise: the prefix is dropped and the row is renamed, not removed.

dropseven on PG17, shell and twin: the same 7 names red, the two fixture premises
green (7 pass + 7 fail = 14 in both). It dates four rows that were never: "heap agrees
on how many rows match", "the executor still reports the rows the scan filtered", "and
the answers are the same with it off" and "and the volatile query still matches the
heap mirror". Clean: 14/14 in both on PG17.

Census for this suite: 14 rows, 12 dated + 2 never (was 8 + 6): never -4, dated +4.
The header's "nothing pruned at the vector level" premise is a check since #1691.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… that drops every free row no longer passes (#1341)

Every reconcile check counted only the injected stale free_space row, so a reconcile
that dropped EVERY free row, leaking all reusable space, passed all 11 checks on main
(overreconcile). A plain compact, which does not reuse, must now keep the legitimate
free ranges the earlier compact_rewrite left (count and bytes), with a premise that one
exists. And "row count intact" compared two readings with plain check: with the server
stopped before it, main passed it on two empty strings. It is check_num now. The
reuse-cycle loop is unrolled so its two check names are literals the grader reads.

New twin test/pytest/test_native_reclaim_reconcile.py, one check per test under the
shell's names, on a private database; readings are values (query_error()), row sets
compared as an md5 over sorted rows.

Mutants on PG17, same names red in shell and twin: noreconcile 4, overreconcile 1.
13/13 shell and twin on each of 15-19. Seeded: 13 rows, 5 dated, 8 never.
Census 4203 -> 4211 never, 2986 -> 2991 dated, 7202 rows; suites_not_covered
65 -> 64; cluster_tests 3461 -> 3474; TESTS.md §305.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd nofree dates it (#1692 review)

jdatcmd's #1692 review: "premise: a legitimate free range exists before the plain
compact" reads pgcolumnar's own free list, which holds ranges only because
compact_rewrite recorded them. nofree (record_free_space drops its insert, one line in
columnar_metadata.c) leaks every freed range and reddened only that premise, so a real
leak read as "the fixture was wrong". It is renamed "compact_rewrite recorded the free
ranges it left" (the row is kept, renamed) and dated by nofree.

nofree on PG17: shell "FAIL  compact_rewrite recorded the free ranges it left: got [no
(0|0)] want [yes]", 12 + 1; the twin fails the same name, 12 pass + 1 fail. Clean: 13/13
in both. The next check still compares against a non-empty legit_before.

Census for this suite: 13 rows, 6 dated + 7 never (was 5 + 8): never -1, dated +1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jdatcmd and others added 12 commits October 23, 2026 03:40
…longer passes as refused (#1341)

Ports test/native_page_offset_bound.sh to test/pytest/test_native_page_offset_bound.py,
every check by the shell's name, one test per check. Three holes in the shell, each
measured red-first on PG17 against main and fixed in both harnesses:

- Every refusal arm counted any psql failure as refused. With the whole-group
  containment guard removed (noguardwhole) the backend segfaulted and the arms still
  passed; with the projected guard removed (noguardproj) a later XX001 ("row group 1
  is inconsistent with its column chunks") passed the projected overrun arm. Each arm
  now wants XX001 with the guard's own message, "lies outside row group".
- Survival was a bare SELECT 1, which answers again once crash recovery has finished.
  With the probe 4s late, main passed both whole-group survival arms with the backend
  killed. It now also reads the server log's count of backends killed by a signal
  since the arm began.
- Nothing read an intact table, and every arm wants a refusal. A guard over-applied
  to refuse every chunk (overall), or a legal chunk ending exactly at its group end
  (offby1), passed all 10 checks on main. Two controls now read rows behind an
  OFFSET 0 fence on each path; an aggregate over an intact table is answered without
  decoding a chunk, which is why the first version of these controls stayed green.

Mutants on PG17, same failing checks by name in shell and twin: noguardwhole 6,
noguardproj 2, offby1 1, overall 2. Without the whole-group guard the read either
segfaults or fails decoding, depending on where it lands (three runs: shell 4-6, twin
6); its three refusal checks are red every time. 12/12 shell and twin on each of
15-19.

Seeded: 12 rows, 10 dated, 2 never (the projected survival arms). Census 4210 ->
4212 never, 2992 -> 3002 dated, 7214 rows; suites_not_covered 64 -> 63;
cluster_tests 3474 -> 3486; TESTS.md §306.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lated param no longer passes (#1341)

Ports test/native_param_pushdown.sh to test/pytest/test_native_param_pushdown.py,
every check by the shell's name, one test per check, and closes three holes in
the shell, each measured on PG17 against main and fixed in both harnesses.

- The PARAM_EXEC guard was untested. With it removed (freezeexec) main passed
  5/5: Begin freezes before any outer row sets the param, so the frozen value is
  its initial one and "ts >= <that>" prunes nothing; the lower-bound correlated
  answer stayed 3002. The arm now reads the scan's Usable Skip Predicates (must
  be 0), asks the upper bound "ts <= thr.lo" (77000), where the frozen value
  prunes every group, and checks the plan rescans the columnar scan per outer row.
- A value frozen once and reused by a later execution (staleparam) passed 5/5:
  every arm ran one execution per session. Two executions of one generic plan in
  one session now each count (2001) and prune (18 of 20) with their own value.
- Survival was a bare SELECT 1: with a backend SIGKILLed before it, main passed.
  It now also requires the server log's crash count to be unchanged.

Mutants on PG17, same failing checks by name in shell and twin: shiftparam 4,
noparam 2, freezeexec 2, staleparam 2. The harness kill reds "backend alive" in
both. 10/10 shell and twin on each of 15-19.

Seeded: 10 rows, 6 dated, 4 never. Census 4212 -> 4216 never, 3002 -> 3008
dated, 7224 rows; suites_not_covered 63 -> 62; cluster_tests 3486 -> 3496;
TESTS.md §307.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wrong order no longer passes (#1341)

Ports test/native_sort_by.sh to test/pytest/test_native_sort_by.py, every check by
the shell's name, one test per check, and fixes two holes in the shell, each
measured red-first against main's shell on PG17 (PG18 for the virtual arm):

- The physical order was never read. Group skipping on host was the only evidence
  of a sort, so a rewrite that applied only the first key (firstonly: ts left
  unordered) passed 16/16. The rows are now read in scan order and every adjacent
  pair is compared on (host, ts) ("the scan order follows the declared key (host,
  ts)"), behind a premise that the inserted order is not already sorted, and the
  explicit-column sort must lay the rows out as the declared key does.
- Every refusal counted any psql failure. A backend aborting in place of the no-key
  refusal (keyabort) passed its own check; a crash in place of the virtual-column
  refusal (virtabort) passed 18/18 on PG18; a dropped declared column given the
  generic message (persgeneric) passed 16/16. Each refusal now wants its guard's
  SQLSTATE and a phrase of its message, every reading after a refusal waits for the
  server, and "backend still up after the refusals" reads the server log's crash
  count. The empty and single-row sorts read psql's exit status.

Mutants on PG17, the same checks red by name in shell and twin: firstonly 1,
persgeneric 1, keyabort 2, nosort 2, wrongcol 4, desc 4; virtabort on PG18 2.
Clean: shell 20/20 (+1 skip) on 15-17 and 22/22 on 18-19, twin 22/22 on each.
Before PG18 the virtual arm is recorded as a skip, and its two twin tests decline
(expected_unrunnable.txt).

23 rows, 9 dated, 14 never. Census 4216 -> 4230 never, 3008 -> 3017 dated, 7247
rows; suites_not_covered 62 -> 61; cluster_tests 3496 -> 3518; TESTS.md §308.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e index stale no longer passes (#1341)

Ports test/native_rewrite.sh to test/pytest/test_native_rewrite.py, every check
by the shell's name, one test per check, and fixes four holes in the shell, each
measured on PG17 against main and fixed in both harnesses:

- The threshold was untested from both sides. With every group that has a live
  row rewritten (overrewrite), or min_deleted_fraction ignored (nothreshold),
  main passed 17/17. Two new checks: "a second rewrite with nothing deleted
  rewrites no group" and "a group deleted below the threshold is not
  rewritten".
- vacuum's index rebuild was untested. With pgcolumnar_compact_relation's
  reindex skipped (noreindex), main passed 17/17: over the whole table the
  stale entries land on every live row once, so a count and a hash through the
  index both still matched. A narrow index range read does not (id 4097 read
  back as 6146). New check: "an index range read after vacuum returns the rows
  asked for".
- Four counts compared two readings with plain check, and "unique constraint
  still enforced" took any psql failure as enforcement. With the server stopped
  after the rewrite, four checks passed on main. Counts are check_num, and the
  refusal wants 23505 and n_id_uq's own message.

Mutants on PG17, same failing names in shell and twin: droprow 6, noindex 5,
overrewrite 2, nothreshold 1, noreindex 1. With the server stopped after the
rewrite, both harnesses fail 15 and pass the 5 read before it. 20/20 shell and
twin on each of 15-19.

Seeded: 20 rows, 9 dated, 11 never. Census 4230 -> 4241 never, 3017 -> 3026
dated, 7267 rows; suites_not_covered 61 -> 60; cluster_tests 3518 -> 3538;
TESTS.md §309.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ector's first value no longer passes (#1341)

The suite checked each zone-map bound for presence and width only, and every
column of its fixture ascended, so a vector's first value was always its minimum.
A writer that never raised a vector's max past its first value (nomaxupd), never
lowered its min (nominupd), or never folded a chunk max past its first vector
(foldmax) stored bounds of the right width, and main's shell passed 18/18 under
each, on PG17.

- v is now a permutation of 1..6150 (its values, nulls and counts unchanged), with a
  premise that it is not ascending within a vector.
- Four checks decode each bound of id, k and v (zm_le reads the stored native bytes)
  and compare it with the heap's own for its group and vector: every vector's min,
  every vector's max, every whole-chunk min, every whole-chunk max.
- "value_count sums (v, nullable)", "null_count sums (v, nullable)" and "whole-chunk
  sum(id) matches heap" compared two readings with check: with the server stopped they
  passed on two empty strings. They are check_num now; a stopped server fails every
  check read after it.

The twin test_native_zonemap.py asserts all 23 checks by the shell's names, one per
test. Mutants on PG17, the same names red in shell and twin: nomaxupd 2, nominupd 2,
foldmax 1, nominmax 5. Clean 23/23 in both on each of 15-19.

Seeded: 23 rows, 5 dated, 18 never. Census 4241 -> 4259 never, 3026 -> 3031 dated,
7290 rows; suites_not_covered 60 -> 59; cluster_tests 3538 -> 3561; TESTS.md §310.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orage id is unread (#1697 review)

jdatcmd's #1697 review: when the storage id could not be read, the twin fell back to
`WHERE storage_id = NULL`, which matches no row, and a count of bad rows over no rows is
the 0 nine checks want. The shell has no fallback: its empty $SID is a syntax error, so
each check fails. Each zone-map reading now goes through _zint, which returns sid's own
failure instead of querying; _bounds_match and the after-drop count short-circuit the
same way, so expect.num refuses each one, as under a stopped server.

nullsid (pgcolumnar_relation_storageid returns NULL for every relation, one line in
columnar_vacuum.c) on PG17:
- before: shell 3 passed + 20 failed; twin 12 pass + 6 fail, with nine names passing in the
  twin that fail in the shell ("id (int4) min width = 4 bytes", "k (int8) max width = 8
  bytes", "min/max present for all columns", "per-vector value+null = 1024", "per-vector
  value_count folds into chunk (k)", "sum null for non-int4 columns", "sum present for int4
  columns", "whole-chunk value+null = group rows", "zone maps cleaned on drop");
- after: the same 20 names red in both (comm prints nothing either way).
Clean: 23/23 in both. No check name or ledger row changes; nullsid is not used to date
rows (every reading is unreadable under it, so it says nothing about what each measures).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…red across scans no longer passes (#1341)

"nested columnar scans each hold their own handle" compared only the join's
count, which a shared handle still answers correctly: with one static zone-map
session shared across scans (the first implementation of #744), main's shell
passed it. It is now the witness, each of the join's two scans reporting its
own groups opened once, and the count is a check of its own, "nested columnar
scans return the right answer". The three scans around the one that errors in
a subtransaction must also each probe exactly their own groups.

Two premises a one-line pgcolumnar change reddens lost the prefix (#1339): the
probe count (probetwice, noprobe) and the middle scan's error (excludeall). A
new check holds zs2 to more than one group, which a writer ignoring
stripe_row_limit fails.

Six mutants on PG17 redden the same names in both twins: shared 5, reopen 5,
probetwice 4, noprobe 7, excludeall 8, nostripe 1. With the server stopped
after setup, only the check read before it passes. The re-resolved index oid
that columnar_metadata.c documents (idxagain) stays 13/13: no witness here
counts it. 13/13 shell and twin on each of 15-19.

Seeded: 13 rows, 12 dated, 1 never. Census 4259 -> 4260 never, 3031 -> 3043
dated, 7303 rows; suites_not_covered 59 -> 58; cluster_tests 3561 -> 3574;
TESTS.md §311.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d optx100 dates it (#1698 review)

jdatcmd's #1698 review: "premise: the fixture really has more than one chunk group"
reads pgcolumnar's own row_group catalog, the writer's output, so it can redden for a
writer bug; zs2's twin check had already lost the prefix for that reason. It is renamed
"the fixture has more than one chunk group" (the row is kept, renamed) and dated by
optx100 (columnar_write_state.c: the writer multiplies the table's stripe_row_limit by
100, one line).

optx100 on PG17: shell "FAIL  the fixture has more than one chunk group: got [no] want
[yes]" and "FAIL  zs2 has more than one chunk group", 11 + 2; the twin fails the same 2
names. Clean: 13/13 in both.

Census for this suite: 13 rows, 13 dated + 0 never (was 12 + 1): never -1, dated +1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ver pruned no longer passes (#1341)

Ports test/native_zonemap_narrow.sh to test/pytest/test_native_zonemap_narrow.py,
every check by the shell's name, one check per test.

Nothing showed a group was skipped. The pruning check read only the answer to
c1 > 19000, which is 1000 rows whether or not a group was removed: with group
skipping reading the zone map but never excluding a group (noexclude), or never
consulting it (nozm), main's shell passed all four checks on PG17. Two checks now
read the scan's own "Columnar Chunk Groups Removed by Filter" under EXPLAIN ANALYZE,
which must be 9 of the 10 groups on the wide table and on the narrow one. Both
fail under each mutant, the same names in shell and twin.

The width check compared two counts with -le and errored to "no" on a blank one; it
now refuses an unread count as "no reading" rather than comparing it, so its lossy_arms.tsv row is removed. With the server
stopped after setup, all six checks fail in both harnesses.

Mutants on PG17, the same failing checks by name in shell and twin: noexclude 2,
nozm 2, wholegroup (#314 reverted) 1, overprune 5. Head is 6/6 in both on each of
15-19.

Seeded: 6 rows, 6 dated, 0 never. Census 4259 -> 4259 never, 3044 -> 3050 dated,
7309 rows; suites_not_covered 58 -> 57; cluster_tests 3574 -> 3580; TESTS.md §312.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ulls as values no longer passes (#1341)

Ports test/native_vecdecode.sh to test/pytest/test_native_vecdecode.py, every check
by the shell's name, one test per check. Three holes in the shell, each measured on
PG17 against main and fixed in both harnesses:

- The fixture had no nulls. With the fold ignoring the validity bitmap (nonull),
  main passed 24/24. A nullable column w (NULL on every seventh row) now runs
  through the fold ("the fold answers count and sum of a nullable column over a
  skipping range": 51|2444 against heap's 44|2606 under nonull) and through a
  scalar parity check, behind a heap premise that the range holds nulls.
- Missing counters read as 0. "decoded plus skipped is the group's vectors" passed
  as 0 + 0 = 0 with the plans failing, and two aggregate parities compared two empty
  readings: with the server stopped before the plans, main passed 3. Every counter
  must now be read, the parities use check_text, and the same stop fails all 28.
- Five "premise:" checks read pgcolumnar's own behaviour: nofold reddens the three
  "reaches the fold" checks, decodeall and noskip the fold arm's decode count, and
  noskip "the narrow range skips vectors". All lost the prefix (#1339).
  "a ruled-out vector is not decoded" now names both readings when it fails, so its
  lossy_arms.tsv row is removed.

The poison check ("the fold does not count rows from a vector it never decoded")
is dated by holeread: the fold ignores the skip mask, and the mutant zero-fills
the hole, so the read is deterministic on a non-assert build too.

Mutants on PG17, the same failing checks by name in shell and twin: decodeall 3,
holeread 1, nonull 1, nofold 4, noskip 3. Head is 28/28 in both on each of 15-19.

Seeded: 28 rows, 10 dated, 18 never. Census 4259 -> 4277 never, 3050 -> 3060 dated,
7337 rows; suites_not_covered 57 -> 56; cluster_tests 3580 -> 3608; TESTS.md §313.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…can and optdiv100 date eight rows (#1700 review)

jdatcmd's #1700 review: four premises read the plan pgcolumnar produces, and noscan
(pgcolumnar.enable_custom_scan off by default, one line; the C initializer too, or an
assert build traps) reddens all four, as it did the same check in #1691. They lose the
prefix (rows renamed, not removed): "the narrow plan is a columnar custom scan", "the
wide plan is a columnar custom scan", "no whole group is pruned on the narrow query" and
"the wide range skips none, so the arms really differ". The review asked to apply the same
test to "one row group": optdiv100 (the writer divides the table's stripe_row_limit by 100,
one line in columnar_write_state.c) reddens it (got [51] want [1]), so it loses the prefix
too. The ground-first order stays, so a failure still names its cause.

PG17, shell = twin by name:
- noscan: 15 red in both (twin: 3 recorded fails + 12 named VacuityErrors); dates the four
  renamed checks plus "EXPLAIN reports Columnar Vectors Decoded", "and it is not stuck at
  zero when a full scan decodes everything" and "control: and the same counter reads every
  vector when nothing is skipped", all never before.
- optdiv100: 6 red in both; dates "one row group, so every difference is per-vector".
- clean: 28/28 in both.

Census for this suite: 28 rows, 18 dated + 10 never (was 10 + 18): never -8, dated +8.
The heap-side nulls check stays a premise: it reads h.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t be its own guard's (#1341)

The refusal arms read only the SQLSTATE, and four of the six refusals share
22023 (absent id, count mismatch, id-less file, duplicate requested id). With the
"file carries no field ids" guard removed (noanyid), an id-less file fell to the
absent-id refusal, also 22023, and main's shell passed 11/11. Each arm now wants its
own guard's primary message, read with VERBOSITY verbose in the shell and
diag.message_primary in the twin.

Survival was a bare SELECT 1, which answers again after crash recovery: with the
absent-id refusal turned into abort() (abortabsent) it passed, and the crash took
five later refusal arms with it. Survival now also reads the server log's count of
killed backends, and every reading first waits for the server, so a crash fails its
own arm and survival only.

The twin's writer runs as a subprocess, so the module imports no pyarrow, and every
test declines under the shell's skip name before it asks for the fixture.

Measured on PG17, each giving the same failing names in shell and twin: noanyid 1,
abortabsent 2, noscalar 1, offbyone 5, bypos 6. With the server stopped before the
projection arms, all 11 fail. Clean: 11/11 shell and twin on each of 15-19.

Seeded: 11 rows, 9 dated, 2 never. Census 4269 -> 4271 never, 3068 -> 3077 dated,
7348 rows; suites_not_covered 56 -> 55; cluster_tests 3608 -> 3619; TESTS.md §314.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jdatcmd and others added 15 commits October 23, 2026 04:20
… through the reader under test no longer passes (#1341)

Every codec was compared with c_none.parquet read back through read_parquet, the
reader under test. A defect after decompression -- in the page decode every codec
shares -- corrupted both sides alike: with one decoded byte flipped in every page
over 4 KB, the same way for every codec (postcorrupt), main's shell passed 9/9 on
PG17. Each file now decodes to the generator's rows, built in SQL from the same
generator the crafter wrote them from; postcorrupt fails five checks in both
harnesses (and the gzip sum reads 199990033, not 199990000).

Each file's codec is now read from its own metadata. The crafter printed it to
stderr and nothing read it: with every file written uncompressed, main passed 9/9;
the three codec premises now fail.

Survival read a bare SELECT 1. With the crafted-size guard aborting (hdrabort) and
the probe 3 s late, main passed it; it now also reads the server log's crash count
and fails as "no ([1], 1 backend crashes)".

The fixtures move into test/craft_parquet_codecs.py, which the twin runs as a
subprocess so its fixture never imports pyarrow; each twin test declines under the
shell's skip name first. The gzip sum is now check_num against 199990000, so two
failed reads can't agree, and the codec and FDW loops are unrolled into literal names.

postcorrupt, nocodec, mapcodec and hdrabort each fail the same checks by name in
shell and twin on PG17 (5, 4, 1, 2). Head is 14/14 in both on each of 15-19.

Seeded: 14 rows, 7 dated, 7 never (the four codec premises read pyarrow's metadata,
and the FDW counts hold under every mutation). Census 4271 -> 4278 never,
3077 -> 3084 dated, 7362 rows; suites_not_covered 55 -> 54; cluster_tests
3619 -> 3633; TESTS.md §315.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le read through the reader under test no longer passes (#1341)

The twin, test/pytest/test_native_parquet_units.py, makes each of the shell's 20
checks under its name, on a private database, and declines under the shell's pyarrow
skip name before it asks for the fixture.

Two holes in the shell, each measured red-first on PG17 against main, fixed in both
harnesses:

- The pushdown oracle was the two-group millis file read back through read_parquet,
  the reader under test. With every row after a page's first taking the next
  dictionary entry (dictshift), the scan and the mirror shifted alike and main passed
  19/19. The oracle is now the generator's hourly series, built in SQL, and a new
  check, "the millis file reads back exactly as written", compares the whole file
  with it. Under dictshift both fail, the same names in shell and twin.
- The nanos truncation checks compared each file with a second file read the same
  way, with plain check. With the server stopped before them, main passed both on two
  empty reads. They now want the toward-zero microseconds as literals, through
  check_text, and fail with the server stopped.

The per-unit and per-tag loops are unrolled, so every name is a literal the grader
reads.

Mutants on PG17, each failing the same checks by name in shell and twin: msasus
(MILLIS read as micros) 9, nsasus (NANOS read as micros) 4, floordiv (nanos
truncated toward -inf) 1, dictshift 2. Head passes 20/20 in both harnesses on each of
15-19.

Not tested, because there is nothing to mutate: the reader skips isAdjustedToUTC, and
a timestamptz column decodes exactly as a timestamp one, so there is no UTC
adjustment for a check to catch.

Seeded: 20 rows, 13 dated, 7 never. Census 4278 -> 4285 never, 3084 -> 3097 dated,
7382 rows; suites_not_covered 54 -> 53; cluster_tests 3633 -> 3653; TESTS.md §316.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shared with the oracle no longer passes (#1341)

The oracle h was filled with read_parquet of the same file, and read_parquet
shares the scan's decoders. With a text decoder dropping the last byte of a
string (txtdec) or an INT64 decoder off by one above 9000 (i64dec), both sides
read the same wrong values, and main's shell passed 15/15 under each. h is now
built in SQL from the generator test/craft_parquet_projection.py wrote the file
from; under those mutants the projections that read c, or b, fail.

No table had a dropped column, so a target's attribute number always equalled
its index, and pqfdw_compute_needed matching the needed set by index (posneed)
passed 15/15. ftd declares a column ahead of a..d and drops it; "after a dropped
column, projection [d] == oracle" and "after a dropped column, one column reads
1" fail under posneed.

The five projection checks were built from a loop variable the grader cannot
read; they are now literal names. Crafting moved to
test/craft_parquet_projection.py, which both harnesses run.

Mutants on PG17, each failing the same names in shell and twin: posneed 2,
txtdec 3, i64dec 2, droplast 8, noproj 8. 17/17 shell and twin on each of 15-19.
Seeded: 17 rows, 15 dated, 2 never. Census 4285 -> 4287 never, 3097 -> 3112
dated, 7399 rows; suites_not_covered 53 -> 52; cluster_tests 3653 -> 3670;
TESTS.md §317.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rd no longer stands in for the chunk-count refusal (#1341)

Ports test/native_parquet_hardening.sh to test/pytest/test_native_parquet_hardening.py,
all 18 checks by the shell's name, one test per check. Both twins now write their files
with one crafter, test/craft_parquet_hardening.py, so neither can drift from the other.

Four holes, each measured on main's shell on PG17 and fixed in both harnesses:

- The short-chunk verdict matched any "has a malformed row group", which five guards
  raise. With the chunk-count guard removed (nochunk) a later guard rejected the file and
  main passed 18/18. The verdict now wants this guard's own detail ("carries 1 column
  chunks, but the schema has 2 leaf columns").
- The "still reads" arms counted rows. A date decoded one day late, or a time or
  timestamp one microsecond late (dateoff, timeoff, tsoff), passed 18/18. They now
  compare the decoded values with the ones the crafter wrote.
- "no wrapped -25536 appears" counted -25536 in the in-range file, which never holds
  40000, so with the int2 guard removed (noi2) it still passed. It now reads the
  out-of-range file itself, which must refuse with 22003.
- Survival was a bare SELECT 1. With the chunk refusal turned into abort() and the probe
  5 s late, main passed it. It now also reads the server log's count of killed backends,
  and every reading waits for the server first, so a crash fails only its own arm.

The range refusals also want their own type's message, since four share 22008.

Mutants on PG17, each the same failing names in shell and twin: noi2 5, overi2 2,
notime 1, nodate 1, nodateall 2, nots 1, nochunk 1, chunkabort 2, tsoff 1, dateoff 1,
timeoff 1. Head passes 18/18 in both harnesses on each of PG15-19. "date offset
overflow" is guarded twice (the offset-overflow branch and IS_VALID_DATE), so only
nodateall, which removes both, reaches it.

Seeded: 18 rows, 16 dated and 2 never (parquet_schema on the bad file, and the valid
two-column read). Census 4287 -> 4289 never, 3112 -> 3128 dated, 7417 rows;
suites_not_covered 52 -> 51; cluster_tests 3670 -> 3688; TESTS.md §318.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ursion guard is pinned to 54001 (#1341)

The footer parser's two unbounded recursions, PgColumnarThriftSkip and walk_schema,
are each guarded by check_stack_depth(). The twin crafts the two deep footers by hand
(no pyarrow), pins max_stack_depth, and asserts each raises 54001 rather than
SIGSEGV; a shallow valid chain still reads its one column; and read_parquet refuses
the crash input the same way.

Measured on PG17, same failing checks by name in shell and twin: nothrift 2
(nested-struct and read_parquet refusals), nowalk 1 (schema-chain refusal),
walkstrict 1 (the valid-file control). Head 8/8 in both harnesses on PG15-19. 8 rows,
4 dated and 4 never (the survival checks, which no mutation crashes at the pinned
depth). Census 4289 -> 4293 never, 3128 -> 3132 dated, 7425 rows; suites_not_covered
51 -> 50; cluster_tests 3688 -> 3696; TESTS.md §319.

Waits for the merge freeze on #1341.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d each runs its own input (#1708 review)

jdatcmd's #1708 review:
1. The twin's read_parquet survival test never read stack_nested.parquet; it called _alive()
   only, so it could not see a crash in read_parquet (skipabort: 1 passed alone). It now
   runs the read_parquet statement first, through the same helper the refusal test uses.
2. All four survival checks, in both harnesses, were a bare SELECT 1 straight after the
   trigger, which answers again once crash recovery is over (with an abort() in the Thrift
   guard and a 3 s delay, both survival checks after the aborts passed). Following #1701 and
   #1705: each arm takes a baseline of "terminated by signal" in the server log right
   before its trigger, every reading first waits for the server, and survival wants SELECT 1
   and an unchanged crash count. A crash now fails only its own arm and its survival check.

Each survival check red by name in both, under an abort() at its input's decode site (PG17):
- thriftabort (PgColumnarThriftSkip: `if (stack_is_too_deep()) abort();` for its guard):
  shell 4 red: the nested-struct and read_parquet refusals and "backend survived the
  nested-struct footer" / "backend survived read_parquet on the crash input" ("got [no ([1],
  1 backend crashes)]"); the twin fails the same 4 names (2 recorded, 2 VacuityErrors).
- walkabort (the same in walk_schema): the schema-chain refusal and "backend survived the
  schema-chain footer", in both.
- validabort (walk_schema aborts on entry): the schema-chain refusal and survival, "a shallow
  valid schema chain still reads" and "backend survived the valid file", in both.
Clean: 8/8 in both. The four survival rows are dated by these mutants.

Census for this suite: 8 rows, 8 dated + 0 never (was 4 + 4): never -4, dated +4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ackend no longer passes as up (#1341)

Ports test/native_parquet_streaming.sh to test/pytest/test_native_parquet_streaming.py,
every check by the shell's name, one test per check. Both harnesses now write their files
with one shared crafter, test/craft_parquet_streaming.py, extracted from the shell's four
heredocs; its files are byte-identical to theirs (md5 of all 13).

Measured on main's shell on PG17:
- Survival was a bare SELECT 1, which answers again once crash recovery has finished.
  With the second-dictionary-page refusal turned into abort() (dictabort), "backend
  survived the crafted page areas" passed, and the crash also failed "v2 level lengths
  past the page are rejected", read while the server recovered. Survival now reads the
  server log's count of killed backends since its section began, and every reading waits
  for the server, so under dictabort exactly the dictionary arm and survival fail, the
  same names in shell and twin.
- Hardening: the two refusals that took any ERROR ("value read of the sparse file fails
  cleanly", "unparseable page header is rejected") and the zero-page verdict now want the
  page loop's own refusal, "could not decode Parquet column". No mutant here separated
  them (hdrerr, a header-window refusal raised as a different ERROR, is inert: the noise
  file is refused elsewhere), so this is stated as hardening, not a measured hole.

Mutants on PG17, each the same names in shell and twin: nodict 1, nosize 1, nooffset 1,
noprogress 1 (the 60 s timeout), dictabort 2. nov2 stays green, as the suite's header
argues. Head is 20/20 in the shell and 21/21 in the twin on each of 15-19; the twin's
extra test mirrors "crafted files were built", which the shell records only when the
crafter fails.

Seeded: 20 rows, 5 dated and 15 never; no row for either pgc_skip name. Census 4289 ->
4304 never, 3136 -> 3141 dated, 7445 rows; suites_not_covered 50 -> 49; cluster_tests
3696 -> 3717; TESTS.md §320.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ust be its own guard's (#1341)

Ports test/native_parquet_partition.sh to test_native_parquet_partition.py, every check
by the shell's name, one test per check. Three holes, each measured red-first on PG17
against main's shell and fixed in both harnesses:

- pruned_for() read a failed or empty EXPLAIN as 0. With the server stopped before them,
  "a predicate matching everything prunes nothing", "a predicate on a file column prunes
  no files" and "a volatile clause prunes nothing" passed on main. "Files Pruned" prints
  only above 0, so a missing line now counts as 0 only beside the plan's Files line, and
  is "no reading" otherwise; all three now fail with the server stopped.
- errs() took any ERROR. With the unknown-partition-column guard removed (nocolcheck),
  a later 42804 stood in; with the explicit encoded-NUL guard removed (nonul), the
  encoding check's 22021 did; main passed 35/35 under each. Every refusal now wants its
  own SQLSTATE and message. Measured: what refuses a file column declared as a partition
  column is the missing-component guard ("has no v= directory component"), not the
  column-count check the comment described; the comment now says so.
- Survival was a bare SELECT 1: with the unknown-column refusal turned into abort() and
  the probe 5 s late, main passed it. Both survival checks now read the server log's
  count of killed backends, and every reading waits for the server.

Mutants on PG17, the same names red in shell and twin: noprune 9, invprune 20,
novolguard 1, nonul 1, nocolcheck 1, abortunk 3. With the server stopped mid-suite
both harnesses pass the 10 checks read before the stop and fail the other 25. Clean
35/35 in both harnesses on each of 15-19.

Seeded: 35 rows, 29 dated, 6 never. Census 4304 -> 4310 never, 3141 -> 3170 dated,
7480 rows; suites_not_covered 49 -> 48; cluster_tests 3717 -> 3752; TESTS.md §321.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s nothing no longer passes (#1341)

Projection and row-group skipping were never read: every arm compared answers,
which the executor's recheck and a discarding projection keep right. With every
column decoded (noproj) or no row group skipped (noskip), main's shell passed
14/14. Two checks now read the scan's EXPLAIN ANALYZE counters: a one-column query
must read 1 of 11 columns, and over a 200,000-row file of four row groups a
selective predicate must skip three. (Only a same-type constant is used for
skipping: id > 196608 against an int8 column skips nothing, id > 196608::int8
skips three. Correct either way, a missed optimization only.)

The refusal arms accepted any psql failure. With the scan's missing-path guard
removed (nopathguard) the backend crashed and main passed; with the short-file
guard removed (nolen) the bad-magic guard refused instead and main passed. Each arm
now wants its own guard's SQLSTATE and message, a survival check reads the server
log's count of killed backends, and setup waits for the server so a crash fails
only its own arm and survival.

The oracle is a heap table written from the generator rather than the columnar
table the file came from, and the self-join counts are check_num, since two failed
reads agreed.

Seven mutants give the same failing names in shell and twin on PG17: noproj 1,
noskip 1, overskip 4, nopathguard 2, nolen 1, acceptopt 1, rgsize 2. Head is 20/20
in both on each of 15-19. 20 rows, 10 dated, 10 never. Census 4310 -> 4320 never,
3170 -> 3180 dated, 7500 rows; suites_not_covered 48 -> 47; cluster_tests 3752 ->
3772; TESTS.md §322.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ps (#1711 review)

jdatcmd's #1711 review: "and the skipped scan still returns every matching row" counted
`id > 196608`, an int4 constant against an int8 column, a cross-type comparison that skips
no row group (#1712), so the count came from a full scan and was right whatever skipping
did. skipshift (pqfdw_compute_skip moves each group's skip decision to the next group:
still 3 skipped, but the only matching group among them) passed 20/20 in both twins.

The count now runs the plan's own query, `id > 196608::int8`, in the shell and the twin.
The plan check above it ("a selective predicate skips the three row groups it cannot
match", 3 skipped) reads that same query, so it already shows the count's scan skips; it
is a dated check, not a premise, since a one-line reader change reddens it, so no new
premise is added.

skipshift on PG17: shell "FAIL  and the skipped scan still returns every matching row:
got [0] want [3392]", 19 + 1; the twin fails the same name. Clean: 20/20 in both. The row
is re-dated by skipshift (it was dated by a mutation that skipped every group); census
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t agreed with itself no longer passes (#1341)

The round trips compared read_parquet with the source table, and the columnar scan
reads that table both to write the file and as the oracle. With the columnar writer
storing int4 777 as 778 (writecorrupt), the file and the "source" agreed on 778, and
main passed 12/12. Every round trip now hashes the generator's own rows, built in SQL.

The refusals counted any psql failure, a crash included. With the scalar
physical-type guard removed (notype), main passed 12/12. Two refusals share 42804, so
each now requires its SQLSTATE and its own message; every reading waits for the
server first, and a survival check reads the server log's count of killed backends.

read_parquet's own pg_read_server_files gate was never tested: with it removed
(nogate), main passed 12/12. A role holding USAGE on the schema but not the role is
now refused by the gate's own message, beside a member that reads every row.

Five mutants give the same failures by name in shell and twin on PG17: writecorrupt
2, notype 1, nogate 1, nocount 1, nonull 7. 15/15 shell and twin on each of 15-19.
Census 4320 -> 4324 never, 3180 -> 3191 dated, 7515 rows; suites_not_covered
47 -> 46; cluster_tests 3772 -> 3787; TESTS.md §323.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ound-trip rows name their mutant's real side (#1713 review)

jdatcmd's #1713 review:
1. "reject missing column definition list" wants 42601 "a column definition list is
   required", which PostgreSQL's parser raises before pgcolumnar runs (the message is not in
   pgcolumnar's source). It reads PostgreSQL alone, so it is renamed "premise: reject missing
   column definition list" in the shell, the twin, TESTS.md and the ledger; it stays never.
2. The two round-trip rows said "columnar_reader.c: the columnar writer stores int4 777 as
   778". The mutant that dated them is in columnar_reader.c, but it is read-side
   (store_att_byval returns 778 for 777), so the text now says "the columnar reader returns
   int4 777 as 778 (store_att_byval)", reproducible from the row alone. (The review's
   write-side version in columnar_write_state.c reddens both rows too.)

Clean on PG17: 15/15 in both twins. Census unchanged (rows renamed/re-described only).

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

Port test/objstore_listing.sh to test/pytest/test_objstore_listing.py, one
test per shell check under its literal name, on a private cluster restarted
with AWS_* and the SigV4 fixture server paging two keys at a time.

Three holes in the shell, measured red-first on PG17 before the fix:
- the two non-progress refusals share 08P01 and read only the SQLSTATE; the
  no-keys page replays its token, so with the progress check removed the
  token check refused it and main passed 12/12. Each now wants its guard's
  own message (refusal_of).
- no check made a LIST fail, so a non-2xx read as an empty prefix passed
  12/12. objstore_http_server.py gains a __listing_status__ hook and a check
  wants the HTTP status refusal (08006).
- the exact-key "no LIST" count read a log nothing proved alive; a control
  now wants the key's own GET in it.
A survival check reads the server log's count of killed backends, and every
refusal waits for the server first, so an abort fails only its own check.

Seven mutants on PG17, the same reds in shell and twin; 15/15 on 15-19.
Ledger 15 rows (11 dated, 4 never); never 4324 -> 4328, dated 3191 -> 3202,
snc 46 -> 45, cluster_tests 3787 -> 3802.

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

test/pytest/test_native_rewrite_conc.py runs the shell's 11 checks by their
literal names, one per test.

Holes found by mutating main's shell on PG17:
- A compact_rewrite under AccessExclusiveLock passed 5/5. The workers just
  waited for it, and nothing claimed non-blocking. New arm: a rewrite held
  mid-verb (pgcolumnar.maintenance_hold_ms) with its lock proven granted in
  pg_locks. A reader and a writer, each under SET LOCAL lock_timeout = '3s',
  must finish while it still holds the lock. Then the hold must be proven
  still held, and the rewrite must complete.
- The overlap was assumed. A new check requires the compactor to move rows
  during the workers' window. Head moves 24-30 rows with 3-5 serialization
  retries per run.

Mutants, PG17, same reds in both harnesses:
- noconflict (the conflict check never raises): 3 reds, the same in 3/3 runs
  (whole-row, count/sum, range)
- ael (AccessExclusiveLock): 2 (reader, writer)
- alwaysconflict (over-apply): 5
- nohold: 2 (holds-lock, still-held)
Head: 11/11 in both harnesses on PG15-19.

Ledger: 11 rows, 9 dated and 2 never (row count sane, the held rewrite
itself completed). Census 4328 + 3202 == 6165 -> 4330 + 3211 == 7541;
suites_not_covered 45 -> 44; cluster_tests 3802 -> 3813.

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

test_objstore_addressing.py is the twin of objstore_addressing.sh, every
check under the shell's name, on a private cluster with its own SigV4
fixture server started with --virtual-host-base s3.local.

Two holes in the shell, red-first on main's code (PG17):
- the gs:// no-endpoint check passed whenever the message did not name
  AWS_ENDPOINT_URL, so a mistyped default host (gswrongdef) passed 12/12.
  It now requires the allow-list refusal naming storage.googleapis.com:443.
- no key needed URI-encoding, so a space sent as '+' (plusspace) or '+'/'='
  left bare (plusraw) passed 12/12. A key "dir sp/a+b=c.parquet" is now
  read under both addressings, and its request path checked in the log.

The twin also checks the request path the server logged for every
addressing read, and asserts each refusal by SQLSTATE plus the phrase
naming the cause.

CI: the pytest-cluster jobs (ci.yml, nightly.yml) gain the step that
resolves s3.local and pgc-bucket.s3.local, which only the suites jobs had.
Without it the twin declines UNAVAILABLE_ENDPOINT and the run ends 67.

Ledger: 16 rows, 11 dated by seven mutations with the same reds in both
harnesses, 5 never. Census 4330 + 3211 == 7541 -> 4335 + 3222 == 7557;
suites_not_covered 44 -> 43; cluster_tests 3813 -> 3829. No row for
either pgc_skip name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@OffgridwithJD
OffgridwithJD force-pushed the test/1341-objstore-addressing-twin branch from c36fab4 to 907c768 Compare October 23, 2026 05:14

@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-approved on 907c768, the one-pass restate onto main c7f0de4. restate_check.sh against my last approval: the PR's own change is the same in every content file; only the registries (budget, TESTS.md, expected_tests) differ, and the head's census agrees with its tsv.

@jdatcmd
jdatcmd merged commit 907c768 into main Oct 23, 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