Skip to content

test: megabyte-scale values are stored exactly (#1341) - #1425

Merged
jdatcmd merged 8 commits into
mainfrom
test/1341-large-values
Oct 15, 2026
Merged

jdatcmd merged 8 commits into
mainfrom
test/1341-large-values

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Covers the #1341 core-am item "no megabyte-scale values". Test-only: no source change.

What it adds

A new suite, test/large_values.sh, and its pytest twin, test_large_values.py. A columnar table has no TOAST table: the writer detoasts each value and stores it inline. Until now, the largest value any test stored was 70 KB.

Each value goes into heap first and is copied to a columnar table. The two must agree on every value's length and md5:

value result
1 MB incompressible (md5 text) exact
two of 16 MB incompressible exact
200 MB of one repeated character exact
an UPDATE taking a 16 MB value that heap has TOASTed exact, and the other rows unchanged

Opt-in arm. With PGC_RUN_OVER_1GB=1, about 1.1 GB of incompressible text goes into one chunk group. The INSERT must be refused with 54000, commit nothing, and leave the server up. It needs several GB of memory, so CI records it as SKIP. I ran it by hand on PG17: it passed in 38 s (54000: out of memory, 0 rows, server answering). The twin doesn't carry it, because this layer has no SKIP verdict, so test_compare_to_bash.py declares the pair INCOMPLETE and says why.

Evidence

  • Green: 5/5 plus the skip on each of PG15–19, and the twin on each (about 29 s).
  • Red on PG17, both twins: a mutant where both the write path and the encoder skip pg_detoast_datum for an external value stores the TOAST pointer instead of the value. The copies and the update then read back wrong.
  • A narrower mutant, skipping only the write path's detoast, stayed green: the encoder flattens again (PgColumnarEncodeValueByLen). So that first detoast only saves work; correctness rests on the encoder.
  • Ledger: 6 rows, 3 dated. The census is 2774 + 6 = 2780 rows, 2095 + 3 = 2098 never, and 682 dated. The gate passes against origin/main. The harness selftest passes 1245/1245 and the guard set 433/433. cluster_tests goes 565 → 566.

🤖 Generated with Claude Code

https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N

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

Approved at cba8315.

  • What it holds: a 1 MB value, two 16 MB values, a 200 MB value, and an UPDATE taking a TOASTed heap value are stored by columnar with heap's length and md5. The other rows are unchanged, and an opt-in arm refuses a chunk over 1 GB.
  • Reproduced red first (PG17): with both the write path and the encoder skipping pg_detoast_datum for an external value (.so md5 766fad3c → fbed70e6), 2 passed + 3 failed + 1 skipped: the values copied into a columnar table are stored exactly, by length and md5: got [] want [1:1048576:45c3a302...], and the same for the UPDATE and the other rows. The twin fails with psycopg.errors.DataCorrupted: pgcolumnar: external TOAST pointer in a native value stream. Restored: green, md5 back to 766fad3c.
  • A note on the red reason: in my form of the mutant nothing is "read back wrong". The decoder guard in columnar.h refuses the INSERT with external TOAST pointer in a native value stream, and the shell's >/dev/null 2>&1 on the INSERT is why the FAIL reads got []. It is still red for the property under test, but the ledger's mutation text could say so.
  • Your narrower mutant, confirmed: with only the write path skipping the detoast (md5 46d0ec8d), 5/5 + skip stay green, because the encoder flattens the value again.
  • Opt-in arm: PGC_RUN_OVER_1GB=1 on PG17 gives 6/6 in 69 s.
  • Green: 5/5 + skip on PG17 and PG18, and the twin on each. The census, 2780/2098/682, matches the tsv, cluster_tests 566 matches collection, and the gate passes.

🤖 Generated with Claude Code

jdatcmd and others added 8 commits October 15, 2026 16:38
…#1407)

older snapshot still finds a recently deleted row by a new index. But it
emitted every deleted row, where heap's build skips a tuple dead to all.
So REINDEX, documented as what reclaims stale entries, reclaimed none:
9 rows in 10 deleted, and the rebuilt index kept its size where heap's
shrank to a tenth.

The build now also reads each row group's delete marks under a horizon
snapshot (xmin = xmax = the oldest non-removable xid, curcid 0), which
sees exactly the transactions committed before every running snapshot
began, and leaves out a row deleted there. The snapshot is registered:
PG 18+ asserts it in the visibility check.

am_contracts gains the arm in both twins, red on main (kept:2260992->
2260992); with the horizon moved to the next xid, the older-snapshot arm
goes red instead. limitations.md says when REINDEX keeps an entry.
cluster_tests +1.

Fixes #1407.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
…f sleeping

The arm from #1364 held a REPEATABLE READ snapshot, slept four seconds,
and assumed the DELETE and CREATE INDEX finished inside them. On a loaded
box they did not: the plan was taken before the index existed, and
'premise: the older snapshot's lookup is an index scan' read 0 on PG16.
A 5-second delay before CREATE INDEX reproduces it on PG17 every time.

Now a gate session holds an advisory lock. Session 1 takes its snapshot,
names itself, and queues for the lock. The script waits for the name,
deletes, builds the index, then ends the gate. With the same delay the
suite passes 28/28. A new premise in both twins says the older snapshot
counted all 10000 rows, so it predates the delete. The arm still goes red
when the index build skips deleted rows again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
…y those (#1341)

A new suite, alter_table_scans, and its pytest twin. A columnar table
keeps a deleted row in its file until compaction, so each form must see
the live rows and only those. On a columnar table and on heap with the
same rows:

- SET NOT NULL, ADD PRIMARY KEY and VALIDATE CONSTRAINT are refused over
  a NULL, a duplicate and a breaking row (23502, 23505, 23514), and each
  succeeds once that row is deleted;
- ALTER COLUMN TYPE and ADD COLUMN with a volatile default rewrite the
  table, keep the live rows and leave the deleted ones gone;
- afterwards the primary key finds id 1 once, through the index.

15/15 on each of PG15-19, and the twin on each. Ten checks are dated on
PG17 by two mutants: every scan returning deleted rows, and the index
build marking every row alive. cluster_tests 565 -> 566 by collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
pgc_conn is a write-watching wrapper, so setting .autocommit on it sets
an attribute on the wrapper and never reaches the connection (found
while writing #1419). The line here set it to the value it already had,
so it did nothing, but it read as if it mattered. The twin still passes
on PG17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
…are set (#1341 items 42, 57, 83)

docs/configuration.md is compared with pg_settings both ways by a new suite,
settings_reference, and its pytest twin:

- every setting the server defines is documented, except those it marks
  NOT_IN_SAMPLE (developer hooks and internal switches);
- every documented setting exists;
- each documented default is the server's boot_val;
- each row's condition (superuser, only at server start, Reloadable, or
  none) is the server's context.

Item 57: a role that is not a superuser, on its own login, is refused
enable_end_truncation with 42501, and a superuser sets it. Item 83:
compression_level reaches the zstd codec, so the same text stores smaller
at level 19 than at level 1.

8/8 on each of PG15-19, and the twin 4/4. All 8 checks are dated on PG17
by seven mutants (two GUC contexts, a default, try_zstd ignoring the
level, and three edits to the reference). cluster_tests 564 -> 568 by
collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
… string is piped into grep -q (#1341)

The harness selftest refused two lines. The thirty-settings premise
reduced the row count to 1 or 0, so a red said nothing about how many rows
were parsed (rule 540); it now reads 'no (N rows parsed from DOC)'. The
zstd test piped q into grep -q, an early-exit reader (rule 080); it now
asks the server whether zstd is among the enum values. The selftest was 2
failed of 1245 on PG17 and is now 0 failed; the suite is 8/8 on each of
PG15-19.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
… names only what differs (#1341)

comm compares whole lines, but the reference's rows were sorted on the name
field alone. In C order `autovacuum|` sorts after `autovacuum_compact_threshold|`,
so the default and condition arms fed comm unsorted input: a red arm named
ten entries for one wrong row, and every run logged comm's sort warnings.
Found by OffgridwithJD in review.

With "Reloadable" taken out of the pgcolumnar.autovacuum row, the condition
arm went from ten entries and six warnings to exactly
`pgcolumnar.autovacuum|sighup pgcolumnar.autovacuum|user` and none. A wrong
default on the same row names only that row. Green stays 8/8 on PG17 and
PG18, with no warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A columnar table has no TOAST table: the writer detoasts every value and
stores it inline. The largest value any test stored was 70 KB.

A new suite, large_values, and its pytest twin. Values go into heap and
are copied to a columnar table, and the two must agree by length and md5:
1 MB and two 16 MB incompressible values, a 200 MB compressible one, and
an UPDATE that takes a value heap has TOASTed. An opt-in arm
(PGC_RUN_OVER_1GB=1) checks that a chunk over 1 GB is refused with 54000,
commits nothing and leaves the server up; it needs several GB of memory,
so CI skips it. It passed when run by hand on PG17 (38 s). The twin does
not carry it, and test_compare_to_bash.py declares the pair incomplete.

5/5 plus the skip on each of PG15-19, and the twin on each. Three checks
are dated on PG17 by a mutant that stores a TOASTed value as its pointer.
Skipping only the write path's detoast reddened nothing, because the
encoder flattens again. cluster_tests 565 -> 566.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
@jdatcmd
jdatcmd force-pushed the test/1341-large-values branch from cba8315 to a22a224 Compare October 15, 2026 17:27
@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, restated as a22a224, on top of #1414 in my merge chain (#1412 → #1417 → #1418 → #1414 → #1425 → #1427). The changed lines are identical to cba8315, the head you approved, apart from re-derived bookkeeping: the budget paragraph, which matches the census derived from the tsv; cluster_tests, which matches collection; and the TESTS.md section number, now 117. The gate gives rc=0 and the guard set passes 435/435.

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

Re-approved at a22a224, stacked on #1414.

  • Same patch: this PR's own commits match what I approved at cba8315: the same code and the same ledger rows, with TESTS.md the same apart from section numbers.
  • Census: 2998 / 2281 / 717 from the tsv, matching the paragraph. suites_not_covered is 231.
  • CI at the time of review: 9 pass, 4 pending.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd merged commit 37ea557 into main Oct 15, 2026
15 checks passed
This was referenced Oct 15, 2026
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