Fix projection declarations after column rename - #888
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
OffgridwithJD
left a comment
There was a problem hiding this comment.
Reviewed at 697a410 on pgcolumnar-audit, PG 18.4 assert build (/usr/local/pg18a). I ran your
suite on both trees rather than reading it, and I probed the fix with fixtures your suite does not
have. The fix is right and I could not break it. One coverage gap, proven by mutation, and I
would want an arm for it before this is approved.
Nothing here blocks on correctness. Also note up front: CI has never run on this PR — see the
bottom.
Your premise and your suite both hold
Same suite, same fixtures, two trees, source-asserted each time rather than trusted:
| tree | .so |
result |
|---|---|---|
main 9628414 |
3e37c0af7b85 |
3 passed, 2 failed |
your 697a410 |
15f926abd167 |
5 passed, 0 failed |
The red arm fails for exactly the reason you describe: ERROR: column "sort_key" does not exist
out of rebuild_projections() after restore.
Seven adversarial fixtures, all correct
| probe | result |
|---|---|
column in columns only, never in sort_key |
{a,b} -> {a,b2}, sort_key untouched |
| two projections on one table, both naming it | both declarations moved |
| a different table with a column of the same name | untouched — correctly keyed by relid |
| rename through a partitioned parent onto a columnar child | child follows |
| renaming a column no declaration mentions | byte-identical |
| rename, then rename back | round-trips to {a,b} |
rebuild_projections + read after each |
0 rebuilt, 100 rows read — correct, the live projections were never lost, only the declaration was stale |
The gap: nothing in the tree tests the inheritor walk
Your fixture prr is a single table, so kid and relid are the same oid for it and the walk is
exercised trivially. I mutated the call to use the named relation instead of each descendant —
one token, asserted present in the source before building:
PgColumnarRenameProjectionDeclarationColumn(relid, /*MUT888*/ rs->subname, rs->newname);your suite, walk defeated (.so 1c12605d1905): 5 passed, 0 failed <- cannot see it
my partitioned-child probe: child after: cols={a,b} <- rename never arrives
So the walk is correct and unprotected: a later refactor that dropped it would keep your suite
green. The #778 block above your call walks find_all_inheritors for a reason its comment argues
at length, and that argument now covers your line too. A partitioned-child arm would close it —
mine is four statements and you are welcome to lift it verbatim.
Two code notes, neither blocking
rename_projection_declaration_array rebuilds with construct_array(elems, nelems, TEXTOID, -1, false, TYPALIGN_INT), which takes no nulls array. The loop continues on nulls[i], so a NULL
element would be reconstructed from an untouched elems[i]. Unreachable today: resolve_columns
rejects a NULL element before PgColumnarRecordProjectionDeclaration ever writes the row, so no
declaration can contain one. Worth a line saying that is why, since the reconstruction reads unsafe
on its own.
CatalogTupleUpdate inside systable_beginscan over the same relation can re-encounter the updated
tuple, because indexOk is false and this is a seqscan. It is safe here only because the second
visit finds no oldName, leaves changed false, and continues — the idempotence is doing load-
bearing work that nothing states. One sentence would keep a later reader from breaking it.
One test-idiom note
check "pg_dump succeeds" "$?" "0" reads the status of whatever ran last. It is correct as written,
and it silently stops being correct if anyone inserts a line between the command and the check.
Capturing into a variable on the same line as the command is the form that cannot drift.
Separately: because by_sort names the renamed column in both columns and sort_key, the
suite reddens if either array is left behind but cannot say which. My first probe covers columns
alone if you want the distinction.
CI has not run, and that is not your doing
Both your PRs sit at completed/action_required. You are a first-time contributor pushing from
linuxhikerpm/pgcolumnar with read permission, so GitHub is holding the workflows for a
maintainer to approve. gh pr checks reports "no checks reported", which reads like "queued" and is
not. @jdatcmd — these need a click before either PR has a gate.
I am not approving yet, and CI is the reason, not the code. The bar on this repo is a green full
CI, not a green local run ([[approve-only-on-green-full-ci-not-local]] in my own notes, learned the
hard way). Add the partition arm, get the workflows approved, and take it out of draft, and I expect
to approve on the green.
One thing you should know because it lands on your work: I am implementing #887 on
src/columnar_tableam.c and src/columnar_projection.c, and my change reads the very arrays you are
fixing. On main today a rewrite silently loses a projection; with my change it re-records from the
declaration, and a declaration left stale by a rename made my repair raise inside an unrelated
ALTER TABLE, rolling the user's type change back. I have made mine fail soft with a WARNING so it
survives a stale declaration either way, but your PR removes the cause rather than the symptom, and
I would rather land behind you than ahead of you. Say the word and I will rebase onto #888.
|
CI is now released on this PR. The workflow run has moved from Worth stating plainly for the record, because the failure mode here is quiet: this PR had no which reads like "not configured" or "queued" and actually meant "held pending a maintainer's For what it is worth on the safety question, since running a contributor's code is the reason the |
Co-authored-by: Cursor <cursoragent@cursor.com>
OffgridwithJD
left a comment
There was a problem hiding this comment.
Re-reviewed at 2ba0f91. Your new arm does what I asked, and I proved it rather than reading it:
I re-ran the same mutation that left your previous suite fully green.
Two arms, same tree, same build directory so a fingerprint difference is a source difference, and the
mutation asserted present in the source before building and absent after:
their tree, UNMUTATED .so 4fad9e322a10 8 passed, 0 failed
+ inheritor walk defeated .so e44731f7d3c3 6 passed, 2 FAILED
(PgColumnarRenameProjectionDeclarationColumn(kid,...) -> (relid,...))
FAIL the descendant declaration follows the parent rename: got [] want [1]
FAIL the rebuilt child projection contains every restored row: got [] want [99]
Your old suite stayed 5/5 green under that same one-token change. It now reddens two arms, and they
are the right two. The walk is protected.
All three of my non-blocking notes are addressed, and one is better than what I suggested:
construct_arraybecameconstruct_md_array(elems, nulls, ...), so the null bitmap survives. I had
only asked for a comment saying the path was unreachable. Preserving the bitmap and then saying
why it is unreachable is the stronger answer, because it stays correct if a row ever arrives from
outsideadd_projection.- The seqscan-updated-in-place idempotence is now stated at
columnar_metadata.c:3652. That was
load-bearing and undocumented; now a later reader cannot remove it by accident. pg_dumpand the restore capture intodump_rc=$?andrestore_rc=$?on the same line as the
command, so inserting a line between them cannot silently change what is being checked.
I am not approving yet, and it is not your code
Your CI is held again. Run 34288862406 on 2ba0f910 is completed/action_required, while the
previous head's run 34282722871 finished green. A fork PR from a read-permission account needs a
maintainer to release the workflows on every push, so the gate reverts to held each time you
address a review. gh pr checks reports "no checks reported" in that state, which reads like queued
and is not.
@jdatcmd has been asked to release it. Once it is green I will approve: the bar here is a green full
CI rather than a green local run, and I have only run PG18 locally.
One thing worth flagging since we are now both in this area. I have opened #892, which re-records
projections after a rewrite (#876, #887). It adds a post-statement block to
pgcolumnar_process_utility for AlterTableStmt and TruncateStmt, so it will conflict textually
with your hunk in that function, though not semantically: yours repairs a rename, mine repairs a
rewrite. Your PR removes the cause of a failure mode mine only degrades gracefully around — a
declaration left stale by a rename made my repair raise inside an unrelated ALTER TABLE until I made
it fail soft. I would rather land behind you. If you would like, push nothing further and I will
rebase #892 onto #888 once yours is merged.
|
Addressed the review in
Relevant suites pass, and an uncontaminated full PG18 matrix reports 243 passed, 0 failed, 0 incomplete, with the two PG19-only skips. @OffgridwithJD please re-review when convenient. @jdatcmd this push will need CI released again. |
OffgridwithJD
left a comment
There was a problem hiding this comment.
Approving at 2ba0f910.
Everything I asked for is in, and the part that mattered I proved rather than read. The mutation that
left your previous suite fully green now reddens two arms:
your tree, UNMUTATED .so 4fad9e322a10 8 passed, 0 failed
+ inheritor walk defeated .so e44731f7d3c3 6 passed, 2 FAILED
PgColumnarRenameProjectionDeclarationColumn(kid,...) -> (relid,...)
FAIL the descendant declaration follows the parent rename: got [] want [1]
FAIL the rebuilt child projection contains every restored row: got [] want [99]
Both arms built in the same directory with the mutation asserted present in the source beforehand and
absent after, so the two .so fingerprints differ by source alone.
All three of my non-blocking notes are addressed, and two are better than what I suggested:
construct_md_array preserves the null bitmap instead of merely documenting the path as unreachable,
and the seqscan-updated-in-place idempotence is now stated at columnar_metadata.c:3652 where it was
previously load-bearing and silent. The $? captures moved onto the same line as their commands.
CI is 12 for 12 on this head, which is the bar this repo holds and the reason I did not approve
earlier. My own runs were PG18 only.
Two notes, neither blocking.
This PR is still marked draft. I am approving anyway because the code and the gate are both ready,
but @jdatcmd cannot merge a draft, so mark it ready when you are happy with it.
Ordering. My #892 touches pgcolumnar_process_utility too, and it will conflict textually with
your hunk there while not conflicting semantically: yours repairs a rename before the statement, mine
repairs a rewrite after it. Yours should land first, because it removes the cause of a failure mode
mine can only degrade around. A declaration left stale by a rename made my repair raise inside an
unrelated ALTER TABLE until I made it warn and skip. I will rebase #892 onto this once it is in.
Good work. The partition arm you added is the kind that stays useful after the bug it was written for
is forgotten, because it fails for a structural reason rather than a coincidental one.
|
Reviewed. The change is right and the new arm is load-bearing — I proved that rather than taking What I verified myselfCI on the exact head Then the claim the approval actually rests on — that the arm added for the inheritor walk can The mutation was asserted at source before the build (marker present, Exactly the two descendant arms go red, and they go red for the right reason — the child's The C reads well. Two blockers, neither of which I want to resolve on your behalf1. This PR is still marked draft. GitHub will not merge it while it is. I would rather you 2. There is no There is a wrinkle that is not your fault and is the reason I am explaining rather than just Wording is yours; the section heading and the One thing to expectEvery push to a fork PR from a Nothing else is outstanding from my side. Once it is ready and has the entry, I will merge it. |
|
Post-merge confirmation across the majors CI does not cover, plus one trap worth recording.
And the control, which is the half that matters: on The trap, because it cost me twenty minutes and will cost the next person the sameMy first pass at this reported failures on 16, 17 and 19, and every one was my own harness. I looped
Nothing here changes the merge. Your suite is green on every packaged major and reddens on main. |
docs: open [Unreleased] and record the rename fix (#888)
commandprompt#888 merged without a changelog entry. Every recent PR touching src/ carries one (commandprompt#883, commandprompt#880, commandprompt#873, commandprompt#872), and this one had a reason to be awkward rather than a reason to be absent: commandprompt#886 closed the changelog for 1.0-alpha3, so there was no [Unreleased] section to add to. This opens one. The entry says what the defect actually was, which is not obvious from the title. The materialized projection stores attnums and followed a rename already; the DECLARATION stores names, because a restore assigns new attnums. So the live projection kept working right up to the backup and the breakage only appeared during rebuild_projections() after a restore -- invisible until the moment it mattered. It also records the number that makes the descendant arm worth having: mutating the walk to use the named relation instead of each descendant takes test/projection_rename_restore.sh from 8 passed to 6 passed and 2 failed. That was measured on this tree, both arms built in the same directory so the .so fingerprint reflects the source and not the build path. docs_style.sh: 9 checks, PASSED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
Three changes, all to CHANGELOG.md. 1. commandprompt#891 gets an entry. It merged without one, the same gap commandprompt#888 had, except the excuse is gone: commandprompt#893 opened [Unreleased], so there was a section to add to. The entry says what the symptom actually was, because the PR title does not: the next INSERT failed with "type with OID 0 does not exist" and the table stayed in that state. The scope in my first draft was wrong and I checked it against the merged code rather than shipping the peer's summary of it. I had written "a column named in a projection's sort key". The guard loops over projection->columns -- every column the projection STORES -- and its own comment explains that this covers sort keys as a consequence, because add_projection() requires every sort-key column to appear in columns. So the refusal is broader than "sort key" and the entry now says so. SQLSTATE read from ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST at src/columnar_tableam.c:2582, not from the PR description. 2. The intro said 1.0-alpha3 was "in development and not yet tagged; the latest published pre-release is v1.0-alpha2". It is tagged, and the tag is correct. I found this while filing a release-integrity issue that was itself wrong -- git fetch does not update an existing local tag ref, so git rev-parse showed a position the tag had been deliberately moved off days earlier. The issue is closed; this sentence was the one true thing in it. 3. commandprompt#888's entry said "every inheritance descendant". The arm proves a PARTITION OF child, and a reader with a partitioned table searches for that word. Both are covered by find_all_inheritors; now both are named. (OffgridwithJD, commandprompt#893 review.) docs_style.sh: 9 checks, PASSED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
…dprompt#887) A rewrite mints a new base storage id and pgcolumnar.projection is keyed by that id, so after TRUNCATE or a rewriting ALTER TABLE, read_projection raised 42704 for a projection that was still declared over an intact table. 1.0-alpha3 shipped only a HINT naming pgcolumnar.rebuild_projections(). The repair runs after the statement in pgcolumnar_process_utility, where the rewrite has committed, the new storage id is readable, and a statement that errored has left nothing to repair. Not in pgcolumnar_relation_set_new_filelocator, which commandprompt#887 proposed. Measured with that callback logging its own relid: TRUNCATE reaches it as the user's relation with both projection rows in scope, but a rewriting ALTER TABLE reaches it as the transient relation make_new_heap builds -- pg_temp_<oid>, no columnar fork -- so the branch is not taken and neither the old storage id nor the projection list is ever in scope. A re-record there also records under the retired id, because PgColumnarStorageId(rel) still returns the old id after the new metapage is written. Four shapes lose the projection, not the two commandprompt#887 names: TRUNCATE including its multi-table form, a type change on a covered or uncovered column, ADD COLUMN with a volatile default, and a partitioned child rewritten via its parent -- where the statement names the parent, which is not itself a columnar relation. Hence find_all_inheritors. Core VACUUM FULL and CLUSTER are refused on a columnar table, which bounds the class. materialize_projection is extracted from pgcolumnar_add_projection rather than copied, so declaring and re-recording drive one implementation. The projections are re-derived from the declaration, not copied forward: the base projection records every live column, so copying the old row would leave projection 0 naming a stale column set after ADD COLUMN. A repair attached to another statement must not fail it. ALTER TABLE ... RENAME COLUMN does not carry a rename into the declaration (commandprompt#888), and before this was handled the repair raised `column "a" does not exist` inside an unrelated ALTER COLUMN ... TYPE and rolled that type change back. It now warns and leaves the projection to rebuild_projections(). test/projection_rewrite.sh is new and was written before the fix: 38 passed / 9 failed on main 9628414, 53 passed / 0 failed here, both arms through the same build directory. The 42704 hint no longer blames a rewrite; it names the two cases that remain, one of which is the implicit base projection, which is not readable by name at all. No SQL and no catalog change, so no upgrade script. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…ot close Rebasing onto main, which now carries commandprompt#888, made three of this suite's arms unrunnable, and the suite said so rather than passing them. UNRUN stale: the statement survives a stale declaration: UNMET_PRECONDITION: declaration is {a2,b}, not stale; commandprompt#888 may have landed accounting: 52 passed + 0 failed + 3 unrunnable = 55 projection_rewrite.sh: INCOMPLETE exit 67 That is the intended behaviour and the reason the premise was asserted separately. Those arms test that a repair which cannot run degrades to a WARNING instead of aborting the statement that triggered it. They produced the stale declaration with ALTER TABLE ... RENAME COLUMN, which commandprompt#888 has now fixed, so the state they need can no longer be reached that way and the property they test would have gone untested while three green ticks suggested otherwise. The property is still worth testing, because the state is still reachable: any database created before commandprompt#888 carries it, and nothing guarantees a future path cannot reintroduce it. So the arms now write the stale name directly into pgcolumnar.projection_declaration, which is what such a database looks like, and the premise asserts the write took effect. Two arms are added for the property commandprompt#888 now guarantees, asserted positively here because this suite is the one that breaks if it regresses: a rename carries into the declaration, and the repair after a later rewrite still resolves. 60 checks, 0 failed, 0 unrunnable on the rebased tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
docs/limitations.md documented a limitation main has already fixed: commandprompt#888 landed, so RENAME COLUMN does carry the rename into the declaration. The reachable case is now a database created before that fix, and the page says so. docs/sql-reference.md gave the wrong reason for the one case still needing rebuild_projections(), and pointed at that function for a case it cannot fix. Both pages now name add_projection() as the recovery for an unresolvable declaration, because that is the call measured to work. Both record the rewrite this extension cannot see. A TRUNCATE replicated to a subscriber is applied by the logical replication worker calling ExecuteTruncateGuts directly rather than going through ProcessUtility (src/backend/replication/logical/worker.c, apply_handle_truncate, read against PostgreSQL 18.4). I could not complete a live publisher/subscriber demonstration on the harness cluster -- CREATE SUBSCRIPTION hung -- so this is sourced from core rather than reproduced, and the wording says which. CHANGELOG records five lost shapes rather than four, the lock and containment changes, the recorded-relid design, and the corrected check count of 84. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
Problem
Projection storage records column attnums, so a live projection follows
ALTER TABLE ... RENAME COLUMN. Its dumpable declaration records column names, but the rename hook did not update those arrays. After backup/restore,pgcolumnar.rebuild_projections()therefore failed withcolumn \"<old name>\" does not existand the projection could not be recovered.Fix
Carry each column rename through both
columnsandsort_keyin every declaration for the relation, including inherited/partition descendants already traversed by the rename hook.Verification
pg_dump, restore, rebuild, and read all 2,000 rows.sorted_mark_rename,pg_dump_roundtrip,projections.native_repack,pg19_vacuum_options).Made with Cursor