Repository navigation
fix: the format stamp is a range, and the first write raises it (#1137) - #1289
Conversation
…andprompt#1137) commandprompt#1130 stopped writing a validity bitmap for a chunk with no nulls, and an alpha4 binary misreads such a chunk rather than refusing it. The owner chose option 2: stamp new tables 2, and turn the equality check into a range so this build still reads 1. COLUMNAR_NATIVE_FORMAT_VERSION 2 written COLUMNAR_NATIVE_FORMAT_MIN_READABLE 1 oldest read Measured with the real v1.0-alpha4 .so on PG 17, one seq scan and 40 single-row index fetches per table (custom scan off): written by alpha4 seq alpha4 fetches main before this (stamp 1) reads 0 right, 9 NULL, 32 errors this change (stamp 2) refused 0A000 41 refused main, appended to by this refused 0A000 41 refused THE THIRD ROW IS A HOLE OPTION 2 AS WRITTEN LEFT OPEN. Existing tables were to keep stamp 1, but this build writes bitmap-less chunks when it APPENDS to one. So PgColumnarInsertNativeStorageRow, which every flush calls, raises an older stamp on the first write. A read never moves it. The bulk loader's unlocked path does not raise: it falls through to the locked path, because two unserialized loaders would race on the UPDATE. The fetch path's liveness-only exemption from the check is now an Assert at the decode point. With that path's check disabled, a correctly stamped table trips it; without the Assert that mutation passed every arm. native_format.sh gains eight arms over four decode entry points. Each of four mutations is caught by the arm aimed at it: equality restored, raise removed, build still writing 1, lower bound dropped. The fetch arm turns the custom scan off; with it on, that path's check could be deleted and every arm stayed green (measured). test_native_format.py ports the new arms and is declared INCOMPLETE for the rest. native_format 28 passed, PG 15..19 pg17 catalog_scan_index native_writer parallel_copy concurrent_diff extension_upgrade docs_style: pass pytest guard half 404; cluster twins 50 harness_selftest 1213 passed Spec section 13, docs/limitations.md and docs/installation.md describe the range. Neither suite has ledger rows, so the ledger is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Upgrade path, at this head ( That covers |
jdatcmd
left a comment
There was a problem hiding this comment.
Approving c71e7f7. I will merge once CI is green on this head; 14 of 15 checks were done when I looked.
What I checked, and how
The raise reaches every chunk writer, projections included. That was the case I most wanted measured, not argued. A covering projection has its own storage row, and an alpha5 append that left that row at 1 would reopen the silent NULL on the projection's scan. PG17, with a real v1.0-alpha4 build, then this head's build, then alpha4's .so swapped back in:
| step | storage rows |
|---|---|
alpha4: tp with a covering projection, tr Z-order clustered |
tp base = v1, tp projection = v1, tr = v1 |
this build, after ALTER EXTENSION UPDATE, before any write |
all still v1, so a read never moves the stamp |
after an INSERT into tp, and an INSERT plus an online recluster of tr |
all three v2, the projection's row included |
Back on alpha4's .so, every read I tried is refused:
projection-served query on tp Relation "tp" was written with native format version 2; this build supports version 1.
seq scan of tp (same refusal)
seq scan of tr Relation "tr" was written with native format version 2; this build supports version 1.
One caveat on that table: EXPLAIN showed a PgColumnarScan, which does not say whether the projection or the base served the query. Both storage rows are at 2, so the conclusion holds either way.
Every chunk write passes the raise. pgcolumnar_flush_row_group calls PgColumnarInsertNativeStorageRow unconditionally for each row group, and PgColumnarEnsureStorageRow (the parallel_copy coordinator) calls it too. Those are the only two callers. That is why recluster and the projection writes are covered without naming them.
Your "append never raises" mutation reproduces here, PG17, my own worktree, if (raise) changed to if (false && raise) in storage_row_probe:
control native_format 28 passed + 0 failed
mutant native_format 27 passed + 1 failed
FAIL appending to a table stamped 1 raises its stamp: got [1] want [2]
On the question you asked to have attacked
The fetch memo's "immutable" becoming "only rises, within the readable range" holds. The memo caches a pass. A raise moves a value that passed (1) to another value that passes (2), so a cached pass is never wrong for this build. The memo could only be wrong if a stamp fell or left the range, and nothing writes either.
The concurrency argument also holds as written. The locked path updates under the advisory lock, which is held to transaction end. The new-in-transaction path is invisible to other backends. The unlocked loader path declines to raise, and an older row falls through to the lock. The raise is a CatalogTupleUpdate on our own catalog, so it adds no WAL record type.
Not blocking
docs/limitations.mdanddocs/installation.mdnow describe the refusal. When this lands I will update #1288's notes with your Q1 answers and your suggested WARNING text.- Your finding that the old fetch arm never reached
pgcolumnar_fetch_rowwith custom scan on is the most useful thing in this PR after the fix itself. Worth checking whether other suites' "fetch" arms have the same blind spot. I will add it to #1236's follow-up list.
🤖 Generated with Claude Code
Filled from #1289 and OffgridwithJD's answers on #1288, and from a run of #1289's build against a real alpha4 build on PG17: - this release writes format version 2 and reads 1 and 2; anything else is refused with 0A000 before a byte is read; - the first write raises a table from 1 to 2, a read never does (measured: stamps stayed 1 through ALTER EXTENSION UPDATE and reads, and became 2 after an INSERT, including a covering projection's own storage row, and after an online recluster); - alpha4 refuses a version 2 table on every path rather than misread it. The upgrade WARNING is OffgridwithJD's text: plan a rollback before the first write, because alpha4 refuses what alpha5 has written to. Only the release date is left as TODO(release). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filled from #1289 and OffgridwithJD's answers on #1288, and from a run of #1289's build against a real alpha4 build on PG17: - this release writes format version 2 and reads 1 and 2; anything else is refused with 0A000 before a byte is read; - the first write raises a table from 1 to 2, a read never does (measured: stamps stayed 1 through ALTER EXTENSION UPDATE and reads, and became 2 after an INSERT, including a covering projection's own storage row, and after an online recluster); - alpha4 refuses a version 2 table on every path rather than misread it. The upgrade WARNING is OffgridwithJD's text: plan a rollback before the first write, because alpha4 refuses what alpha5 has written to. Only the release date is left as TODO(release). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filled from #1289 and OffgridwithJD's answers on #1288, and from a run of #1289's build against a real alpha4 build on PG17: - this release writes format version 2 and reads 1 and 2; anything else is refused with 0A000 before a byte is read; - the first write raises a table from 1 to 2, a read never does (measured: stamps stayed 1 through ALTER EXTENSION UPDATE and reads, and became 2 after an INSERT, including a covering projection's own storage row, and after an online recluster); - alpha4 refuses a version 2 table on every path rather than misread it. The upgrade WARNING is OffgridwithJD's text: plan a rollback before the first write, because alpha4 refuses what alpha5 has written to. Only the release date is left as TODO(release). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filled from #1289 and OffgridwithJD's answers on #1288, and from a run of #1289's build against a real alpha4 build on PG17: - this release writes format version 2 and reads 1 and 2; anything else is refused with 0A000 before a byte is read; - the first write raises a table from 1 to 2, a read never does (measured: stamps stayed 1 through ALTER EXTENSION UPDATE and reads, and became 2 after an INSERT, including a covering projection's own storage row, and after an online recluster); - alpha4 refuses a version 2 table on every path rather than misread it. The upgrade WARNING is OffgridwithJD's text: plan a rollback before the first write, because alpha4 refuses what alpha5 has written to. Only the release date is left as TODO(release). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes #1137. @jdatcmd for review; this lands only on your approval. It fills the placeholder in your alpha5 release notes.
The decision, and what it does
The owner chose option 2 today. New tables are stamped
format_version = 2, and the read check becomes a range:The headline, on the real alpha4 binary
PG 17. I built
v1.0-alpha4's.soand swapped it in under one cluster after three builds had written tables. Each table got one sequential scan and 40 single-row index fetches with custom scan off, which is #1137's own fixture: wide text,compression = 'none'.0A0000A000A hole in option 2 as written: the third row
The issue says existing tables keep stamp 1. But this build writes bitmap-less chunks when it appends to such a table, which leaves a table stamped 1 holding chunks alpha4 misreads, and alpha4 accepts the stamp. So the first write by this build raises an older stamp to 2, in
PgColumnarInsertNativeStorageRow, which every flush already calls.concurrent_diffandparallel_copypass.parallel_copycoordinator'sPgColumnarEnsureStorageRowtakes the locked path first, so in practice loaders find the row already raised.What I'd most like attacked: this changes a value the fetch memo called "immutable for a storage". It now only rises, and only within the readable range, so a check that passed stays valid for its command. I've rewritten that comment to say so.
The
wantValuesexemption is now held by codeWe both asked for this on #1137. There's an
Assert(columnarFetchFmtOk && columnarFetchFmtOkStorageId == storageId)at the decode point, after the liveness-only early return.Removal proof: with the fetch path's stamp check disabled (
if (false && wantValues && ...)), a correctly stamped table trips it:Without the Assert, that same mutation passed every arm.
A finding about the fetch arm itself
The existing
native_format.shfetch arm never reachedpgcolumnar_fetch_row. With custom scan on, pgcolumnar's own node serves the index query. I deleted that path's stamp check outright and all 28 arms stayed green. The new arms setpgcolumnar.enable_custom_scan = off, which is what #1137's original reproduction did.Arms and mutations
native_format.shgains eight arms over all four decode entry points: seq scan, aggregate scan, group-aggregate scan, and row fetch.a table stamped 1 is read on all four paths(0A000 x4),and returns its rowsappending to a table stamped 1 raises its stamp(got 1)native format_version is 2, the stamped-by-this-build premise, the raise arma stamp below MIN_READABLE is refused on all four paths(ok x4)test_native_format.pyports the new arms and the version pin. It is declared INCOMPLETE intest_compare_to_bash.py, with the reason (the metapage hook and the heap-mirror round trip are #432's work). Its own removal proof: the raise removed reddens the append arm.Runs, on the rebased head (
c71e7f7, on951b3ec)Before the rebase, on PG 17, all of these also passed:
concurrency,differential(204),parallel_copy_dedup,native_upgrade_converge,native_backend_crash,recovery,rewrite_storage_oidandrls_direct_storage.Not code:
test_build_refusalfails when the guard half runs as root in a worktree owned by another user, because git refuses the "dubious ownership". Unmodified main fails the same way under root and passes as the owner.Rebase note
The rebase conflicted with #1207 in the three
storageexistence checks. Each now uses #1207'spgcolumnar_scan_index_oidand the probe. My TESTS.md section is renumbered 84.cluster_testsis 490 by collection: git merged both branches'489without a conflict, which is the README's collision exactly.Docs
Spec §13 now describes the range and when the stamp moves.
docs/limitations.mdsays the format changed once (it said it had not) and that downgrading after a newer build has written is refused, not misread.docs/installation.mdsays the same. Neither suite has ledger rows, so the ledger is untouched.🤖 Generated with Claude Code