Namespace the exported C symbols under pgcolumnar (#382) - #389
Conversation
jdatcmd
left a comment
There was a problem hiding this comment.
Requesting changes: this breaks every existing install, including plain SELECT
The rename itself is right and the reasoning is the best part of the PR. The GUC
backing variables are the real find: columnar_stripe_row_limit and friends binding
one library's setting to another's storage, silently, is worse than a duplicate
function and would have been miserable to diagnose. Checking the export surface per
major rather than assuming, and finding that PG15 exports 257 symbols where PG18
exports 63, is exactly the right instinct.
But renaming the C link names orphans every pg_proc row that already recorded the
old ones, and there is no upgrade path in this PR.
Measured, not argued
Installed the released build, created a table, then swapped in only the .so from
this branch, which is what a package upgrade does:
STEP 1: released build
rows: 1000
pg_proc recorded link name: columnar_vacuum
STEP 2: .so replaced with this branch
SELECT count(*) FROM t -> ERROR: could not find function "columnar_handler"
INSERT INTO t -> ERROR: could not find function "columnar_handler"
SELECT pgcolumnar.vacuum -> ERROR: could not find function "columnar_vacuum"
CREATE TABLE ... USING pgcolumnar -> ERROR: could not find function "columnar_handler"
It is not partial degradation. Reading an existing columnar table fails, and so
does creating a new one. The extension is inert until pg_proc is repaired.
And there is no repair available to a user: default_version is still 1.0-dev, so
ALTER EXTENSION pgcolumnar UPDATE has nothing to run. The only route back is
DROP EXTENSION ... CASCADE, which takes their tables with it.
This matters more than it would have yesterday: v1.0-alpha shipped a few hours ago
carrying these exact link names.
The fix, also measured
A standard upgrade script does work. On the broken install:
CREATE OR REPLACE FUNCTION pgcolumnar.columnar_handler(internal)
RETURNS table_am_handler LANGUAGE C AS '$libdir/pgcolumnar', 'pgcolumnar_handler';
-- scan after fix: 1000
CREATE OR REPLACE FUNCTION pgcolumnar.vacuum(tablename regclass, stripe_count int DEFAULT 0)
RETURNS void LANGUAGE C AS '$libdir/pgcolumnar', 'pgcolumnar_vacuum';
-- vacuum after fix: okCREATE OR REPLACE keeps the function OID, so the CREATE ACCESS METHOD binding and
every dependency survive; only prosrc moves. So what this PR needs is:
- bump
default_versioninpgcolumnar.control; - rename
pgcolumnar--1.0-dev.sqlto match; - add
pgcolumnar--1.0-dev--<new>.sqlcontainingCREATE OR REPLACE FUNCTIONfor
all 26 SQL-callable functions with their new link names.
I checked the 26 against the C side and they all resolve on this branch, so the
upgrade script is mechanical from the same list.
This is the consequence of the call I made cutting the release, where I left
default_version at 1.0-dev because no upgrade script existed. That was right then
and it is what makes the script mandatory now.
Everything else checks out
I verified the parts that fail at call time rather than build time, since CI cannot
see them:
- All 26 SQL link names have a matching
PG_FUNCTION_INFO_V1. No orphans in
either direction beyond the three_debug_functions, which tests bind themselves
viaAS 'pgcolumnar', 'pgcolumnar_debug_...'and which are correctly updated. PG_MODULE_MAGICand_PG_initare untouched, as they must be.- Both handlers bind correctly:
pgcolumnar_handlerand
pgcolumnar_parquet_fdw_handler. - The surviving
Columnar*strings are custom-scan node labels and filenames in
comments, not exported symbols.
Add the upgrade script and I will re-review and run the matrix against it.
I added a line saying default_version is still 1.0-dev and that extversion reports it. commandprompt#382's rename needs an upgrade script, so commandprompt#389 bumps default_version to 1.0-alpha and extversion then agrees with VERSION. Rather than write a sentence whose truth depends on which PR merges first, the README now states the version marker and stops there. That reads correctly before and after commandprompt#389. The default_version detail lives in the changelog and in docs/installation.md, where the upgrade step it belongs to is documented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Renaming the C link names orphans every pg_proc row that recorded the old ones. Replacing only the shared library, which is what a package upgrade does, leaves the extension inert rather than degraded: reading an existing columnar table fails with "could not find function columnar_handler", and so does creating a new one. jdatcmd measured this on commandprompt#389. There was also no way out. default_version was still 1.0-dev, so ALTER EXTENSION pgcolumnar UPDATE had nothing to run, and the only remaining route was DROP EXTENSION CASCADE, which takes the user's tables with it. v1.0-alpha shipped carrying the old link names, so this is a live install base, not a hypothetical. My error in the first version of this PR was reading "no upgrade scripts exist" as "every install is fresh". It means the opposite. With no upgrade script an existing install has no way to repair itself. So: default_version 1.0-dev -> 1.0-alpha pgcolumnar--1.0-dev.sql renamed to pgcolumnar--1.0-alpha.sql new pgcolumnar--1.0-dev--1.0-alpha.sql The upgrade script is 27 CREATE OR REPLACE FUNCTION declarations covering 26 distinct link symbols, vacuum_sorted having two overloads. CREATE OR REPLACE keeps each function's OID, so the CREATE ACCESS METHOD binding and every dependency survive and only prosrc moves. No signature, permission, catalog or on-disk format change. The declarations are lifted verbatim from the install script rather than retyped, so the two cannot drift, and the generator refuses to emit anything if a link name still lacks the new prefix. This also makes SELECT extversion report 1.0-alpha, matching VERSION. That contradicts a sentence in my open commandprompt#383, which I will update rather than leave wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bug jdatcmd caught on commandprompt#389 compiles, links, and passes every suite on a fresh install, while leaving every existing install inert. CI cannot see it because CI only ever creates the extension from scratch. test/extension_upgrade.sh installs a released build, creates a columnar table with rows in it, installs the tree under test over the top, runs ALTER EXTENSION UPDATE, and then requires that reads, writes, table creation and a maintenance function all still work. Two checks are deliberately general rather than about this rename: - if default_version moved and no matching upgrade script was installed, that is the failure, because an existing install then has no route forward that keeps its tables - every C function owned by the extension must have a prosrc that resolves in the namespace, which catches the next rename as well as this one The old build is made in a throwaway clone, so the caller's working tree is never checked out from under it. Not in run_all_versions.sh, for the same reason pg_upgrade.sh is not: the matrix builds one tree per invocation and this needs two at once. It is a second gate beside the matrix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You were right, and the correction is pushed. What I got wrong, preciselyI checked whether upgrade scripts existed, found none, and wrote in my own notes Your The fix, and the proofExactly the three steps you specified:
The upgrade script is 27 The declarations are lifted verbatim from the install script rather than retyped, so I reproduced your break before fixing it, because a repair that never saw the failure A gate for it, since CI cannot see this classYou noted you had to verify the call-time parts by hand. Two checks are deliberately general, so the next rename is caught too:
It sits beside Three bugs the new gate found in itselfWorth reporting, because the first one is the same failure shape as the bug we are fixing. It skipped and exited 0. Run against the broken commit to check it actually fails It picked a port inside the ephemeral range. It did not build clean. Run after a preflight that had built for PG19, Docs, which were worse than missing
The changelog header claimed Also fixed Gate
One dependencyThis makes |
Two extensions that both call themselves columnar can define the same symbol. We
share four with citus_columnar today: columnar_handler, columnar_relation_storageid,
and the pg_finfo_ twin of each. columnar_handler is the table access method handler,
which is the worst entry on that list to share a name for.
The exposure is larger on PostgreSQL 15 than the symbol count suggests. PGXS adds
-fvisibility=hidden from 16 onward and does not on 15, so 15 exports the internal API
as well:
major exported columnar_* Columnar* fb_*
15 257 52 145 24
16 63 27 0 0
17 63 27 0 0
18 63 27 0 0
Comparing our 15 exports against the full citus_columnar symbol table, which is the
overlap if neither side hides anything, finds twelve of ours rather than four. Four of
the additions are GUC backing variables: columnar_stripe_row_limit,
columnar_chunk_group_row_limit, columnar_compression and columnar_compression_level.
Citus uses those exact names for the same settings. A duplicate definition there does
not crash. It binds one library's setting to the other's storage, silently.
So this renames the identifiers, not just the four that collide today:
columnar_<lower> -> pgcolumnar_<lower>
Columnar<Upper> -> PgColumnar<Upper>
fb_<lower> -> pgc_fb_<lower>
fb_init, fb_start, fb_end and fb_grow are generic enough to collide with anything, and
they export on 15.
Nothing a user types or reads changes. The SQL names were already namespaced, so
columnar_vacuum only ever appeared as the link name:
CREATE FUNCTION pgcolumnar.vacuum(tablename regclass, ...)
AS 'MODULE_PATHNAME', 'columnar_vacuum';
Only that second argument moves. The GUC strings were already pgcolumnar.*, and only
the C variables behind them were not.
Deliberately unchanged, because both are user visible and belong in their own change:
the CustomScan node name, which makes EXPLAIN print a line identical to Citus's, and
the SQL name pgcolumnar.columnar_handler. Source file names are unchanged too, so
#include targets and the Makefile object list still read columnar_*.
The rename skips string and character literals. Three kinds of literal in src/ contain
these tokens and none of them should move: the #include targets, the CustomScan node
name, and the EXPLAIN keys such as "Columnar Chunk Groups Read".
Verified by rebuilding and reading nm -D rather than by reading the diff. On 15 and 18
the build is warning free and no symbol beginning columnar_, Columnar or fb_ is
exported. What remains outside our namespace is _PG_init and Pg_magic_func, which every
extension defines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Several suites assert a property by grepping src/ for a function name, so the rename moved the thing their patterns were looking for. Eight went red on 18 and 19: encode_invariants, native_fetch_position, native_gap, native_format, wal_envelope, decode_interrupts, native_fetch_interrupt and native_fetch_projection. The patterns are updated from a map derived from what actually changed in src/, rather than by rerunning the regex over test/. A regex cannot tell a renamed function from the two kinds of token that did not move: source file names such as columnar_reader.c, and the CustomScan node names ColumnarScan and ColumnarAgg. Both are asserted intact. test/pbt builds against our headers, so its stub and its test follow too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The type became PgColumnarSlot but its StaticAssertDecl message still said ColumnarSlot, so a failure would have named a type that no longer exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Renaming the C link names orphans every pg_proc row that recorded the old ones. Replacing only the shared library, which is what a package upgrade does, leaves the extension inert rather than degraded: reading an existing columnar table fails with "could not find function columnar_handler", and so does creating a new one. jdatcmd measured this on commandprompt#389. There was also no way out. default_version was still 1.0-dev, so ALTER EXTENSION pgcolumnar UPDATE had nothing to run, and the only remaining route was DROP EXTENSION CASCADE, which takes the user's tables with it. v1.0-alpha shipped carrying the old link names, so this is a live install base, not a hypothetical. My error in the first version of this PR was reading "no upgrade scripts exist" as "every install is fresh". It means the opposite. With no upgrade script an existing install has no way to repair itself. So: default_version 1.0-dev -> 1.0-alpha pgcolumnar--1.0-dev.sql renamed to pgcolumnar--1.0-alpha.sql new pgcolumnar--1.0-dev--1.0-alpha.sql The upgrade script is 27 CREATE OR REPLACE FUNCTION declarations covering 26 distinct link symbols, vacuum_sorted having two overloads. CREATE OR REPLACE keeps each function's OID, so the CREATE ACCESS METHOD binding and every dependency survive and only prosrc moves. No signature, permission, catalog or on-disk format change. The declarations are lifted verbatim from the install script rather than retyped, so the two cannot drift, and the generator refuses to emit anything if a link name still lacks the new prefix. This also makes SELECT extversion report 1.0-alpha, matching VERSION. That contradicts a sentence in my open commandprompt#383, which I will update rather than leave wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bug jdatcmd caught on commandprompt#389 compiles, links, and passes every suite on a fresh install, while leaving every existing install inert. CI cannot see it because CI only ever creates the extension from scratch. test/extension_upgrade.sh installs a released build, creates a columnar table with rows in it, installs the tree under test over the top, runs ALTER EXTENSION UPDATE, and then requires that reads, writes, table creation and a maintenance function all still work. Two checks are deliberately general rather than about this rename: - if default_version moved and no matching upgrade script was installed, that is the failure, because an existing install then has no route forward that keeps its tables - every C function owned by the extension must have a prosrc that resolves in the namespace, which catches the next rename as well as this one The old build is made in a throwaway clone, so the caller's working tree is never checked out from under it. Not in run_all_versions.sh, for the same reason pg_upgrade.sh is not: the matrix builds one tree per invocation and this needs two at once. It is a second gate beside the matrix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by running the gate against the broken commit to check it actually fails there. It did not. It skipped and exited 0, because the ref lookup ran from the wrong directory. A skip that exits 0 is the same shape as the bug this gate exists to catch: everything green, nothing checked. The gate is invoked deliberately, so a missing ref is a setup error worth stopping on, not a reason to pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rdcoding it The suite read pgcolumnar--1.0-dev.sql by name, so renaming the install script for the version bump broke it. It now reads default_version from the control file and asserts the file exists, which also survives the next bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docs. The Upgrade section told users to run make install and restart, which is
exactly the sequence that leaves them broken. It now has a third step, ALTER
EXTENSION pgcolumnar UPDATE, with a query to list the databases that need it,
the error they see if they miss it, and the fact that their data is untouched.
It also says plainly not to reach for DROP EXTENSION, which would take their
tables with it.
CHANGELOG. The header claimed default_version is pinned at 1.0-dev because no
upgrade script exists. That was true until this branch. Replaced, and an
Unreleased section records the rename and the upgrade step.
Harness. harness_selftest caught two real problems with the new gate:
- it was unregistered. Registered as an exemption beside pg_upgrade and
run_san, which are also second gates rather than matrix suites.
- it drew a port from inside the kernel's ephemeral range, so the kernel could
hand the same port to something else between the choice and the bind. It now
draws from the band portlib.sh carves below that floor, and probes it.
The second one is a real defect in my test, not a convention quibble, and it is
the kind that fails once a month with nothing to show for it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects in the gate, both found by running it after a preflight that had built the tree against another major. make was handed objects compiled for PG19 and relinked them into a .so for a PG18 server, which then refused to start. Both builds now clean first. The restart was unchecked, so a postmaster that never came back produced six connection errors instead of one statement of what went wrong. It now fails immediately and prints the server log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…iew) Reviewing jdatcmd's commandprompt#392 turned up a second copy of the instruction I rewrote in docs/installation.md, 85 lines below the paragraph that PR fixes. limitations.md said the pre-release ships no ALTER EXTENSION UPDATE scripts, which this branch makes false, and then told the reader they can replace the shared library and restart without one. That is precisely the sequence that leaves the extension inert. It then pointed at DROP EXTENSION, which takes the user's columnar tables with it. A user who hits the broken state and follows the status badge lands on that page, so it was the worst remaining copy. The paragraph now states that replacing the library is not sufficient on its own, gives the command, explains why an un-updated catalog fails and with which error, and says plainly that the data is untouched. The DROP EXTENSION paragraph is scoped to a build no upgrade script covers, and says it is not the remedy for this case. I missed this in commandprompt#383 and again when I rewrote installation.md. Sweeping for a second copy is the step I skipped both times. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eb03b41 to
6d817e6
Compare
jdatcmd
left a comment
There was a problem hiding this comment.
Approved. The break is fixed, and I verified the repair independently.
I re-ran my own upgrade test against this branch rather than reading your transcript.
Released build, table with 1,000 rows, install this branch over it, then
ALTER EXTENSION pgcolumnar UPDATE:
1. released build: extversion=1.0-dev rows=1000 link=columnar_vacuum
2. this branch in: SELECT count(*) FROM t -> could not find function "columnar_handler"
3. ALTER EXTENSION pgcolumnar UPDATE -> clean
extversion now=1.0-alpha link now=pgcolumnar_vacuum
4. PASS read pre-existing table (1000) PASS insert (1001)
PASS update + delete (1000) PASS maintenance function
PASS access method still bound PASS create a NEW columnar table
PASS index scan on the old table PASS parquet export
5. fresh install of this branch: extversion 1.0-alpha, works
The one red in my run was my error, not yours: I called
pgcolumnar.relation_storageid, which is the C symbol. The SQL function is
pgcolumnar.get_storage_id, and it was never named the other thing.
The check that settles it beyond spot-calls:
SELECT count(*) FROM pg_proc
WHERE pronamespace='pgcolumnar'::regnamespace AND prolang=(c)
AND prosrc NOT LIKE 'pgcolumnar\_%'; -- 0Zero rows left on an old link name after the upgrade. And statically, the upgrade
script's 26 symbols are exactly the install script's 26, with nothing in install that
is missing from upgrade.
Full matrix PG18 + PG19: ALL VERSIONS PASSED, 112 suites each, on a 2,650-line
rename. test/extension_upgrade.sh passes standalone.
The correction you wrote is the useful part
No upgrade script does not mean every install is fresh. It means an existing install
has no way to repair itself.
That is the reasoning error worth keeping in the record, and reproducing the break
before repairing it is why the fix can be trusted.
Two follow-ups, neither blocking
1. Nothing runs the new guard. extension_upgrade is in not_a_suite, which is
correct since it needs two builds. But pg_upgrade.sh is also in that list and is
explicitly invoked by run_all_versions.sh:505 -- that invocation is what closed
#257, whose title is "so #256's coverage doesn't rot". extension_upgrade.sh has the
exemption without the invocation, so it will never run unless somebody remembers to
type it. That is the same gap #257 existed to close, one suite over. I will file it.
2. It cannot run in the documented container loop. The dev loop copies the tree
without .git, and the suite needs tags:
FAIL v1.0-alpha is not present. Fetch tags, or pass an explicit ref
I had to copy .git into the container to exercise it. Worth either falling back to a
non-git source for the old build, or saying in the header that it needs a real
checkout.
Merging.
The suite landed in #389 with pg_upgrade's exemption from the registration check and without pg_upgrade's invocation, so nothing ran it. Not the matrix, not CI, not the Makefile. It ran only when a human typed its name. That is the gap #257 existed to close, and it is worse here than there. The break this suite catches is invisible to a build and to every suite that creates the extension from scratch, so a guard nobody runs leaves exactly the failure it was written for undetected. It is invoked now from run_all_versions.sh under PGC_RUN_UPGRADE=1, the same switch and the same terms as pg_upgrade. One major is enough, so it runs once against the first config rather than per pair. Asking for the gate and getting nothing is a failure rather than a quiet pass, matching the pair check above it. Second problem from the same report: it could not run in the documented container loop at all, because that loop copies the tree without .git and the ref form needs tags. The second argument now takes either a git ref or a path to an already-checked-out old source tree. A tree that is not a git checkout now says so, and says what to pass instead, rather than failing on a missing tag. docs/testing.md gains a section for it, since the cross-major upgrade had one and this is a different upgrade with a different failure.
Closes #382.
Nothing a user types or reads changes. The SQL names were already namespaced, so the old
names only ever appeared as C link names.
Scoping changed the shape of this twice
The export surface is not the same on every major. PGXS adds
-fvisibility=hiddenfrom 16 onward and does not on 15, which I checked rather than assumed:
columnar_*Columnar*fb_*The 27 I first reported was the PG18 figure. On 15 the whole internal API is exported.
The latent collision is 12, not 4. Citus on the bench is a PG18 build, so it hides
its internals too and only 4 overlap today. Comparing our PG15 exports against Citus's
full symbol table, which is the overlap when neither side hides, finds:
The right column is the one I had not considered. Those are GUC backing variables, and
Citus uses the identical names for the identical settings. That case does not crash. It
binds one library's setting to the other's storage, silently, which is worse than a
duplicate function.
What changed
fb_init,fb_start,fb_endandfb_groware generic enough to collide with anything,and they export on 15.
The extension script changes only the link name:
Deliberately not in this PR
Both are user visible and belong in their own change, as I asked on the issue:
CustomScanMethodsname, which makesEXPLAINprintCustom Scan (ColumnarScan),byte identical to Citus's
pgcolumnar.columnar_handlerSource file names are also unchanged, so
#includetargets and the Makefile object liststill read
columnar_*.The rename skips string literals, on purpose
Three kinds of literal in
src/contain these tokens and none of them should move: the#includetargets, the node names, and the EXPLAIN keys such as"Columnar Chunk Groups Read". All three are asserted intact rather than eyeballed.The same care applies to
test/. Several suites assert a property by grepping the Csource, so they name C functions in their patterns, and eight went red on the first gate
run. Their patterns are updated from a map derived from what actually changed in
src/, not from rerunning the regex overtest/, because a regex cannot tell a renamedfunction from a source file name like
columnar_reader.c.Verification
Read from
nm -Don a real build, not from the diff.No symbol beginning
columnar_,Columnarorfb_is exported on any major. Whatremains outside our namespace is
_PG_initandPg_magic_func, which every extensiondefines.
Full suites on the assert builds: PG18 and PG19, ALL VERSIONS PASSED, 116 suites each.
The tree that was gated is the tree in this PR, checked by comparing tree hashes.
The diff is 2649 insertions against 2649 deletions, symmetric, as a pure rename has to be.
One thing worth doing separately
Adding
-fvisibility=hiddento the PG15 build removes 194 exported symbols at a strokeand makes 15 behave like 16 and up. This PR is the namespace fix. That is the structural
one, and the two are independent.