Skip to content

Install count_nulls without superuser, and prove it in the suite - #62

Open
jnasbyupgrade wants to merge 10 commits into
Postgres-Extensions:masterfrom
jnasbyupgrade:non-superuser-install
Open

Install count_nulls without superuser, and prove it in the suite#62
jnasbyupgrade wants to merge 10 commits into
Postgres-Extensions:masterfrom
jnasbyupgrade:non-superuser-install

Conversation

@jnasbyupgrade

@jnasbyupgrade jnasbyupgrade commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

count_nulls is nothing but SQL functions, so requiring a superuser to CREATE EXTENSION it was never anything more than the control file's default. Set superuser = false.

For that to stay true, the suite now runs as an ordinary role throughout — both the install session and every test session. test/helpers/test_user.sql creates "Test user for count_nulls" if it isn't there, grants it exactly two privileges (CREATE on the database, and USAGE on schema tap, since pgTAP is harness and pgxntool's setup.sql creates that schema as the connecting role, which grants nobody else access), and raises if the role is a superuser or a member of any managed-cloud equivalent — rds_superuser, cloudsqlsuperuser or azure_pg_admin, none of which carry rolsuper. Reverting superuser = false now fails the suite outright, with "permission denied to create extension".

Every decision is made server-side, by a pg_temp function taking the role name and returning the role the session should run as, because psql can't branch before 10: \if is psql 10 and CI covers back to 9.4, where psql reports it as an invalid command and then runs the branch it should have skipped. \gset feeds that returned name into a plain SET ROLE. The file opens with RESET ROLE, so what it does depends only on how the session connected.

One case deliberately doesn't switch: a count_nulls already installed and owned by somebody else, which is what a real pg_upgrade leaves behind. PostgreSQL has no ALTER EXTENSION ... OWNER TO, so pg_dump can't carry an extension's ownership across — --binary-upgrade emits binary_upgrade_create_empty_extension(), which takes no owner, and the extension ends up belonging to whoever ran the restore. Its member functions keep their owner; only the extension object itself is out of reach, which is exactly what test__shutdown__drop_all needs. That's an acknowledged upstream shortcoming (BUG #18625), not something a newer PostgreSQL will retire, so the branch is commented as permanent. Nothing is lost by staying put: existing mode installs nothing, so it was never the leg proving the install works unprivileged.

Also fixes a live bug in test/install/load.sql's existing-mode assertion, whose nested $$-quoted RAISE message closed its own DO block's body, leaving the remainder to be parsed as bare SQL — the whole assertion was a no-op. Fixed with a distinct tag. It stays silent for now because that file has no ON_ERROR_STOP; turning that on needs its \if branching to go server-side first, which is #63.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 0aa3a53f-e149-4f79-bc24-ea8eab1cb672

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jnasbyupgrade
jnasbyupgrade force-pushed the non-superuser-install branch from 91b89a6 to 0110c07 Compare August 29, 2026 23:22
count_nulls is nothing but SQL functions, so requiring a superuser to
CREATE EXTENSION it was never anything more than the control file's
default. Set superuser = false.

For that to stay true, the suite now runs as an ordinary role throughout -
both the install session and every test session. test/helpers/test_user.sql
creates "Test user for count_nulls" if it isn't there, grants it exactly two
privileges (CREATE on the database, and USAGE on schema tap, since pgTAP is
harness and pgxntool's setup.sql creates that schema as the connecting role,
which grants nobody else access), and raises if the role is a superuser or a
member of any managed-cloud equivalent - rds_superuser, cloudsqlsuperuser or
azure_pg_admin, none of which carry rolsuper. Reverting superuser = false
now fails the suite outright, with "permission denied to create extension".

Every decision is made server-side, by a pg_temp function taking the role
name and returning the role the session should run as, because psql can't
branch before 10: \if is psql 10 and CI covers back to 9.4, where psql
reports it as an invalid command and then runs the branch it should have
skipped. \gset feeds that returned name into a plain SET ROLE. The file
opens with RESET ROLE so a second \i in one session behaves like the first -
not hypothetical, since on psql older than 10 install/load.sql's mode
branches all run and this file gets reached twice.

One case deliberately doesn't switch: a count_nulls already installed and
owned by somebody else, which is what a real pg_upgrade leaves behind.
PostgreSQL has no ALTER EXTENSION ... OWNER TO, so pg_dump can't carry an
extension's ownership across - --binary-upgrade emits
binary_upgrade_create_empty_extension(), which takes no owner, and the
extension ends up belonging to whoever ran the restore. Member functions
keep their owner; only the extension object is out of reach, which is
exactly what test__shutdown__drop_all needs. Nothing is lost by staying put:
existing mode installs nothing, so it was never the leg proving the install
works unprivileged.

Also fixes a live bug in install/load.sql's existing-mode assertion, whose
nested $$-quoted RAISE message closed its own DO block's body, leaving the
rest to be parsed as bare SQL - the whole assertion was a no-op. Fixed with
a distinct tag. It stays silent for now because that file has no
ON_ERROR_STOP; turning that on needs its \if branching to go server-side
first, which is a separate change.
@jnasbyupgrade
jnasbyupgrade force-pushed the non-superuser-install branch from 0110c07 to a730479 Compare August 29, 2026 23:30
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 2s —— View job


I'll analyze this and get back to you.

…rtcoming

The carve-off for an extension owned by somebody else reads like something
that ought to be fixable, or like something a newer PostgreSQL would make
unnecessary. It isn't either: extowner has had no matching ALTER EXTENSION
... OWNER TO since it was added in 2011, because what that should do to the
contained objects was never settled, and it's been reported (BUG #18625) and
acknowledged as a known shortcoming without being fixed.
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

Claude encountered an error after 2s —— View job


I'll analyze this and get back to you.

It's declared CONSTANT, so it takes the constant prefix, matching
c_managed_superuser_roles right above it. v_role stays as it is - that
one really is a loop variable.
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 5m 30s —— View job


Claude finished the code review

Todo list

  • Gather context (PR body, changed files)
  • Read repo CLAUDE.md conventions
  • Review diff (git diff origin/master...HEAD)
  • Post review feedback

Summary

This is a well-executed, thoroughly-documented change. I traced the core logic in test/helpers/test_user.sql end-to-end against every call site (test/deps.sql, test/helpers/create_test_schema.sql, bin/test_existing's prepare-old) and the interacting mode branches in test/install/load.sql, and didn't find a correctness bug:

  • The RESET ROLE + pg_temp function + \gset + SET ROLE sequence correctly handles the double-\i case caused by pre-10 psql running both \if branches.
  • The extension-ownership early-return (existing/pg_upgrade mode) correctly leaves the session as current_user post-RESET ROLE, which is a superuser for every entry point that reaches it, matching what test__shutdown__drop_all needs to actually drop a superuser-owned extension.
  • I checked for a CREATE ROLE/GRANT race across parallel test/sql/*.sql sessions (pg_regress's REGRESS files run as one parallel batch): pgxntool's schedule-based install files run to completion before that parallel batch starts (per the comment in pgxntool/base.mk), so the role/grants are already in place by the time concurrent sessions re-run this same code path. The repeated GRANT calls in each parallel session are redundant but idempotent — not a bug.
  • CI is green on this PR, including the bin/test_existing prepare-old/pg_upgrade/pg_tle legs that exercise the ownership-mismatch branch, which corroborates the analysis above.
  • The test/install/load.sql dollar-quoting fix ($msg$...$msg$ instead of a bare $$...$$ nested inside the outer DO $$...$$) is correct and matches the described bug exactly.
  • Docs (HISTORY.md, README.md, test/README.md) are accurate and consistent with the code, including the "any role with CREATE on the target schema" claim — that's specifically about CREATE EXTENSION itself, distinct from the CREATE ON DATABASE grant the test harness separately needs just to create its own scratch schema.

One documentation gap worth considering (non-blocking)

test/helpers/test_user.sql's bootstrap block only auto-creates the role and grants when current_setting('is_superuser') = 'on' (lines 86–108). On managed-cloud platforms, the admin role used to connect (e.g. an RDS rds_superuser member) is exactly the kind of role this same function's later check treats as not a real superuser (is_superuser reports 'off' for it too, same as for the test role). That means on RDS/Cloud SQL/Azure, the auto-provisioning step won't fire even for a legitimate admin connection — the role and its two grants would need to be created manually before running the suite there.

This is likely intentional/out of scope (the PR is about proving superuser = false works, not about managed-cloud bootstrap), but test/README.md's new "Running as a non-superuser" section doesn't currently mention this asymmetry, and a reader could reasonably assume the managed-cloud role list is only relevant to disqualifying the test role, not to the connecting role's ability to bootstrap it. A one-line note there would close the gap.

test/README.md had grown into a paraphrase of the comments in the files it
describes, so it's now a layout list plus the two cross-cutting facts a
reader needs before touching anything - the non-superuser role and the
randomly named schema - each pointing at the file that explains itself.

test/deps.sql said it was intentionally empty, which stopped being true when
it took over switching to the test user, and spent a paragraph restating
pgxntool's own docs. It now points at them instead.

Records why the database grant can't move into the CREATE ROLE beside it:
the role is cluster-wide and outlives a run, but the grant lives in the
current database's ACL, so a run against a new database has to reissue it.

Corrects the MEMBER-vs-USAGE note, which claimed being able to SET ROLE to a
role is equivalent to holding its privileges. It isn't; the check is
deliberately over-strict, and stays that way because nothing needs looser.
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 11m 10s —— View job


Code review posted

Ran a code review (/code-review) on this PR at master...HEAD and posted 3 inline findings, most severe first:

  1. test/helpers/create_test_schema.sql:46 (correctness) — The role switch to the non-superuser test user now happens before the pre-existing stale-schema cleanup loop (DROP SCHEMA %I CASCADE for leftovers matching count_nulls test schema %). This only works if every leftover schema was created by that same role. A schema left behind by a run predating this change (created as superuser) or by any other role can no longer be dropped by the non-superuser test user — turning a self-healing cleanup into a hard must be owner of schema failure that aborts the whole run.

  2. test/helpers/test_user.sql:86 (correctness/design gap) — The CREATE ROLE/GRANT bootstrap for the test role is gated solely by current_setting('is_superuser') = 'on'. The function explicitly names AWS RDS/Aurora, GCP Cloud SQL, and Azure Flexible Server admin roles — but those accounts never report is_superuser = on by design, so on a fresh database on any of those platforms the bootstrap can never run, even though the admin genuinely has enough privilege via rds_superuser/etc. membership. The managed-role list is only used later as a negative check, never to widen the bootstrap gate. (This echoes the gap flagged in an earlier automated review comment on this PR.)

  3. test/helpers/test_user.sql:100 (test-coverage gap) — The test role is granted CREATE ON DATABASE, but the PR's documented claim (HISTORY.md/README.md/count_nulls.control) is narrower: "any role with CREATE on the target schema." Since the test schema is created by, and owned by, this same database-CREATE-privileged role, the suite never actually isolates/proves the narrower schema-only claim it documents.

I also checked and ruled out several other candidates (transaction/rollback interaction with SET ROLE, trusted vs superuser control-file semantics, ACL-diff false positives in bin/compare_fresh_vs_update, and a suspected dollar-quote nesting bug in test/install/load.sql — that one is already correctly fixed by this diff, not a live bug).

Comment thread test/helpers/use_test_user.sql Outdated
RETURN current_user;
END IF;

IF current_setting('is_superuser') = 'on' THEN

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness/design gap: bootstrap never runs on the managed-cloud platforms this code explicitly targets.

The CREATE ROLE / GRANT bootstrap is gated only by current_setting('is_superuser') = 'on'. But c_managed_superuser_roles (lines 51-53) exists specifically because AWS RDS/Aurora, GCP Cloud SQL, and Azure Flexible Server admin accounts are members of rds_superuser/cloudsqlsuperuser/azure_pg_admin while deliberately never carrying rolsuper — which is exactly what makes is_superuser report off for them.

That means on a fresh database on any of these three platforms (which the comments show were deliberately considered), the bootstrap block can never execute, and the very first run fails at:

IF NOT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = p_test_user) THEN
  RAISE EXCEPTION
    'role "%" does not exist, and this session is not a superuser so it cannot be created'
    , p_test_user
  ;
END IF;

...even when the connecting admin genuinely holds enough privilege (via rds_superuser membership, etc.) to create the role and issue the grants. The managed-role list is only consulted later (lines 123-141) as a negative check ("test user must not be a member of these"), never as a positive check to widen the bootstrap gate — e.g. current_setting('is_superuser') = 'on' OR pg_has_role(current_user, ANY(c_managed_superuser_roles), 'MEMBER').

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude here — fixed. The bootstrap gate now treats the managed-cloud admin roles as equivalent to superuser instead of consulting is_superuser alone, so the same role list drives both the bootstrap and the later disqualification check. Verifying this surfaced a second half of the same problem: an admin that isn't a real superuser can't SET ROLE to the role it just created, because from PG16 a CREATEROLE creator gets ADMIN on that role rather than SET — so it now grants the role to itself in that case, which a real superuser doesn't need. Verified by simulating a NOSUPERUSER role that is a member of rds_superuser. Landed in commit 6529b72.

Comment thread test/helpers/use_test_user.sql
Comment thread test/helpers/create_test_schema.sql Outdated
* not just before CREATE EXTENSION: a leftover schema is one this same role
* created on a previous run, so it's the role that must be able to drop it.
*/
\i test/helpers/test_user.sql

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: stale-schema cleanup a few lines below can no longer drop schemas it doesn't own.

This \i test/helpers/test_user.sql now switches the session to the non-superuser test role before the cleanup loop further down in this file runs, per the added header comment above ("the switch has to come before the cleanup loop ... it's the role that must be able to drop it"). That reasoning only holds if every leftover count_nulls test schema % was created by that same role.

If a schema matching the prefix was left behind by a run predating this change (when the whole file still ran as a superuser), or by any session that isn't the test user (e.g. a manual superuser session, or a differently-named test role after a future rename), the cleanup loop's EXECUTE format('DROP SCHEMA %I CASCADE', r.nspname) will now fail with "must be owner of schema ..." instead of self-healing — aborting the entire test run instead of quietly cleaning up, exactly the crash-resilience case that cleanup was written to handle.

Failure scenario: a developer with a persistent local Postgres instance who ran the suite before this branch (leaving a schema owned by the superuser), then checks out this branch and reruns make test — the very first run fails on that DROP instead of proceeding.

@jnasbyupgrade jnasbyupgrade Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude here — fixed. The stale-schema cleanup now runs before the role switch, so it executes as the connecting role rather than the test user. A leftover schema can belong to anyone (e.g. a superuser, if it's left over from a run predating this change), and only the connecting role is sure to be able to drop it. Landed in commit 6529b72.

test/helpers/test_user.sql becomes use_test_user.sql. The other helpers are
named for the action running them performs (create_test_schema,
find_test_schema); this one performed an action but read as a noun.

Drops the comment above the SET ROLE, which restated what the two statements
under it plainly do, and the one explaining why a nested RAISE message needs
its own dollar-quote tag - that's general plpgsql knowledge, not something
this site needs to teach.
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 3m 14s —— View job


Working on it

Todo list

The bootstrap that creates the test user and grants it what it needs was
gated on is_superuser alone. On RDS, Cloud SQL and Azure Flexible Server the
admin account never reports that, by design, so on those platforms the
bootstrap could never run even though the account has the rights - the
managed roles were only ever consulted afterwards, to disqualify the test
user. Both checks now consult the same list.

An admin that isn't a real superuser also can't SET ROLE to the role it just
created: from PG16 a CREATEROLE creator gets ADMIN on that role, which isn't
SET. It now grants the role to itself in that case, which a real superuser
doesn't need.

Stale-schema cleanup moves ahead of the role switch, so it runs as the
connecting role. A leftover schema can belong to anyone - one created by a
run predating this change belongs to a superuser - and only the connecting
role is sure to be able to drop it.
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 8m 39s —— View job


Working on it

Todo list

Doubled single quotes ('' ... '') in RAISE EXCEPTION messages made two
call sites hard to read; switch both to distinct-tag dollar quoting so
the literal quotes read as themselves.

create_test_schema.sql's header comment had grown to explain nearly
everything the file does, most of it far from the code it was
describing. Cut it to a few lines on what the file is and why it
exists standalone (bin/test_existing's prepare-old runs it outside any
psql session that also runs load.sql), and move each remaining
rationale down next to the code it explains: the GUC-bridging and
'current' sentinel notes by the guard, the crash-recovery note by the
cleanup loop, the identifier-quoting note by the name generation, and
the WITH SCHEMA note by CREATE EXTENSION.
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 2m 34s —— View job


Code review posted

Todo list

I re-reviewed the full diff against origin/master, with particular attention to the two most recent commits (6529b72, 922c349), which fix the two correctness findings from the prior automated review round (stale-schema cleanup ordering in create_test_schema.sql, and the managed-cloud bootstrap gate in use_test_user.sql). Both fixes check out:

  • test/helpers/create_test_schema.sql: the stale-schema cleanup loop now runs before \i test/helpers/use_test_user.sql, so it executes as the connecting role rather than the test user — correctly handles a schema left behind by any role, not just the current test user.
  • test/helpers/use_test_user.sql: the bootstrap gate is now c_admin (superuser OR member of rds_superuser/cloudsqlsuperuser/azure_pg_admin), so it fires for managed-cloud admins too. The added self-GRANT %I TO %I correctly covers the PG16+ case where a CREATEROLE creator gets ADMIN but not SET on a role it just created — gated on is_superuser = 'off' so real superusers (who can already SET ROLE to anything) skip it.
  • The third finding (test role holds database-level CREATE, so the suite doesn't isolate the narrower schema-level claim) was correctly left unfixed per the maintainer's own follow-up comment — a reasonable scope call, not something worth re-raising.

New finding

count_nulls.control:7 (nit, stale reference) — the comment explaining superuser = false says:

# (test/helpers/test_user.sql), which fails outright if this reverts to the

but the file was renamed to test/helpers/use_test_user.sql in commit 8b32f15 ("name the helper for what running it does"), after this comment was written in a730479. It's the only remaining reference to the old name anywhere in the tree — every other doc/comment (test/README.md, test/deps.sql, test/helpers/create_test_schema.sql) already uses the new name.

Fix this →

No other issues found. Docs (HISTORY.md, README.md, test/README.md) are consistent with the code, and I didn't find any new correctness or coverage issues in the diff beyond what's already been discussed on this PR.
· Branch

count_nulls_prepare_test_user() now takes the load mode as an explicit
argument (the caller must supply it - the GUC it comes from isn't set for
every invocation, e.g. bin/test_existing's prepare-old) and only skips the
role switch for a foreign-owned extension when that mode is 'existing'. In
fresh/update, a foreign-owned count_nulls is a cleanup miss, not a
preserved pg_upgrade artifact, so it now raises instead of silently letting
the suite run as the connecting role.
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 3m 25s —— View job


Code review posted

Todo list

This synchronize added one new commit since the last review round: b5659f4 ("Scope the extension-owner short-circuit to existing mode only"), which threads an explicit p_load_mode argument through count_nulls_prepare_test_user() and now RAISE EXCEPTIONs for a foreign-owned count_nulls in fresh/update mode instead of silently treating it like a preserved pg_upgrade artifact.

I traced every call site of use_test_user.sql (test/deps.sql, test/helpers/create_test_schema.sql, bin/test_existing's prepare-old via create_test_schema.sql) and the Makefile's PGOPTIONS export of count_nulls.test_load_mode, and the change checks out:

  • test/deps.sql correctly reads the mode from the GUC the Makefile exports unconditionally (export PGOPTIONS := ... -c count_nulls.test_load_mode=$(TEST_LOAD_SOURCE)), matching the new no-missing_ok comment's claim.
  • create_test_schema.sql hardcodes 'fresh' for count_nulls_load_mode rather than reading the GUC — correct per its own header, since it's only ever included in fresh/update mode (test/install/load.sql's existing branch never calls it) or from prepare-old, which is also a from-scratch install.
  • The new RAISE EXCEPTION only fires when an extension exists, is foreign-owned, and mode isn't 'existing' — i.e. exactly the "cleanup miss" case the commit message describes, not the legitimate pg_upgrade case (still handled by the existing-mode RETURN current_user).
  • Docs (test/README.md's "why it deliberately does not switch" note) remain accurate — that description is specifically about the pg_upgrade/existing-mode case, which is unchanged.

New findings

  1. count_nulls.control:7 (nit, still open) — the comment still says test/helpers/test_user.sql, but the file was renamed to test/helpers/use_test_user.sql back in commit 8b32f15. This was flagged in the last review round and hasn't been addressed yet — it's the only remaining reference to the old filename in the tree.

  2. test/helpers/create_test_schema.sql:48 (nit) — the literal 'fresh' set here is also used for update-mode runs (test/install/load.sql's update branch sets version '0.9.6' and \is this same file). Harmless for the role-switch logic itself (only 'existing' vs. everything else matters there), but if the new foreign-owned-extension error in use_test_user.sql:115-118 ever fires during an actual update run, its message will claim load mode "fresh" even though TEST_LOAD_SOURCE=update was what ran — a minor debugging-clarity nit, not a correctness bug.

No other issues found. The role-switch and mode-scoping logic is sound, and CI's pg-upgrade-test/pg-tle-test legs (which exercise the existing-mode branch this change carves out) should still validate the untouched path.
· Branch

Comment thread count_nulls.control Outdated
relocatable = false
# Pure SQL functions, nothing privileged - anyone with CREATE on the target
# schema can install it. Enforced by the suite running as a non-superuser
# (test/helpers/test_user.sql), which fails outright if this reverts to the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still stale: this comment references test/helpers/test_user.sql, but the file was renamed to test/helpers/use_test_user.sql in commit 8b32f15. Flagged in an earlier review round on this PR but not yet fixed — it's the only remaining reference to the old name anywhere in the tree.

Fix this →

Comment thread test/helpers/create_test_schema.sql Outdated
* Never reached in existing mode - see this file's own header - so a
* literal is enough; there's no GUC to lose by not reading it.
*/
\set count_nulls_load_mode 'fresh'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this literal is also used for the 'update' mode invocation (test/install/load.sql's \if :count_nulls_update_mode branch, which sets :version '0.9.6' and then \is this file) — not just 'fresh'. It's harmless for the actual role-switch logic (use_test_user.sql only distinguishes 'existing' from everything else), but if the new foreign-owned-extension RAISE EXCEPTION in use_test_user.sql:115-118 ever fires during an actual update run, the error text will claim load mode "fresh" expects no pre-existing installation even though TEST_LOAD_SOURCE=update was what ran — mildly misleading for anyone debugging that failure. Might be worth a value like 'not-existing' (or similar) to avoid implying a specific mode that isn't necessarily the one that ran.

comment, and trim an unenforceable ownership claim

The load-mode literal comment asserted an unenforced fact about the
rest of the system instead of stating the intent of this code path;
restate it as intent, noting the loud-failure fallback if that intent
ever breaks. The WITH SCHEMA comment had ended up above an unrelated
CASE expression when the header was split up; move it back next to the
CREATE EXTENSION statement it documents. The extension-owner
short-circuit's comment assumed a foreign-owned extension always came
from a real pg_upgrade restore, which the code doesn't and can't know;
trim it to the two facts the code actually depends on plus the one
non-obvious reason (no ALTER EXTENSION ... OWNER TO) existing mode can
meet a foreign owner at all.
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 1m 46s —— View job


Code review posted

Todo list

This synchronize added one new commit since the last review round: af48b23 ("Address reviewer comments: reframe intent, relocate a misplaced comment, and trim an unenforceable ownership claim"), which is itself a documentation/comment-only response to the previous round's findings.

I re-traced the changes against their call sites:

  • test/helpers/create_test_schema.sql: the count_nulls_load_mode comment now states intent ("this file exists to install count_nulls, so a foreign-owned extension here is always an error") with a note that use_test_user.sql raises loudly if that stops being true — accurate, since the literal 'fresh' really is passed unconditionally for both fresh and update invocations from this file. The WITH SCHEMA comment move puts it back next to the CREATE EXTENSION statement it documents, no longer floating above the unrelated version_clause CASE.
  • test/helpers/use_test_user.sql: the extension-owner short-circuit's comment now states only what the code depends on (existing mode tolerates a foreign owner; other modes error) plus the one non-obvious fact (no ALTER EXTENSION ... OWNER TO, so pg_upgrade can't preserve ownership) rather than assuming every foreign-owned extension came from a real pg_upgrade — correctly hedged, since the code can't actually distinguish that from a cleanup miss.
  • Confirmed bin/test_existing's prepare-old still works with the two-arg count_nulls_prepare_test_user(): it calls create_test_schema.sql directly, which self-sets count_nulls_load_mode = 'fresh', so it never depends on the GUC.
  • Docs (HISTORY.md, README.md, test/README.md) remain accurate and consistent with the code.

Finding (posted inline)

count_nulls.control:7 (nit, unaddressed for a third round) — the comment still reads test/helpers/test_user.sql, but the file was renamed to test/helpers/use_test_user.sql in commit 8b32f15. This was flagged in the previous two review rounds and is the only remaining reference to the old filename anywhere in the tree. Posted as an inline suggestion this time.

No other issues found. The role-switch, extension-ownership, and load-mode logic all check out against every call site.
· Branch

Comment thread count_nulls.control Outdated
relocatable = false
# Pure SQL functions, nothing privileged - anyone with CREATE on the target
# schema can install it. Enforced by the suite running as a non-superuser
# (test/helpers/test_user.sql), which fails outright if this reverts to the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still stale: this comment references test/helpers/test_user.sql, but the file was renamed to test/helpers/use_test_user.sql back in commit 8b32f15. This is the third review round flagging it — it's the only remaining reference to the old filename anywhere in the tree.

Suggested change
# (test/helpers/test_user.sql), which fails outright if this reverts to the
# (test/helpers/use_test_user.sql), which fails outright if this reverts to the

…load mode

count_nulls.control referenced test/helpers/test_user.sql, which was renamed
to use_test_user.sql; the tree-wide grep for that rename had been scoped to
subdirectories and missed it.

create_test_schema.sql hardcoded \set count_nulls_load_mode 'fresh' right
before including use_test_user.sql, which both invented a mode the caller
never asked for and would silently override one a caller had legitimately
set. It's already wrong in practice: test/install/load.sql's update leg runs
through this file with the mode hardcoded to 'fresh' even though it's really
'update'. The value now comes from whichever caller genuinely knows it -
test/install/load.sql (derived from count_nulls.test_load_mode, the same way
test/deps.sql already does) or bin/test_existing's -v on the command line
(always a genuine fresh install) - and create_test_schema.sql only consumes
:count_nulls_load_mode, same as every other includer of use_test_user.sql.
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 1m 17s —— View job


Code review posted

Todo list

This synchronize added one new commit since the last review round: 06bce4e ("Fix stale control-file filename, and make the caller supply the test load mode"). It addresses the two outstanding nits from prior rounds:

  • count_nulls.control:7 now correctly reads test/helpers/use_test_user.sql — the stale filename reference (flagged three rounds running) is fixed. Confirmed it's the last reference to the old name anywhere in the tree.
  • test/helpers/create_test_schema.sql no longer hardcodes \set count_nulls_load_mode 'fresh' right before including use_test_user.sql. The mode now has to arrive from the caller instead:
    • test/install/load.sql:27 renames its \gset target from count_nulls_test_load_mode to count_nulls_load_mode, so the real mode (fresh/update/existing) threads all the way through instead of being overwritten — this also fixes the debugging-clarity nit from the previous round, where an update-mode error would have misreported load mode "fresh".
    • bin/test_existing:190-193's create_extension_in_schema now passes -v count_nulls_load_mode=fresh explicitly on the command line, since prepare-old invokes create_test_schema.sql directly without going through test/install/load.sql or the count_nulls.test_load_mode GUC.
    • test/helpers/use_test_user.sql:37-41's comment now correctly explains why p_load_mode must be passed in rather than read from the GUC directly (the GUC is only exported via PGOPTIONS for pg_regress sessions; prepare-old bypasses that path entirely).

I traced every caller of count_nulls_load_mode (test/deps.sql, test/install/load.sql, create_test_schema.sql, bin/test_existing) and confirmed the naming is now consistent tree-wide, with no leftover references to the old count_nulls_test_load_mode name. test/README.md's doc references to use_test_user.sql were already correct and remain so.

No new issues found. All findings from the previous review rounds — stale filename, managed-cloud bootstrap gate, stale-schema cleanup ordering, and now the load-mode threading nit — are resolved. The one remaining open item (test role holding database-level CREATE rather than the narrower schema-level claim the docs describe) was explicitly deferred by the maintainer as a follow-up, which is a reasonable scope call.
· Branch

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.

1 participant