Repository navigation
Add event_trigger__disable()/__enable() for update scripts - #45
jnasbyupgrade wants to merge 8 commits into
Conversation
`object__getsert()` (via `_object_v__for_update()`) now raises feature_not_supported for any object belonging to the object_reference extension. Letting the tracking machinery observe its own members means the extension's own DDL -- including its update scripts restructuring the extension -- becomes something that machinery reacts to. `_object_reference._is_own_object()` checks `pg_depend` membership (`deptype = 'e'`), plus the two objects that are never 'e' members: the extension's declared schema and its own `pg_extension` row. `_etg_capture` also skips objects in the object_reference, _object_reference and __object_reference schemas: an object created by the extension's own install/update script isn't recorded as an 'e' member until the surrounding CREATE/ALTER EXTENSION finishes, so `_is_own_object()` can't see it yet when ddl_command_end fires. The 0.1.0 -> stable update script creates `_is_own_object()` and the updated `_etg_capture` inside its existing session_replication_role section. Part of #40
…le() `_object_reference.event_trigger__disable(name[])` disables the given event triggers and records each one's prior `evtenabled` value in a temp table; `event_trigger__enable()` restores exactly that state. ALTER EVENT TRIGGER is transactional, so a rolled-back caller undoes the DISABLE without separate cleanup. `_etg_capture` and `_etg_fix_identity` return early while that temp table exists in the current session, so they never need disabling. `_etg_drop` can't recognize the extension's own update script that way, so the 0.1.0 -> stable update script disables it with `event_trigger__disable()` around its structural section, replacing the `session_replication_role = replica` bracket. 0.1.0's `_etg_capture`/`_etg_fix_identity` have no such guard, so the update script disables both with raw ALTER EVENT TRIGGER for the bootstrap statements that run before their guarded replacements are installed, then restores each one's saved prior state. Closes #40
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
|
Claude finished @jnasbyupgrade's task in 30s —— View job Code review: PR #45 (event_trigger__disable()/__enable())I read the diff against No blocking bugs found. The save/restore logic holds up:
Observations
|
In update mode, test/install/load.sql now starts `zzz_object_reference_capture`, `zzz_object_reference__fix_identity` and `zzz__object_reference_drop` in distinct non-default states (DISABLE, ENABLE REPLICA, ENABLE ALWAYS), runs the update, and raises unless each one is back in exactly that state. That covers both restore paths: the bootstrap block for the first two and `event_trigger__enable()` for the third. load.sql's own output is never compared against anything, so the update and the check share a transaction: a failed check also undoes the update, and the suite then fails against the old version. All three triggers are reset to ENABLE afterwards so the rest of the suite runs unchanged.
|
Claude finished @jnasbyupgrade's task in 24s —— View job Code review: PR #45 (
|
|
Written by Claude. Replies to items 2-5 of the automated review.
No change. The error is the intended behavior, and the state is not lost.
No change.
No change. The bootstrap temp table can't be registered. 0.1.0's
Fixed in 8429c9d. In update mode, |
|
Written by Claude. Re: #45 (comment)
No change. Passing NULL does not produce a generic error: PL/pgSQL raises
Items 2 and 3 of this review repeat items 2 and 3 of the earlier review, answered in #45 (comment). |
|
Claude finished @jnasbyupgrade's task in 14s —— View job Code review: PR #45 (
|
| ; | ||
| END; | ||
|
|
||
| IF array_length(event_trigger_names, 1) <> (SELECT count(DISTINCT x) FROM unnest(event_trigger_names) x) THEN |
There was a problem hiding this comment.
Caller sanity checks like this should be done before we take meaningful action (unless there's a GOOD reason to wait).
There was a problem hiding this comment.
Claude here. Done in 7a44f99. event_trigger__disable() now runs every argument check before any side effect (temp table creation, lock, ALTER): NULL/empty array, NULL elements, duplicate names, every named trigger exists, and finally the "previous call still in effect" check via to_regclass() instead of catching duplicate_table. event_trigger__enable() likewise checks for a matching disable() up front. The only check left after the lock is a defensive re-check that a trigger was not dropped by another session in between. Because a raised error rolls the call back, the tests assert error precedence (argument errors are reported even while a previous call is in effect) rather than leftover state.
| SELECT evtenabled INTO v_enabled | ||
| FROM pg_catalog.pg_event_trigger | ||
| WHERE evtname = v_name | ||
| FOR UPDATE |
There was a problem hiding this comment.
This is not going to work on something like RDS that doesn't give true superuser access. I'd say we should use an advisory lock as a backup mechanism (ie, detect the error we'd get for not being able to modify the catalog).
Also, we should force event_trigger_names to be sorted (in a fixed local); otherwise there's a deadlock potential.
There was a problem hiding this comment.
Claude here. Done in 7a44f99.
Lock fallback: per-trigger locking moved into _object_reference._event_trigger__lock(name). It tries SELECT ... FOR UPDATE on pg_event_trigger and, on insufficient_privilege (SQLSTATE 42501) only, takes pg_advisory_xact_lock(<pg_event_trigger regclass oid>::int, <trigger oid>::int). I confirmed 42501 ("permission denied for table pg_event_trigger") by running that SELECT as a plain non-superuser role on PG12 and PG17. Oid-to-int casts wrap (4294967295::oid::int is -1) rather than error, so large oids are safe as keys. The advisory lock only serializes against other callers of this function. A test calls the helper under SET ROLE as a role that cannot row-lock and checks pg_locks.
Sorting: I read "force event_trigger_names to be sorted (in a fixed local)" as the function sorting the names itself under COLLATE "C", not rejecting unsorted input. disable() locks and disables in that order and enable() restores in the same order. A test passes unsorted names and checks they are processed in "C" order.
In update mode, test/install/load.sql now runs three update scenarios before the committed update, each installing 0.1.0 in its own transaction and rolling back: 1. All three event triggers at ENABLE: the update leaves the capture group empty, restores every trigger, leaves no state table behind, and a table created afterwards is captured. 2. zzz__object_reference_drop already disabled: it stays disabled. 3. A table captured before the update and renamed after it shows its new name in the capture group. Each scenario asserts the trigger state it starts the update from. Also restore the trailing space after WHERE in the update script's copy of _etg_fix_identity, so its body matches sql/object_reference.sql, and correct load.sql's description of how a failed check fails the run.
|
Claude finished @jnasbyupgrade's task in 21s —— View job Code review: PR #45 (
|
…fects disable() now rejects a NULL/empty array, NULL elements, duplicates, and unknown names, and a call still in effect, before creating its state table or locking anything. enable() checks for a matching disable() up front. Names are processed in "C" collation order (and restored in the same order) so concurrent callers lock in a consistent order. Per-trigger locking moves to _event_trigger__lock(), which falls back to a transaction-scoped advisory lock when the role cannot row-lock pg_event_trigger (insufficient_privilege). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Claude finished @jnasbyupgrade's task in 17s —— View job Code review: PR #45 (
|
…rage - Move _event_trigger__lock() ahead of the WARNING block so the block documents event_trigger__disable()/__enable() again; state the advisory fallback's actual scope (other callers of disable() only). - Use pg_event_trigger column %TYPE declarations in the functions this PR adds. - Drop the "nothing was changed" assertions (a rolled-back call can't fail them) and add error-precedence tests for an empty array and a NULL element. - Correct the load.sql comment: a failed check does not fail the make run on any PostgreSQL version.
|
Claude finished @jnasbyupgrade's task in 19s —— View job Code review: PR #45 (
|
Questioned automated-review findings: #45 (comment), #45 (comment)
Replaces the 0.1.0 -> stable update script's
session_replication_role = replicabracket with a reusable pair,_object_reference.event_trigger__disable(name[])andevent_trigger__enable(), for this and future update scripts.disable()records each named trigger's exact priorevtenabledvalue before disabling it, andenable()restores that state rather than assumingorigin. Concurrent protection depends on privilege: with UPDATE onpg_event_trigger,disable()row-locks each trigger so another session'sALTER EVENT TRIGGERon it waits; without it (as on managed services such as RDS) it takes an advisory lock, which serializes only other callers ofdisable(), not a rawALTER EVENT TRIGGERin another session.Closes #40
Details
zzz_object_reference_captureandzzz_object_reference__fix_identityreturn immediately whiledisable()'s temp table exists in the current session (checked withto_regclass()), so they never need disabling themselves.zzz__object_reference_dropcan't recognize the extension's own update script that way, so the update script disables it withevent_trigger__disable('{zzz__object_reference_drop}')around its structural section.ALTER EVENT TRIGGERis transactional, so a rolled-back caller undoes theDISABLEwithout separate cleanup.disable()validates its arguments before any side effect, in this order: NULL or empty array, a NULL element, a duplicate name, an unknown trigger name, and finally a previous call still in effect.enable()rejects a call with no matchingdisable()before doing anything.disable()sorts the names underCOLLATE "C"and locks and disables in that order;enable()restores in the same order, so concurrent callers can't deadlock on lock order._object_reference._event_trigger__lock(name), which triesSELECT ... FOR UPDATEon the trigger'spg_event_triggerrow and, only oninsufficient_privilege(42501), takespg_advisory_xact_lockkeyed on the catalog and trigger oids. It returns the trigger'sevtenabled, or NULL if it no longer exists._etg_capture/_etg_fix_identitybodies have no such check, so the update script first disables both with rawALTER EVENT TRIGGER, installs the guarded bodies, then restores each one's saved prior state before creatingdisable()/enable().test/sql/event_trigger_disable.sql(38 assertions) exercises the mechanism against its own dummy event triggers: prior-state preservation across two triggers, rejection of a nested call, an unknown name, a duplicate name, a NULL array, an empty array and a NULL element, which error wins over a call still in effect, "C"-order processing of unsorted names, the advisory fallback for a role that can't row-lock the catalog, andzzz_object_reference_capturestanding down (a table created while adisable()is in effect isn't captured). A raised error rolls the call back, so the tests can't observe whether validation ran before a side effect; they check which error is reported.make test-update,test/install/load.sqlchecks the 0.1.0 -> stable update, with a capture group active, in three rolled-back scenarios: (1) all three event triggers at their default state, then a table created after the update is captured; (2)zzz__object_reference_dropalready disabled, and still disabled after; (3) a table captured before the update is renamed after it, and the group shows the new name. It then runs the update once more withzzz_object_reference_capture,zzz_object_reference__fix_identityandzzz__object_reference_dropinDISABLE,ENABLE REPLICAandENABLE ALWAYS, and fails unless each is restored to exactly that state, which covers both the bootstrap restore andenable(). A failed check exits psql nonzero (ON_ERROR_STOP), but that does not fail the make run on any PostgreSQL version (pg_regress 16+ printsnot ok, PG12's printsok, and make exits 0 either way). CI runs fresh mode only.test/build/expected/build.outchanges only by line-number shifts.Known open item
On PostgreSQL 12 (and probably 13), updating from 0.1.0 fails when a capture group is active and
zzz__object_reference_dropwas already disabled, withcache lookup failed for namespacefrom_etg_capture. The cause is apg_event_trigger_ddl_commands()bug fixed upstream only in PG14+; the fix here is pending. PG12's pg_regress reports the resulting psql failure as ok, somake test-updatedoes not flag it there.