From e03a0e292790fe9cfe41b04ea483efe609bd5865 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 17 Aug 2026 17:43:04 -0500 Subject: [PATCH] Add internal update disable mechanism; refuse to self-track extension objects object__getsert() (via _object_v__for_update()) now refuses to track any object that is itself a member of the object_reference extension, closing a bootstrapping hazard where the extension's own update-time restructuring could trip its own rename-detection/repair machinery. Replace the update script's session_replication_role trick with a real, reusable mechanism: zzz_object_reference__fix_identity and zzz_object_reference_capture self-recognize (and skip) DDL from any extension's own install/update script via in_extension; zzz__object_reference_drop can't self-recognize that way, so internal_update__begin()/__end() explicitly disable/re-enable it (saving and restoring its actual prior state) for future update scripts to reuse. Closes #40. --- sql/object_reference--0.1.0--stable.sql | 278 +++++++++++++++++++++--- sql/object_reference.sql | 127 ++++++++++- test/build/expected/build.out | 13 +- test/expected/base.out | 10 +- test/expected/internal_update.out | 18 ++ test/sql/base.sql | 16 +- test/sql/internal_update.sql | 91 ++++++++ 7 files changed, 514 insertions(+), 39 deletions(-) create mode 100644 test/expected/internal_update.out create mode 100644 test/sql/internal_update.sql diff --git a/sql/object_reference--0.1.0--stable.sql b/sql/object_reference--0.1.0--stable.sql index b965953..eb3f94f 100644 --- a/sql/object_reference--0.1.0--stable.sql +++ b/sql/object_reference--0.1.0--stable.sql @@ -105,32 +105,132 @@ 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 internal_update__begin()/__end() 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') +) +$body$ + , 'Is the object a member of the object_reference extension itself? (pg_depend deptype = e membership, not just co-installation.)' +); + +/* + * New: internal update-time 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 internal_update__begin() below -- a fresh + * install only ever needs these for FUTURE update scripts. */ -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.internal_update__begin' + , $args$ + event_trigger_names name[] DEFAULT '{zzz__object_reference_drop}' +$args$ + , 'void LANGUAGE plpgsql' + , $body$ +DECLARE + v_name name; +BEGIN + BEGIN + CREATE TEMP TABLE __object_reference__internal_update( + evtname name PRIMARY KEY + , evtenabled "char" NOT NULL + ); + EXCEPTION WHEN duplicate_table THEN + RAISE 'internal_update__begin() called while already in an internal update' + USING HINT = 'A previous internal_update__end() call may have been skipped.' + ; + END; + + FOREACH v_name IN ARRAY event_trigger_names LOOP + INSERT INTO pg_temp.__object_reference__internal_update(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 for the duration of this extension''s own internal update; pair with internal_update__end().' +); +SELECT __object_reference.create_function( + '_object_reference.internal_update__end' + , '' + , 'void LANGUAGE plpgsql' + , $body$ +DECLARE + r record; +BEGIN + FOR r IN SELECT evtname, evtenabled FROM pg_temp.__object_reference__internal_update 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__internal_update; +EXCEPTION WHEN undefined_table THEN + RAISE 'internal_update__end() called without a matching internal_update__begin()'; +END +$body$ + , 'Restore event triggers disabled by internal_update__begin() to their prior enabled state.' +); + +SELECT _object_reference.internal_update__begin(); /* * _object_reference.object: no column changes, just a missing @@ -293,6 +393,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 +524,120 @@ $body$ , 'Check the sanity of object and _object_oid' ); +/* + * _etg_fix_identity/_etg_capture: gain a self-recognition guard so they skip + * DDL issued by any extension's own install/update script (ours included) + * instead of reacting to it -- see internal_update__begin()/__end() above + * for why zzz__object_reference_drop 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 +901,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 + * internal_update__begin() 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.internal_update__end(); -- vi: expandtab sw=2 ts=2 diff --git a/sql/object_reference.sql b/sql/object_reference.sql index cbed4f5..68b1cdb 100644 --- a/sql/object_reference.sql +++ b/sql/object_reference.sql @@ -161,6 +161,27 @@ $body$ , 'Execute arbitrary SQL with logging.' ); +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') +) +$body$ + , 'Is the object a member of the object_reference extension itself? (pg_depend deptype = e membership, not just co-installation.)' +); + CREATE TABLE _object_reference.object( object_id serial PRIMARY KEY , object_type cat_tools.object_type NOT NULL @@ -886,6 +907,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 * @@ -1397,7 +1426,7 @@ BEGIN END LOOP; END; - FOR r IN SELECT + FOR r IN SELECT _object_reference._object_v__for_update( object_type::cat_tools.object_type , objid, objsubid @@ -1411,6 +1440,12 @@ BEGIN 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; @@ -1431,6 +1466,15 @@ 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 @@ -1516,6 +1560,87 @@ $body$ , 'Event trigger function to drop object records when objects are removed.' ); +/* + * Internal update-time disable/enable mechanism, for use by this extension's + * OWN install/update scripts only (not part of the public API). + * + * zzz_object_reference__fix_identity and zzz_object_reference_capture can + * recognize (and skip) DDL issued by any extension's own script via + * pg_event_trigger_ddl_commands()'s in_extension column, so they never need + * to be disabled. zzz__object_reference_drop cannot: it fires from + * pg_event_trigger_dropped_objects(), which has no equivalent column, and it + * queries _object_reference._object_v -- a view an update script may itself + * be dropping and recreating -- so it must be truly disabled for the + * duration of such a script's structural section. + * + * ALTER EVENT TRIGGER is ordinary transactional DDL, so if the calling + * script's transaction rolls back, the DISABLE (and any ENABLE already run) + * rolls back with it -- no separate cleanup-on-error logic is needed here. + */ +SELECT __object_reference.create_function( + '_object_reference.internal_update__begin' + , $args$ + event_trigger_names name[] DEFAULT '{zzz__object_reference_drop}' +$args$ + , 'void LANGUAGE plpgsql' + , $body$ +DECLARE + v_name name; +BEGIN + BEGIN + CREATE TEMP TABLE __object_reference__internal_update( + evtname name PRIMARY KEY + , evtenabled "char" NOT NULL + ); + EXCEPTION WHEN duplicate_table THEN + RAISE 'internal_update__begin() called while already in an internal update' + USING HINT = 'A previous internal_update__end() call may have been skipped.' + ; + END; + + FOREACH v_name IN ARRAY event_trigger_names LOOP + INSERT INTO pg_temp.__object_reference__internal_update(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 for the duration of this extension''s own internal update; pair with internal_update__end().' +); +SELECT __object_reference.create_function( + '_object_reference.internal_update__end' + , '' + , 'void LANGUAGE plpgsql' + , $body$ +DECLARE + r record; +BEGIN + FOR r IN SELECT evtname, evtenabled FROM pg_temp.__object_reference__internal_update 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__internal_update; +EXCEPTION WHEN undefined_table THEN + RAISE 'internal_update__end() called without a matching internal_update__begin()'; +END +$body$ + , 'Restore event triggers disabled by internal_update__begin() to their prior enabled state.' +); + SELECT __object_reference.create_function( '_object_reference.etg_raise__start' , '' diff --git a/test/build/expected/build.out b/test/build/expected/build.out index fadaba6..0e62ce5 100644 --- a/test/build/expected/build.out +++ b/test/build/expected/build.out @@ -2,17 +2,17 @@ This extension must be loaded via CREATE EXTENSION object_reference; You really, REALLY do NOT want to try and load this via psql!!! -psql:test/temp_load.not_sql:176: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! -psql:test/temp_load.not_sql:177: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:197: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:198: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! -psql:test/temp_load.not_sql:425: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:446: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! @@ -21,9 +21,12 @@ psql:test/temp_load.not_sql:425: WARNING: I promise you will be sorry if you tr -psql:test/temp_load.not_sql:537: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! -psql:test/temp_load.not_sql:544: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:558: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! + +psql:test/temp_load.not_sql:565: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! + + diff --git a/test/expected/base.out b/test/expected/base.out index d5180ba..f2fa6f4 100644 --- a/test/expected/base.out +++ b/test/expected/base.out @@ -1,5 +1,5 @@ \set ECHO none -1..12 +1..14 ok 1 - Role object_reference__dependency should be granted USAGE on schema _object_reference ok 2 - Role object_reference__dependency should be granted REFERENCES on table _object_reference.object ok 3 - CREATE TEMP TABLE test_object AS SELECT object_reference.object__getsert('table', 'test_table') AS object_id; @@ -9,7 +9,9 @@ ok 6 - object__identity returns same result as pg_identify_object ok 7 - Existing object works, provides correct ID ok 8 - secondary may not be specified for table objects ok 9 - temp objects are rejected -ok 10 - CREATE EXTENSION test_factory -ok 11 - object_reference schema must not be part of the resolved search_path -ok 12 - _object_reference schema must not be part of the resolved search_path +ok 10 - own tracking table is rejected +ok 11 - own event trigger function is rejected +ok 12 - CREATE EXTENSION test_factory +ok 13 - object_reference schema must not be part of the resolved search_path +ok 14 - _object_reference schema must not be part of the resolved search_path # TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/internal_update.out b/test/expected/internal_update.out new file mode 100644 index 0000000..f939ea7 --- /dev/null +++ b/test/expected/internal_update.out @@ -0,0 +1,18 @@ +\set ECHO none +1..15 +ok 1 - internal_update__begin() disables the default trigger +ok 2 - zzz__object_reference_drop is disabled while an internal update is in progress +ok 3 - internal_update__end() re-enables it +ok 4 - zzz__object_reference_drop is back to its original (origin) state +ok 5 - manually disable zzz_object_reference_capture ahead of time +ok 6 - begin()/end() round-trip on an already-disabled trigger +ok 7 - still disabled afterward -- its prior state was preserved, not assumed enabled +ok 8 - restore zzz_object_reference_capture for later tests +ok 9 - begin() the first time +ok 10 - a second begin() without end() in between is rejected +ok 11 - end() cleans up so later tests are unaffected +ok 12 - end() without begin() is rejected +ok 13 - begin() rejects an unknown event trigger name +ok 14 - object_reference schema must not be part of the resolved search_path +ok 15 - _object_reference schema must not be part of the resolved search_path +# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/sql/base.sql b/test/sql/base.sql index 538f4f8..7c8dd7e 100644 --- a/test/sql/base.sql +++ b/test/sql/base.sql @@ -9,7 +9,7 @@ SELECT plan( +1 -- schema +3 -- initial +2 -- new functions - +3 -- errors (includes temp object test) + +5 -- errors (includes temp object + self-tracking rejection tests) +1 -- create extensions +2 -- schema-qualification (search_path) ); @@ -74,6 +74,20 @@ SELECT throws_ok( , 'temp objects are rejected' ); +-- Test rejection of object_reference's own extension-member objects +SELECT throws_ok( + $$SELECT object_reference.object__getsert('table', '_object_reference.object')$$ + , '0A000' -- feature_not_supported + , 'cannot track an object that is a member of the object_reference extension itself' + , 'own tracking table is rejected' +); +SELECT throws_ok( + $$SELECT object_reference.object__getsert('function', '_object_reference._etg_drop', '')$$ + , '0A000' -- feature_not_supported + , 'cannot track an object that is a member of the object_reference extension itself' + , 'own event trigger function is rejected' +); + -- Create extensions SELECT lives_ok( $$CREATE EXTENSION test_factory$$ diff --git a/test/sql/internal_update.sql b/test/sql/internal_update.sql new file mode 100644 index 0000000..e5c0c62 --- /dev/null +++ b/test/sql/internal_update.sql @@ -0,0 +1,91 @@ +\set ECHO none + +\i test/load.sql + +SELECT plan( + 0 + +4 -- default begin/end round-trip disables, then restores, the trigger + +4 -- begin/end preserves a non-default prior state instead of assuming enabled + +3 -- nested begin() without an intervening end() is rejected + +1 -- end() without a matching begin() is rejected + +1 -- begin() rejects an unknown event trigger name + +2 -- schema-qualification (search_path) +); + +-- Default begin/end round-trip +SELECT lives_ok( + $$SELECT _object_reference.internal_update__begin()$$ + , 'internal_update__begin() disables the default trigger' +); +SELECT is( + (SELECT evtenabled FROM pg_catalog.pg_event_trigger WHERE evtname = 'zzz__object_reference_drop') + , 'D' + , 'zzz__object_reference_drop is disabled while an internal update is in progress' +); +SELECT lives_ok( + $$SELECT _object_reference.internal_update__end()$$ + , 'internal_update__end() re-enables it' +); +SELECT is( + (SELECT evtenabled FROM pg_catalog.pg_event_trigger WHERE evtname = 'zzz__object_reference_drop') + , 'O' + , 'zzz__object_reference_drop is back to its original (origin) state' +); + +-- Preserve a non-default prior state (already disabled for unrelated reasons) +SELECT lives_ok( + $$ALTER EVENT TRIGGER zzz_object_reference_capture DISABLE$$ + , 'manually disable zzz_object_reference_capture ahead of time' +); +SELECT lives_ok( + $$ + SELECT _object_reference.internal_update__begin('{zzz_object_reference_capture}'); + SELECT _object_reference.internal_update__end(); + $$ + , 'begin()/end() round-trip on an already-disabled trigger' +); +SELECT is( + (SELECT evtenabled FROM pg_catalog.pg_event_trigger WHERE evtname = 'zzz_object_reference_capture') + , 'D' + , 'still disabled afterward -- its prior state was preserved, not assumed enabled' +); +SELECT lives_ok( + $$ALTER EVENT TRIGGER zzz_object_reference_capture ENABLE$$ + , 'restore zzz_object_reference_capture for later tests' +); + +-- Nested begin() without an intervening end() +SELECT lives_ok( + $$SELECT _object_reference.internal_update__begin()$$ + , 'begin() the first time' +); +SELECT throws_ok( + $$SELECT _object_reference.internal_update__begin()$$ + , NULL + , 'internal_update__begin() called while already in an internal update' + , 'a second begin() without end() in between is rejected' +); +SELECT lives_ok( + $$SELECT _object_reference.internal_update__end()$$ + , 'end() cleans up so later tests are unaffected' +); + +-- end() without a matching begin() +SELECT throws_ok( + $$SELECT _object_reference.internal_update__end()$$ + , NULL + , 'internal_update__end() called without a matching internal_update__begin()' + , 'end() without begin() is rejected' +); + +-- Unknown event trigger name +SELECT throws_ok( + $$SELECT _object_reference.internal_update__begin('{no_such_event_trigger}')$$ + , NULL + , 'event trigger "no_such_event_trigger" does not exist' + , 'begin() rejects an unknown event trigger name' +); + +\i test/finish.sql + +-- vi: expandtab sw=2 ts=2