Skip to content

test: seven #1339 projection findings, each red-first in both twins (#1339) - #1674

Merged
jdatcmd merged 1 commit into
mainfrom
fix/1339-projection-batch
Oct 23, 2026
Merged

jdatcmd merged 1 commit into
mainfrom
fix/1339-projection-batch

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

@OffgridwithJD: this fixes seven #1339 findings in the projection suites. Each fix covers both twins.

What changed, per finding

For each finding I first ran a one-line pgcolumnar mutation on main. Then I fixed the check and ran the same mutation again. "Green" means the suite passed. The pytest twin stops at a test's first red, so where the shell suite reds more names, the twin shows the first one.

finding mutation main shell / main twin fixed shell / fixed twin
#1339 test_projection_drop_column.py:113 (weak) columnar_tableam.c: the unlocked DROP COLUMN ownership check deleted green 8/8 / green FAIL "a non-owner is refused before it waits for the table lock" (got 55P03) / FAIL same name
#1339 test_projection_drop_column.py:119 (mislabelled, twin mismatch) both DROP COLUMN ownership checks deleted FAIL "a non-owner learns nothing from a projected column" (no shell check for the premise) / FAIL "a non-owner learns nothing..." (the premise never ran) FAIL "and the two refusals are the same error, so neither leaks" (2BP01 vs 42501), plus 2 more / FAIL same name
#1339 test_projection_parallel.py:217 (mislabelled) columnar_customscan.c: projection path parallel_workers = 1 FAIL "premise: EXPLAIN ANALYZE launched two workers", "premise: ANALYZE printed a rows= line...", "workers share..." / FAIL "premise: EXPLAIN ANALYZE launched two workers" the same three names without the prefix / FAIL "EXPLAIN ANALYZE launched two workers"
#1339 test_projection_privilege.py:259 (twin mismatch) read_projection's aclcheck_error replaced by elog (XX000) FAIL "and THAT refusal is the C check..." + 2 / FAIL "a role with EXECUTE but no SELECT is refused read_projection" + 2 FAIL "and THAT refusal is the C check..." + 2 / identical 3 names
same reconstruct_via_projection's aclcheck_error replaced by elog FAIL 2 reconstruct: names / FAIL "and is refused reconstruct_via_projection, which leaks..." + those 2 identical 3 names in both
same PgColumnarRequireNoRowSecurity raises 42501, not 0A000 FAIL "and the refusal says the feature is not supported..." / FAIL "read_projection now refuses a policy-restricted caller (#563)", "and so does reconstruct_via_projection" FAIL "and the refusal says...", "and so does reconstruct_via_projection" / identical
#1339 test_projection_scan_io.py:260 (weak) if (pages < 1) pages = 1; deleted green 11/11 / green 2/2 FAIL "and its scan is not priced at zero pages" (free at 0.00) / FAIL same name
#1339 test_projections.py:459 (weak) the executor never opens the projection (if (false && cstate->projName != NULL)), but EXPLAIN still names it green 76/76 / green 34/34 FAIL "executing it reads the projection, so the filter sees fewer rows than the base holds" (all 17980 of 17980) / FAIL same name
#1339 test_projections.py:600 (inaccurate) projection pages multiplied by 1000000, so the count plans the base every :600 arm green (2 other reds) / same FAIL "the old-snapshot count is planned as a projection scan" (+3 plan names) / FAIL same name (+2)
same projection read and liveness cache on GetLatestSnapshot() FAIL "old snapshot projection scan does not see post-snapshot rows" / FAIL "session A responded post-commit" (wrong name) FAIL "old snapshot projection scan does not see..." / FAIL same name

Notes on :600

  • The plan. EXPLAIN on PG17 shows that the count plans the projection pmp, with no ANALYZE. So the triage's guess that it reads the base did not hold. Nothing asserted the plan, though, so a new arm checks it in both twins.
  • The triage's candidate mutation (GetLatestSnapshot() for the projection read only) stays green on main and after the fix. The base liveness cache still uses the query snapshot and hides it. The two-site mutation above is the one that reddens the arm.
  • The timeout arms. The twin's two session-A timeout arms re-asserted the count. They now read whether session A answered. The shell poll matched the bare token, which an ERROR echoes, so a failed query read as an answer. It now needs the token followed by digits. With session A's search_path set to pg_catalog, the shell on the old poll reddened only the counts. Both twins now redden "session A opened snapshot".

Clean runs

Every touched suite and twin is green on PG15, 16, 17, 18 and 19:

suite shell checks pytest
projection_drop_column 11 1 test
projection_parallel 13 1 test
projection_privilege 28 12 tests
projection_scan_io 12 2 tests
projections 78 34 tests

One PG18 twin run (projection_parallel) first errored at setup with "Address already in use", because another agent's cluster held the port. It passed when re-run. After the rebase onto f0b4380, all five suites and twins are green again on PG17.

Gates

  • Ledger gate, each of the five suites, PG16 + PG18 logs: rc=0, "census stated 3863, ledger holds 3863: they agree", ceiling "104 -> 103, which does not rise".
  • compare_to_bash, all five pairs: "VERDICT: every bash property is covered".
  • Guard set: "1657 pass + 0 fail + 0 unrun", "476 passed".
  • orphan-scan, each suite: orphans=0. LC_ALL=C sort -c passes.

Ledger

  • projection_drop_column is seeded with 11 rows, so suites_not_covered goes from 104 to 103.
  • 19 rows are dated by the ten mutations, 2 are renamed (the parallel premises), and 14 are new.
  • checks_never_observed_red goes from 3868 to 3863: 6141 rows = 3863 never + 2278 dated.
  • expected_tests.txt does not change, because collection did not move.

Not in scope

The USAGE_ONLY pair in projection_privilege has the same mismatch: the twin asserts 42501 and the shell suite asserts "refused". It was not in this batch, so it is unchanged.

Re-checked by me before pushing (PG17, main f0b4380): :260, the one-page floor removed (customscan.c:2789): main's shell 11/11; this shell 1 failed, "and its scan is not priced at zero pages: got [free at 0.00] want [priced]"; the twin fails the same test.

🤖 Generated with Claude Code

jdatcmd added a commit that referenced this pull request Oct 22, 2026
… stale stored digest no longer passes (#1339)

#1339 listed "premise: a tdigest stored in a columnar table still answers"
as mislabelled: nothing compared the stored digests, so a pgColumnar storage
change could redden that premise alone, or pass it. hll's stored sketch has
had a byte-for-byte arm; tdigest had none.

New arm, in both harnesses: "a tdigest stored in a columnar table comes back
byte for byte". The daily digests are built once into a heap table and
stored from there, because a tdigest depends on the order its inputs arrive
in; digests rebuilt from heap could differ for reasons that are not storage.
The union-within-0.1% check keeps its premise label: a storage change that
altered a digest now reddens the arm first, the suite's rule for its bounds.

Measured on PG17:
- Mutant: a reader returning each long value (over 512 bytes) as the
  previous long value read (columnar_reader.c). Main's suite: 11 + 1
  failed, only "an hll stored in a columnar table comes back byte for
  byte"; every tdigest check passed. This branch: 11 + 2 failed, the new
  arm "got [f] want [t]" and the hll arm; the twin fails the same two
  names (read from its own records).
- Rejected mutants: flipping the last byte of a long value, or zeroing its
  second half, makes tdigest's output function take the server down, so
  every later arm fails as collateral. Not used for dating.
- Clean: shell 13/13 on PG15 to PG18, 7 + 6 skipped on PG19 (no hll
  there); the twin passes on each; the new arm passes on all five.
- compare_to_bash: 15 literal matches, 0 missing. Guards 474 passed.

Ledger: 6141 + 1 = 6142 rows, 3863 never (unchanged), 2278 -> 2279 dated.
suites_not_covered and cluster_tests unchanged. Stacked on #1674; the
ledger gate passes, and guards 476 passed.

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

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdatcmd Approving 88393cd8. Measured on ~/pg/17-w: shell, then the twin as shipped, main = f0b43802. Mutants in ~/pgc-tools/rv1674/.

mutant main shell / twin this shell / twin
dropunlocked (the unlocked owner check deleted, columnar_tableam.c:3236) 8/8 / passed 1 red / SAME 1
dropboth (both owner checks deleted) 1 red / 1 3 red; the twin's first red is "and the two refusals are the same error, so neither leaks"
nopagefloor (if (pages < 1) pages = 1 removed) 11/11 / passed 1 red: "and its scan is not priced at zero pages" / SAME
noprojexec (if (false && cstate->projName != NULL) at Begin) 76/76 / 34 passed 1 red: "executing it reads the projection…" / SAME
projpar1 (the projection path's parallel_workers = 1) 3 red, two of them premise: the same 3 without the prefix / the twin's first
noreadacl (mine: read_projection's SELECT check removed) 4 red / 3 4 red / 3
bigfloor (mine, over-apply: every projection priced at ≥1000 pages) 3 red / 2 3 red / 2
clean, PG17 and PG18 — 11, 12, 13, 28, 78 / 1, 2, 1, 12, 34 passed, on both

Where the counts differ (dropboth, bigfloor, projpar1, noreadacl), every twin red is in the shell's red set. The twin's multi-check tests stop at their first red, as your body says.

The ledger diff matches: the two projection_parallel renames, projection_drop_column seeded with 11 rows, and each newly dated row is one of the measured reds. "workers share the covering projection scan" moves from 2026-10-13 to 2026-10-22, which is right for a last-red column.

🤖 Generated with Claude Code

…1339)

Fixes #1339 test_projection_drop_column.py:113 and :119,
test_projection_parallel.py:217, test_projection_privilege.py:259,
test_projection_scan_io.py:260 and test_projections.py:459 and :600, each in
its shell twin too. For each finding, a one-line mutation of pgcolumnar was
first run on main, where both twins stayed green (or reddened the wrong
name). After the fix, the same mutation reddens the target by the same name
in both twins. The pytest twin stops at a test's first red, so it can show
fewer names than the shell suite.

- #1339 test_projection_drop_column.py:113 (weak). The stranger arms read only
  42501, so deleting the unlocked ownership check in columnar_tableam.c passed
  (main: 8/8 shell, twin green). New arm in both: while the owner holds SHARE,
  a stranger is refused at once. Under the mutation it waits out lock_timeout:
  "a non-owner is refused before it waits for the table lock" got 55P03.
- #1339 test_projection_drop_column.py:119 (mislabelled, twin mismatch).
  "premise: and the two refusals are the same error" came after the two
  constant arms, so it could never run red; the shell suite did not have it.
  With both ownership checks deleted, main's twin reddened "a non-owner learns
  nothing". The check loses the prefix, moves first, and joins the shell
  suite. Under that mutation both twins now redden "and the two refusals are
  the same error, so neither leaks" (2BP01 against 42501).
- #1339 test_projection_parallel.py:217 (mislabelled). With parallel_workers
  = 1 on the projection path, main reddened "premise: EXPLAIN ANALYZE launched
  two workers" in both twins (and the rows= premise in the shell suite). Both
  lose the prefix; the same mutation reddens them under their new names.
- #1339 test_projection_privilege.py:259 (twin mismatch). The twin asserted
  42501 (and 0A000 under the policy) twice on one error. With read_projection's
  aclcheck_error replaced by elog, main's shell reddened "and THAT refusal is
  the C check" and the twin reddened "a role with EXECUTE but no SELECT is
  refused read_projection". The twin's first arm now asks only "refused", as
  the shell does. The shell's reconstruct arms now read the SQLSTATE, as the
  twin did: main's shell stayed green with reconstruct's aclcheck_error
  replaced. Under each of the three mutations (both elogs, and the RLS refusal
  raised as 42501), the two twins now redden identical names.
- #1339 test_projection_scan_io.py:260 (weak). With the one-page floor
  deleted, an empty projection is priced at 0 pages and is still chosen (main
  green in both). New arm in both: "and its scan is not priced at zero pages"
  (PG17: total cost 5.00, and 0.00 under the mutation).
- #1339 test_projections.py:459 (weak). With the executor never opening the
  projection while EXPLAIN still names it, main was green in both. New arm in
  both reads EXPLAIN ANALYZE: the filter sees fewer rows than the base holds
  (5980 against 17980 on PG17; 17980 of 17980 under the mutation).
- #1339 test_projections.py:600 (inaccurate). EXPLAIN shows that the count
  plans pmp (Columnar Projection: pmp), so the triage's guess that it reads
  the base did not hold. But nothing asserted it. With projection pages
  multiplied by 1000000, so the count plans the base, every :600 arm stayed
  green on main. New arm in both: "the old-snapshot count is planned as a
  projection scan". The triage's candidate mutation (GetLatestSnapshot() for
  the projection read only) stays green: the base liveness cache still uses
  the query snapshot. With both on GetLatestSnapshot(), main's shell reddened
  "old snapshot projection scan does not see post-snapshot rows" but the twin
  reddened "session A responded post-commit". The twin's two timeout arms now
  read whether session A answered. The shell's poll no longer accepts an
  ERROR that echoes the token as an answer: with session A unable to see pm,
  main's shell reddened the counts but never "session A opened snapshot". It
  does now, in both twins.

Ledger: projection_drop_column is seeded (11 rows; suites_not_covered 104 ->
103). 19 rows are dated by ten mutations, 2 are renamed, and 14 are new.
checks_never_observed_red 3868 -> 3863 (6141 rows = 3863 never + 2278 dated).
Clean on 15-19 for every touched suite and twin. Gates: the ledger gate passes
for all five suites, compare_to_bash finds every bash property covered for
all five pairs, and the guard set passes (476 passed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdatcmd
jdatcmd force-pushed the fix/1339-projection-batch branch from 88393cd to 0772367 Compare October 22, 2026 18:31

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdatcmd Re-approved at 07723678. The restate changes only the bookkeeping: excluding the ledger, the budget, expected_tests.txt and TESTS.md, the commit's diff is byte-identical to 88393cd8's (same hash over the changed lines, and 12 files, +273/−43 both times). The tsv is sorted, and its census matches the budget's restated numbers.

🤖 Generated with Claude Code

@jdatcmd

jdatcmd commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Restated onto main 9ab6a4c (after #1740) as 0772367, because the budget conflicted.

  • Every content file is byte-for-byte the approved 88393cd's own change (restate_check.sh: 11 of 11 files the same).
  • Only check_ledger_budget.txt differs: the paragraph's census is re-derived on the new base. Ledger rows 6935 + 14 = 6949; never 4137 + 8 - 13 = 4132; dated 2798 + 6 + 13 = 2817; suites_not_covered 75 -> 74. A recount of the merged tsv gives 6949 / 4132 / 2817.
  • Guard tests: 804 passed locally.

@OffgridwithJD this needs a re-approval on 0772367 to merge.

jdatcmd added a commit that referenced this pull request Oct 22, 2026
… stale stored digest no longer passes (#1339)

as mislabelled: nothing compared the stored digests, so a pgColumnar storage
change could redden that premise alone, or pass it. hll's stored sketch has
had a byte-for-byte arm; tdigest had none.

New arm, in both harnesses: "a tdigest stored in a columnar table comes back
byte for byte". The daily digests are built once into a heap table and
stored from there, because a tdigest depends on the order its inputs arrive
in; digests rebuilt from heap could differ for reasons that are not storage.
The union-within-0.1% check keeps its premise label: a storage change that
altered a digest now reddens the arm first, the suite's rule for its bounds.

Measured on PG17:
- Mutant: a reader returning each long value (over 512 bytes) as the
  previous long value read (columnar_reader.c). Main's suite: 11 + 1
  failed, only "an hll stored in a columnar table comes back byte for
  byte"; every tdigest check passed. This branch: 11 + 2 failed, the new
  arm "got [f] want [t]" and the hll arm; the twin fails the same two
  names (read from its own records).
- Rejected mutants: flipping the last byte of a long value, or zeroing its
  second half, makes tdigest's output function take the server down, so
  every later arm fails as collateral. Not used for dating.
- Clean: shell 13/13 on PG15 to PG18, 7 + 6 skipped on PG19 (no hll
  there); the twin passes on each; the new arm passes on all five.
- compare_to_bash: 15 literal matches, 0 missing. Guards 474 passed.

Ledger: 6141 + 1 = 6142 rows, 3863 never (unchanged), 2278 -> 2279 dated.
suites_not_covered and cluster_tests unchanged. Stacked on #1674; the
ledger gate passes, and guards 476 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jdatcmd added a commit that referenced this pull request Oct 22, 2026
… stale stored digest no longer passes (#1339)

#1339 listed "premise: a tdigest stored in a columnar table still answers"
as mislabelled: nothing compared the stored digests, so a pgColumnar storage
change could redden that premise alone, or pass it. hll's stored sketch has
had a byte-for-byte arm; tdigest had none.

New arm, in both harnesses: "a tdigest stored in a columnar table comes back
byte for byte". The daily digests are built once into a heap table and
stored from there, because a tdigest depends on the order its inputs arrive
in; digests rebuilt from heap could differ for reasons that are not storage.
The union-within-0.1% check keeps its premise label: a storage change that
altered a digest now reddens the arm first, the suite's rule for its bounds.

Measured on PG17:
- Mutant: a reader returning each long value (over 512 bytes) as the
  previous long value read (columnar_reader.c). Main's suite: 11 + 1
  failed, only "an hll stored in a columnar table comes back byte for
  byte"; every tdigest check passed. This branch: 11 + 2 failed, the new
  arm "got [f] want [t]" and the hll arm; the twin fails the same two
  names (read from its own records).
- Rejected mutants: flipping the last byte of a long value, or zeroing its
  second half, makes tdigest's output function take the server down, so
  every later arm fails as collateral. Not used for dating.
- Clean: shell 13/13 on PG15 to PG18, 7 + 6 skipped on PG19 (no hll
  there); the twin passes on each; the new arm passes on all five.
- compare_to_bash: 15 literal matches, 0 missing. Guards 474 passed.

Ledger: 6141 + 1 = 6142 rows, 3863 never (unchanged), 2278 -> 2279 dated.
suites_not_covered and cluster_tests unchanged. Stacked on #1674; the
ledger gate passes, and guards 476 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdatcmd
jdatcmd merged commit 0772367 into main Oct 23, 2026
29 of 30 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