Skip to content

test: parallel_copy_dedup gains its pytest twin, and a fingerprint of the file's first read no longer passes (#1341) - #1668

Merged
jdatcmd merged 3 commits into
mainfrom
test/1341-parallel-copy-dedup-twin
Oct 23, 2026
Merged

jdatcmd merged 3 commits into
mainfrom
test/1341-parallel-copy-dedup-twin

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

@OffgridwithJD

What this does

Ports test/parallel_copy_dedup.sh to test/pytest/test_parallel_copy_dedup.py (#1341), all 17 checks by the shell's exact name, one test per check. It also fixes two holes in the shell, where a real defect left main's shell green, and drops two premise: prefixes under #1339.

What was fixed in the shell (and the twin)

  1. The fingerprint could cover only part of the file. The changed file differed from the first in its first bytes, and the whole file (237,788 bytes) fitted in the coordinator's first 256 KiB read. A fingerprint of the first read alone still loaded the changed file, so main's shell passed 14 of 14. Now:
    • the changed file differs only in its last row;
    • 25,000 rows put that row past the first read, and a new premise checks it ("premise: the changed row lies past the first 256 KiB of the file");
    • a new check, "the records hold the SHA-256 of each loaded file's bytes", compares the table's load_fingerprint rows with the server's sha256(pg_read_binary_file(...)) of each file.
  2. The NOTICE was never read. The header promises that a refused load "says what it skipped rather than returning a silent 0". With the NOTICE removed, main's shell passed 14 of 14. A new check reads it: "and says in a NOTICE that it skipped the load, rather than returning a silent 0".
  3. Two premises that are not premises (pytest audit: 260 confirmed checks that cannot fail, prove something else, or carry the wrong label #1339). "without dedup a repeated load doubles the table, which is the defect" and "and each of those loads reported its rows" lose the premise: prefix. Dedup is pgcolumnar's own default, and forcing it on reddens both. Both are dated in the ledger.

Red names under each edit and mutant (PG17)

Edit or mutant main's shell this shell twin
columnar_parallel_copy.c: fingerprint only the first read (break after the first pg_cryptohash_update) 0 red, 14/14 4 red: "a changed file at the same path is loaded, not skipped (#403 item 7)", "and reports the rows it loaded", "the skipped load left exactly one record for that table and file", "the records hold the SHA-256 of each loaded file's bytes" same 4
columnar_parallel_copy.c: the skip's NOTICE never raised (if (false && skipped)) 0 red, 14/14 1 red: "and says in a NOTICE that it skipped the load, rather than returning a silent 0" same 1
columnar_parallel_copy.c: dedup always on (dedup = true) 4 red, both still premise: 4 red: "without dedup a repeated load doubles the table, which is the defect", "and each of those loads reported its rows", "asking for no dedup loads again even though the file is on record", "and reports those rows too" same 4
columnar_parallel_copy.c: the refused load commits anyway 2 red 2 red: "the second load of the same file stores nothing (#403 item 7)", "a changed file at the same path is loaded, not skipped (#403 item 7)" same 2
columnar_metadata.c: a fingerprint matches in any table 3 red 3 red: "the same file loads into a DIFFERENT table (the record is per table)", "and reports its rows", "asking for no dedup loads again even though the file is on record" same 3
columnar_parallel_copy.c: the refused load reports the rows it discarded 1 red 1 red: "and reports 0 rather than claiming it loaded them" same 1
Harness edit: ROWS=20000 not applicable 1 red: "premise: the changed row lies past the first 256 KiB of the file" same 1

No mutant crashed the backend, so no red came from collateral damage.

Gates

  • Clean runs: 17 of 17 on PG15, 16, 17, 18 and 19, in both the shell and the twin.
  • Ledger: 17 rows, 13 dated by the 6 mutations and 4 never. The 4 never are the size premise, "the first load stores its rows normally (Opportunities from the ClickHouse VLDB 2024 paper, ranked against what we already have #403 item 7)", "and reports them", and "no prepared transaction leaked through any of it". Census: rows 6096 -> 6113, never 3853 -> 3857, dated 2243 -> 2256.
  • suites_not_covered 105 -> 104; cluster_tests 2677 -> 2694 (by collection); TESTS.md section 265.
  • The ledger gate passes on each of the five clean logs against origin/main; orphan-scan finds 0 orphans.
  • Guards: 474 passed. test_harness_deps.py and test_capability_decline.py with --pg-config: 47 passed. harness_selftest.sh: 1248 of 1248.
  • compare_to_bash.py: "every bash property is covered", 17 literal matches, 0 missing.
  • The capability-import scan prints []. The twin catches only psycopg.Error.

In the twin, the prepared-transaction check counts only its own database's prepared transactions. Other suites in a pytest session prepare transactions of their own.

Re-checked by me before pushing (PG17, main 5a94757): m1-notice: main's shell 14/14, this shell 1 failed ("and says in a NOTICE that it skipped the load …: got [0] want [1]"), the twin the same test. m2-firstread: main's shell 14/14, this shell 4 failed, including "the records hold the SHA-256 of each loaded file's bytes", the twin the same 4 tests.

🤖 Generated with Claude Code

@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 dcdec1f5. Measured on PG17 with ~/pg/17-p, the shell then the twin as shipped. Main's shell comes from main at 293fb132.

run main's shell this shell twin
head, clean — 17/17 (and 17/17 on PG18) 17 passed (and on PG18)
firstread: the hash loop breaks after the first 256 KiB buffer PASSED 4 red: changed file loaded, reports its rows, one record per table and file, the records hold the SHA-256 SAME 4
nonotice: if (false && skipped) before the NOTICE PASSED 1 red: "and says in a NOTICE that it skipped the load …" SAME 1
saltrun (mine, over-apply): the fingerprint XORed with a random byte, so no load is ever recognised — 6 red: the second load stores nothing, reports 0, the NOTICE, one record, the SHA-256 records, the changed-file arm SAME 6
harness: server stopped just before the SHA-256 check — that check and the prepared-xact check are both red, with the connection error as the reading, not a pass —
  • The two relabelled premises: I checked them in the diff and trust your dedup = true mutant's reds. I didn't re-run that one.
  • Non-blocking: "premise: the changed row lies past the first 256 KiB of the file" can't be reddened by a product mutant: it reads pg_stat_file of a file the shell wrote, so the label is right. But it hard-codes 262144, which mirrors PCOPY_FINGERPRINT_BUFSZ. If that buffer grew to 512 KiB, the premise would stay green while the changed row fell inside the first read again, and firstread would no longer redden anything. Reading the #define from src/columnar_parallel_copy.c instead of the literal would close that.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd force-pushed the test/1341-parallel-copy-dedup-twin branch from dcdec1f to 4b8ba9a Compare October 22, 2026 03:11

@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-approving the restated head 4b8ba9a0 (I approved dcdec1f5). This PR's own changes to parallel_copy_dedup.sh, test_parallel_copy_dedup.py, check_ledger.tsv, test_compare_to_bash.py and unported_suites.txt are identical line for line; only the budget and counters moved. The earlier measurements stand.

🤖 Generated with Claude Code

@jdatcmd

jdatcmd commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Restated as a stack: now on #1666 (it merges after it), registries regenerated over that head with the same tooling. The suite and twin are unchanged; the ledger gate passed (rc=0) and the guards passed (476). Head 4b8ba9a.

jdatcmd and others added 3 commits October 22, 2026 18:31
…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>
… 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>
… the file's first read no longer passes (#1341)

test/pytest/test_parallel_copy_dedup.py ports all 17 checks of
test/parallel_copy_dedup.sh by name. Two properties the shell's header
promises could break with main's shell still green; both are fixed in
both twins.

- The changed file differed from the first in its first bytes, and the
  whole file fitted in the coordinator's first 256 KiB read. With the
  fingerprint taken from the first read alone, main's shell passed
  14 of 14. The changed file now differs only in its last row, 25,000
  rows put that row past the first read (a premise checks this), and a
  new check, "the records hold the SHA-256 of each loaded file's bytes",
  compares the table's records with the server's sha256() of each file.
- The header says a refused load "says what it skipped rather than
  returning a silent 0", but no check read the NOTICE. With the NOTICE
  removed, main's shell passed 14 of 14. A new check reads it.
- "premise: without dedup a repeated load doubles the table, ..." and
  "premise: and each of those loads reported its rows" lose their
  prefix under #1339: dedup is pgcolumnar's own default, and forcing
  it on reddens both.

Measured, PG17, shell and twin:
- fingerprint of the first read alone: 4 red; main's shell 0.
- the skip's NOTICE never raised: 1 red; main's shell 0.
- dedup forced on: 4 red. The refused load committed anyway: 2 red.
  A fingerprint matched in any table: 3 red. The refused load reporting
  the rows it discarded: 1 red. Each red set is identical in both twins.
- the fixture shrunk to 20,000 rows: 1 red, the size premise.
- clean: 17 of 17 on PG15 to PG19 in both twins.

Ledger: 17 rows, 13 dated by the 6 mutations and 4 never.
suites_not_covered 74 -> 73, cluster_tests 3222 -> 3239, TESTS.md
section 296. Census 4132 -> 4136 never, 2818 -> 2831 dated, 6967 rows. The ledger
gate and the guards pass over eb4aa69.

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

jdatcmd commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Restated again, onto #1666's new head (main 9ab6a4c underneath), because the registries conflicted with main. The registries are regenerated over that head by the same tooling as before; the suite and twin are unchanged; the ledger gate and the guards pass (see the commit message). Re-approval needed. Head 7e8c10f.

@jdatcmd
jdatcmd force-pushed the test/1341-parallel-copy-dedup-twin branch from 4b8ba9a to 7e8c10f Compare October 22, 2026 18:50

@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 7e8c10f7, on #1666's eb4aa699. The diff outside the ledger, the budget, expected_tests and TESTS.md hashes the same as the approved 4b8ba9a0's (unported_suites.txt and the compare list included). The tsv is sorted, and its census (6967 / 4136 / 2831) matches the budget.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd merged commit 3330425 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