-
Notifications
You must be signed in to change notification settings - Fork 1
Reject self-tracked objects; add reusable update-disable mechanism #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e03a0e2
79474be
7ba1a93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,32 +105,154 @@ END | |
| $body$; | ||
|
|
||
| /* | ||
| * 0.1.0 already installed this extension's own event triggers, and they | ||
| * stay active for the rest of THIS session while the structural changes | ||
| * below run. zzz__object_reference_drop in particular queries | ||
| * _object_reference._object_v inside its own body, so it would fire -- and | ||
| * error, since the view is momentarily gone -- the instant this script drops | ||
| * that view a few statements down. All three are default-enabled (origin), | ||
| * so setting session_replication_role = replica suppresses them for the | ||
| * structural section below. | ||
| * New: _object_reference.exec(), the permanent counterpart to | ||
| * __object_reference.exec() above, used by object__dependency__add() / | ||
| * object_group__dependency__add() (unchanged since 0.1.0, but 0.1.0 never | ||
| * created this permanent helper -- an existing gap this update closes) and | ||
| * by event_trigger__disable()/__enable() below. | ||
| */ | ||
| SELECT __object_reference.create_function( | ||
| '_object_reference.exec' | ||
| , 'sql text' | ||
| , 'void LANGUAGE plpgsql' | ||
| , $body$ | ||
| BEGIN | ||
| RAISE DEBUG 'sql = %', sql; | ||
| EXECUTE sql; | ||
| END | ||
| $body$ | ||
| , 'Execute arbitrary SQL with logging.' | ||
| ); | ||
|
|
||
| /* | ||
| * New: refuse to track objects that are themselves members of the | ||
| * object_reference extension (see the guard added to | ||
| * _object_v__for_update() below). | ||
| */ | ||
| SELECT __object_reference.create_function( | ||
| '_object_reference._is_own_object' | ||
| , $args$ | ||
| classid oid | ||
| , objid oid | ||
| $args$ | ||
| , 'boolean LANGUAGE sql STABLE' | ||
| , $body$ | ||
| SELECT EXISTS( | ||
| SELECT 1 | ||
| FROM pg_catalog.pg_depend d | ||
| WHERE d.classid = _is_own_object.classid | ||
| AND d.objid = _is_own_object.objid | ||
| AND d.deptype = 'e' | ||
| AND d.refclassid = 'pg_catalog.pg_extension'::regclass | ||
| AND d.refobjid = (SELECT oid FROM pg_catalog.pg_extension WHERE extname = 'object_reference') | ||
| ) | ||
| /* | ||
| * The extension's own declared schema (object_reference) is a special | ||
| * case: CREATE EXTENSION records the EXTENSION as depending on it (a plain | ||
| * DEPENDENCY_NORMAL row, extension -> schema), not the schema as an 'e' | ||
| * member of the extension the way every other object it creates is -- so | ||
| * it never matches the pg_depend check above. | ||
| */ | ||
| OR ( | ||
| _is_own_object.classid = 'pg_catalog.pg_namespace'::regclass | ||
| AND _is_own_object.objid = (SELECT extnamespace FROM pg_catalog.pg_extension WHERE extname = 'object_reference') | ||
| ) | ||
| $body$ | ||
| , 'Is the object a member of the object_reference extension itself? (pg_depend deptype = e membership, not just co-installation.)' | ||
| ); | ||
|
|
||
| /* | ||
| * New: general-purpose event-trigger disable/enable mechanism, replacing | ||
| * the session_replication_role trick 0.1.0 had no equivalent of. 0.1.0 | ||
| * already installed this extension's own event triggers, and they stay | ||
| * active for the rest of THIS session while the structural changes below | ||
| * run. zzz__object_reference_drop in particular queries | ||
| * _object_reference._object_v inside its own body, so it would fire -- | ||
| * and error, since the view is momentarily gone -- the instant this | ||
| * script drops that view a few statements down. The other two event | ||
| * triggers self-recognize and skip our own script's DDL instead (see | ||
| * _etg_fix_identity/_etg_capture below), so only zzz__object_reference_drop | ||
| * needs to be disabled here. | ||
| * | ||
| * This script is not necessarily the only thing running in its transaction | ||
| * -- ALTER EXTENSION UPDATE can be issued as one statement among several in | ||
| * a caller-managed transaction -- so session_replication_role cannot simply | ||
| * be left disturbed for "the rest of the transaction" to sort out, and | ||
| * whatever it's restored to afterward must be the caller's actual prior | ||
| * value, not an assumed 'origin' default (the caller may already have it set | ||
| * to something else for their own reasons). Stashed in a placeholder GUC | ||
| * (there's no other way to carry a value between separate top-level | ||
| * statements in a plain multi-statement SQL script -- this isn't a single | ||
| * PL/pgSQL block) and restored explicitly right after the cleanup at the end | ||
| * of this script, once every object the event triggers reference is back in | ||
| * its final, current-source shape. A fresh install never hits this: it | ||
| * creates these event triggers only at the very end, once nothing they | ||
| * reference is still being modified. | ||
| * These are created now, ahead of the structural section, specifically so | ||
| * this script itself can call event_trigger__disable() below -- a fresh | ||
| * install only ever needs these for FUTURE update scripts, or anywhere | ||
| * else a future need to safely quiet an event trigger comes up (hence the | ||
| * mechanism-focused name, not one tied to "being mid-update"). Unlike | ||
| * session_replication_role, disabling this way is visible database-wide | ||
| * the instant it runs, not just to this script's own session -- run | ||
| * updates without concurrent DDL on tracked objects for this reason. | ||
| */ | ||
| SELECT set_config('object_reference.saved_session_replication_role', current_setting('session_replication_role'), true); | ||
| SET LOCAL session_replication_role = replica; | ||
| SELECT __object_reference.create_function( | ||
| '_object_reference.event_trigger__disable' | ||
| , $args$ | ||
| event_trigger_names name[] DEFAULT '{zzz__object_reference_drop}' | ||
| $args$ | ||
| , 'void LANGUAGE plpgsql' | ||
| , $body$ | ||
| DECLARE | ||
| v_name name; | ||
| BEGIN | ||
| BEGIN | ||
| /* | ||
| * Mirrors pg_event_trigger's own evtname/evtenabled columns (via CTAS, | ||
| * so the connection to that catalog is visible in the code, not just a | ||
| * hand-typed column list that happens to reuse its names) -- this is | ||
| * exactly the row this extension needs to restore later. | ||
| */ | ||
| CREATE TEMP TABLE __object_reference__event_trigger_state AS | ||
| SELECT evtname, evtenabled FROM pg_catalog.pg_event_trigger WHERE false; | ||
| ALTER TABLE pg_temp.__object_reference__event_trigger_state ADD PRIMARY KEY (evtname); | ||
| EXCEPTION WHEN duplicate_table THEN | ||
| RAISE 'event_trigger__disable() called while a previous call is still in effect' | ||
| USING HINT = 'A previous event_trigger__enable() call may have been skipped.' | ||
| ; | ||
| END; | ||
|
|
||
| FOREACH v_name IN ARRAY event_trigger_names LOOP | ||
| INSERT INTO pg_temp.__object_reference__event_trigger_state(evtname, evtenabled) | ||
| SELECT evtname, evtenabled FROM pg_catalog.pg_event_trigger WHERE evtname = v_name; | ||
|
|
||
| IF NOT FOUND THEN | ||
| RAISE 'event trigger "%" does not exist', v_name; | ||
| END IF; | ||
|
|
||
| PERFORM _object_reference.exec(format('ALTER EVENT TRIGGER %I DISABLE', v_name)); | ||
| END LOOP; | ||
| END | ||
| $body$ | ||
| , 'Disable the given (or default) event triggers, remembering their exact prior state; pair with event_trigger__enable().' | ||
| ); | ||
| SELECT __object_reference.create_function( | ||
| '_object_reference.event_trigger__enable' | ||
| , '' | ||
| , 'void LANGUAGE plpgsql' | ||
| , $body$ | ||
| DECLARE | ||
| r record; | ||
| BEGIN | ||
| FOR r IN SELECT evtname, evtenabled FROM pg_temp.__object_reference__event_trigger_state LOOP | ||
| PERFORM _object_reference.exec(format( | ||
| 'ALTER EVENT TRIGGER %I %s' | ||
| , r.evtname | ||
| , CASE r.evtenabled | ||
| WHEN 'O' THEN 'ENABLE' | ||
| WHEN 'R' THEN 'ENABLE REPLICA' | ||
| WHEN 'A' THEN 'ENABLE ALWAYS' | ||
| WHEN 'D' THEN 'DISABLE' | ||
| END | ||
| )); | ||
| END LOOP; | ||
|
|
||
| DROP TABLE pg_temp.__object_reference__event_trigger_state; | ||
| EXCEPTION WHEN undefined_table THEN | ||
| RAISE 'event_trigger__enable() called without a matching event_trigger__disable()'; | ||
| END | ||
| $body$ | ||
| , 'Restore event triggers disabled by event_trigger__disable() to their exact prior state.' | ||
| ); | ||
|
|
||
| SELECT _object_reference.event_trigger__disable(); | ||
|
|
||
| /* | ||
| * _object_reference.object: no column changes, just a missing | ||
|
|
@@ -293,6 +415,14 @@ BEGIN | |
| ; | ||
| END IF; | ||
|
|
||
| -- Refuse to track objects that are themselves members of this extension | ||
| IF _object_reference._is_own_object(c_classid, objid) THEN | ||
| RAISE 'cannot track an object that is a member of the object_reference extension itself' | ||
| USING DETAIL = format('object %s belongs to the object_reference extension', r_identity.identity) | ||
| , ERRCODE = 'feature_not_supported' | ||
| ; | ||
| END IF; | ||
|
|
||
| -- Ensure the object record exists | ||
| SELECT INTO r_object_v | ||
| * | ||
|
|
@@ -416,6 +546,130 @@ $body$ | |
| , 'Check the sanity of object and _object_oid' | ||
| ); | ||
|
|
||
| /* | ||
| * _etg_fix_identity/_etg_capture: gain a self-recognition guard so they skip | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs to also explain why this is needed.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Expanded: without the guard, _etg_capture would try to call _object_reference._object_v__for_update() (the FUNCTION) for any CREATE-tagged command in this very script if a capture happened to be active during an extension update — including a moment where that function has been dropped and not yet recreated, which would fail outright; _etg_fix_identity would otherwise run its blanket identity-recompute pass on every one of this script's DDL statements for no reason, since nothing it touches is (or, after this update's self-tracking guard, ever legitimately can be) one of this extension's own tracked rows. |
||
| * DDL issued by any extension's own install/update script (ours included) | ||
| * instead of reacting to it. Without it, _etg_capture would try to call | ||
| * _object_reference._object_v__for_update() (the FUNCTION) to register any | ||
| * CREATE-tagged command in this very script -- including a moment where | ||
| * that function has been dropped and not yet recreated (see the structural | ||
| * section below), which would fail outright if a capture happened to be | ||
| * active during an extension update; _etg_fix_identity would otherwise run | ||
| * its blanket identity-recompute pass on every one of this script's many | ||
| * DDL statements for no reason, since nothing it touches is (or, after this | ||
| * update's self-tracking guard, ever legitimately can be) one of this | ||
| * extension's own tracked rows. zzz__object_reference_drop can't | ||
| * self-recognize the same way; see event_trigger__disable()/__enable() | ||
| * above for why it needs a different mechanism. Same signatures as 0.1.0, | ||
| * so a plain CREATE OR REPLACE (via create_function) is enough -- no DROP | ||
| * needed. | ||
| */ | ||
| SELECT __object_reference.create_function( | ||
| '_object_reference._etg_fix_identity' | ||
| , '' | ||
| , 'event_trigger SECURITY DEFINER LANGUAGE plpgsql' | ||
| , $body$ | ||
| DECLARE | ||
| r_ddl record; | ||
| r record; | ||
| BEGIN | ||
| /* | ||
| * Self-recognition: skip DDL issued by any extension's own install/update | ||
| * script (ours included); pg_event_trigger_ddl_commands() marks this via | ||
| * in_extension, unlike pg_event_trigger_dropped_objects() (see _etg_drop). | ||
| */ | ||
| IF EXISTS(SELECT 1 FROM pg_catalog.pg_event_trigger_ddl_commands() WHERE in_extension) THEN | ||
| RETURN; | ||
| END IF; | ||
|
|
||
| /* | ||
| * It's tempting to use pg_event_trigger_ddl_commands() to find exactly what | ||
| * items have changed and worry about only those. That won't work because an | ||
| * object_names array can depend on multiple names (ie: a column depends on | ||
| * the name of it's table, as well as the name of the schema the table is in. | ||
| * You might think we could simply recurse through pg_depend to handle this, | ||
| * but not every name dependency gets enumerated that way. For example, | ||
| * columns are not marked as dependent on their table. | ||
| * | ||
| * Rather than trying to be cute about this, we just do a brute-force check | ||
| * for any names that have changed. | ||
| */ | ||
|
|
||
| /* | ||
| * Presumably there's no way for an objects type/classid to change, but be | ||
| * safe and attempt the update to object_type. If it actually does change the | ||
| * constraint on the table should catch it. | ||
| */ | ||
| FOR r IN | ||
| UPDATE _object_reference.object | ||
| SET object_type = (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)).type::cat_tools.object_type | ||
| , object_names = (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)).object_names | ||
| , object_args = (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)).object_args | ||
| FROM _object_reference._object_oid oo | ||
| WHERE | ||
| oo.object_id = object.object_id | ||
| AND (object_type::text, object_names, object_args) IS DISTINCT FROM | ||
| (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)) | ||
| RETURNING * | ||
| LOOP | ||
| RAISE DEBUG 'modified_objects(): %', r; | ||
| END LOOP; | ||
| END | ||
| $body$ | ||
| , 'Event trigger function to update any records with object names or args that have changed.' | ||
| ); | ||
| SELECT __object_reference.create_function( | ||
| '_object_reference._etg_capture' | ||
| , '' | ||
| , 'event_trigger SECURITY DEFINER LANGUAGE plpgsql' | ||
| , $body$ | ||
| DECLARE | ||
| c_group_id CONSTANT int := object_group_id FROM object_reference.capture__get_current(); | ||
| r record; | ||
| BEGIN | ||
|
|
||
| IF c_group_id IS NOT NULL THEN -- Would be NULL if table is empty | ||
| RAISE DEBUG E'\n\n*** START ***'; | ||
| BEGIN | ||
| FOR r IN | ||
| SELECT classid, objid, objsubid, command_tag, object_type, schema_name, object_identity, in_extension | ||
| -- Have to manually exclude command field :/ | ||
| FROM pg_catalog.pg_event_trigger_ddl_commands() | ||
| LOOP | ||
| RAISE DEBUG 'ddl: %', row_to_json(r); | ||
| END LOOP; | ||
| END; | ||
|
|
||
| FOR r IN SELECT | ||
| _object_reference._object_v__for_update( | ||
| object_type::cat_tools.object_type | ||
| , objid, objsubid | ||
| , c_group_id | ||
| , classid | ||
| ) | ||
| , classid, objid, objsubid, command_tag, object_type, schema_name, object_identity, in_extension | ||
| FROM pg_catalog.pg_event_trigger_ddl_commands() | ||
| WHERE command_tag ~ '^CREATE' --'^(ALTER|CREATE)' | ||
| AND NOT object_reference.unsupported(object_type::cat_tools.object_type) | ||
| AND (schema_name IS NULL | ||
| OR schema_name NOT LIKE 'pg_temp%' -- pg_my_temp_schema() doesn't seem worth it... | ||
| ) | ||
| /* | ||
| * Self-recognition: skip DDL issued by any extension's own | ||
| * install/update script (ours included) rather than trying to | ||
| * capture it. | ||
| */ | ||
| AND NOT in_extension | ||
| LOOP | ||
| RAISE DEBUG 'registered %', row_to_json(r); | ||
| END LOOP; | ||
| RAISE DEBUG E'*** END ***\n\n'; | ||
| END IF; | ||
| END | ||
| $body$ | ||
| , 'Event trigger function to capture newly created objects in an object group.' | ||
| ); | ||
|
|
||
| /* | ||
| * object_reference.unsupported(): additionally exclude "partitioned | ||
| * table"/"partitioned index" (pg_get_object_address() only recognizes the | ||
|
|
@@ -679,10 +933,10 @@ DROP FUNCTION __object_reference.exec( | |
| DROP SCHEMA __object_reference; | ||
|
|
||
| /* | ||
| * Restore session_replication_role to the caller's actual prior value | ||
| * (saved near the top of this script), now that the structural section and | ||
| * its cleanup are both done. | ||
| * Re-enable zzz__object_reference_drop (to its actual prior state, saved by | ||
| * event_trigger__disable() near the top of this script), now that the | ||
| * structural section and its cleanup are both done. | ||
| */ | ||
| SELECT set_config('session_replication_role', current_setting('object_reference.saved_session_replication_role'), true); | ||
| SELECT _object_reference.event_trigger__enable(); | ||
|
|
||
| -- vi: expandtab sw=2 ts=2 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-recognition guard is installed too late in this script.
event_trigger__disable()here only disableszzz__object_reference_drop. The comment above it claims the other two event triggers (zzz_object_reference__fix_identity/zzz_object_reference_capture) "self-recognize and skip our own script's DDL instead" via their newin_extensionguard — but that guard isn't actually installed until_etg_fix_identityand_etg_captureare recreated later in this same script (around the_object_reference._etg_fix_identity/_etg_capturecreate_functioncalls, well after the structuralDROP VIEW/CREATE VIEW/CREATE FUNCTIONsection for_object_v/_object_v__for_update/_object_oid__add).Between this line and that recreation, the old (0.1.0) unguarded bodies of those two triggers are still live. Previously
session_replication_role = replicasilenced all three triggers for this whole window; now only one is silenced.Failure scenario: a session calls
object_reference.capture__start()and then runsALTER EXTENSION object_reference UPDATEin the same transaction (the very scenario the deleted comments anticipated). TheCREATE VIEW _object_reference._object_vstatement a few lines down fires the old_etg_capture, which calls_object_reference._object_v__for_update(...)(the FUNCTION) — but that function was justDROPped and hasn't been recreated yet, so the update aborts with "function does not exist". No test intest/sql/event_trigger_disable.sqlortest/sql/base.sqlexercises an update-in-progress capture, so this isn't caught by CI.