Skip to content

Dropped owner roles: reassign_owned, drop_owned, orphans, on_drop_owned (M10 step 6) - #57

Merged
wfaithfull merged 6 commits into
mainfrom
m10-dropped-roles
Oct 23, 2026
Merged

wfaithfull merged 6 commits into
mainfrom
m10-dropped-roles

Conversation

@wfaithfull

Copy link
Copy Markdown
Contributor

This is M10 step 6 (DESIGN.md §5.7, "Dropped owner roles"). It implements §14 items 44a and 45b, as the owner decided them.

New functions

  • warren.reassign_owned(old, new) gives old's graphs to new and makes new the creator of old's vectors. It returns the number of graphs moved.
    • It follows set_owner's rules, but opens no source queries, as REASSIGN OWNED checks none.
    • The caller needs the privileges of old and must be able to become new.
  • warren.drop_owned(role) drops the role's graphs, deletes the vectors it wrote on other graphs, and removes it from every readers list.
  • warren.on_drop_owned() is the function for an optional ddl_command_start event trigger, which a superuser creates. The extension script never creates the trigger; docs/semantics.md documents how to opt in.
    • It refuses a direct call with 39P03.
    • It reads the roles from the parse tree.
    • It refuses with 2BP01 while a named role owns graphs, naming the graphs and the two functions (item 44a).
    • Otherwise it removes the role's reader entries and vectors, writing as the catalog owner.
  • warren.orphans() lists graphs, reader entries and vectors whose role was dropped. warren.drop_orphans() removes them.
  • Item 45b: members of the extension's owner may drop orphaned graphs; only a superuser may adopt one, through set_owner.

Deviations from §5.7 (both recorded in DESIGN.md)

  1. A direct call of on_drop_owned gets 39P03 (event_trigger_protocol_violated), which is what Postgres's own event-trigger functions raise, not 39P01.
  2. The trigger skips roles whose privileges the caller lacks. Postgres checks DROP OWNED's privileges only after ddl_command_start, so without this an unprivileged DROP OWNED BY victim would get a 2BP01 listing the victim's graph names. With it, DROP OWNED raises its own 42501.

Tests

  • pgrx tests on PG 16: 670 passed, with 20+ new ones seen failing first.
  • test/run.py: drop_owned, trusted_install, set_owner_race, search_path_notice and lock_reread_race, in normal and --trusted modes.
  • test/dump.py with the trigger present: a superuser's restore recreates it; the installer's restore fails only at the trigger's statements.
  • Static checks: test/upgrade.py passes, schema-drift.sh reports no drift, and a cargo check on PG 15 is clean.

The quota rows are step 7: each cleanup has a comment marking where they go.


Generated by Claude Code

…e sweep

DESIGN.md §5.7, §14 items 44 (a, refuse) and 45 (b), work breakdown step 6.

- warren.reassign_owned(old, new) and warren.drop_owned(role): SQL
  counterparts of REASSIGN OWNED and DROP OWNED for graphs, their vectors
  and reader entries, needing the privileges of the role
  (has_privs_of_role) as the commands do.
- warren.on_drop_owned(): an event-trigger function over the raw fcinfo,
  for a superuser's CREATE EVENT TRIGGER ... WHEN TAG IN ('DROP OWNED');
  the extension never creates the trigger. It refuses a direct call
  (39P03), reads the roles from the DropOwnedStmt (get_rolespec_oid),
  skips those that do not exist or whose privileges the caller lacks, and
  refuses with 2BP01 while one owns graphs, naming them and both
  functions; otherwise it removes the roles' reader entries and vectors as
  the catalog owner.
- warren.orphans() and warren.drop_orphans(): the sweep after a bare
  DROP ROLE.
- Item 45b: members of the extension's owner may drop orphaned graphs
  (drop_graph, drop_orphans); only a superuser may adopt one with
  set_owner, which refused it before.
- Quota rows (step 7) get a marked seam where they will be deleted.

Every new function is in parallel_labels and the guard's CALLS, and in
sql/pg_warren--0.6.0.sql and the 0.5.0 -> 0.6.0 upgrade script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
- test/sql/drop_owned.sql: the standard procedure without the trigger
  orphans graphs, which orphans() and drop_orphans() sweep; with it,
  DROP OWNED is refused while the role owns graphs, and cleans up after
  reassign_owned or for a role that owns none; DROP EXTENSION needs
  CASCADE.
- trusted_install.sql: non-superusers reassign and drop a leaving role's
  graphs; the installer drops an orphan but may not adopt one, and cannot
  create the trigger, whose presence makes its DROP EXTENSION need
  CASCADE.
- test/dump.py: both dumps carry the trigger; the superuser's restore
  recreates it, and the installer's restore fails at its two statements
  and nowhere else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
docs/semantics.md gains "Dropping a role": the procedure in each database
before DROP ROLE, the opt-in event trigger, the sweep and who may act on
an orphan. DESIGN.md §5.7 records step 6 as done with an Implemented
note (including the 39P03 for a direct call, where the text said 39P01,
and the trigger leaving roles the caller lacks the privileges of to DROP
OWNED); §7 and the API listing follow. CHANGELOG 0.6.0, README and
docs/limits.md too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
# Conflicts:
#	DESIGN.md
- drop_orphans locks each pass's candidate rows FOR UPDATE, then deletes
  or updates them by key in a statement of its own: EvalPlanQual
  re-checked "no such role" under the first snapshot, so a graph adopted
  for, or a reader granted to, a role created by the transaction it waited
  for was dropped or pruned.
- The deletes that follow a creator check (drop_vector, replacement,
  compact's move) delete by id and the checked creator (vector::Checked),
  so one queued behind a reassign_owned fails with 40001 instead of
  deleting the new creator's vector.
- Dropping orphans needs what DROP EXTENSION needs of the extension's
  owner: on PG 16+ its privileges or SET ROLE to it, not a bare
  membership (WITH INHERIT FALSE, SET FALSE); membership on PG 15.
- drop_owned and reassign_owned refuse the bootstrap superuser (2BP01),
  as the commands do; the trigger takes each named role once; the
  get_rolespec_oid SAFETY comment says PUBLIC would raise.
- test/sql/dropped_roles_race.sql queues reassign_owned, drop_owned,
  drop_orphans and drop_vector behind uncommitted set_owner, refresh,
  grant and reassign_owned; its adoption, reader and drop_vector cases
  failed before the fixes, as did the new pg_tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
DESIGN.md §5.7's Implemented note records the locking sweep, the
creator-checked vector deletes, the PG 16+ rule for the extension's owner,
the bootstrap refusal, and that the trigger's refusal reads without a lock
(an uncommitted transfer to the role is missed and left to the sweep);
docs/semantics.md and the CHANGELOG follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
@wfaithfull
wfaithfull merged commit 891e81f into main Oct 23, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants