Skip to content

Commit e03a0e2

Browse files
committed
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.
1 parent 65be85b commit e03a0e2

7 files changed

Lines changed: 514 additions & 39 deletions

File tree

sql/object_reference--0.1.0--stable.sql

Lines changed: 250 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -105,32 +105,132 @@ END
105105
$body$;
106106

107107
/*
108-
* 0.1.0 already installed this extension's own event triggers, and they
109-
* stay active for the rest of THIS session while the structural changes
110-
* below run. zzz__object_reference_drop in particular queries
111-
* _object_reference._object_v inside its own body, so it would fire -- and
112-
* error, since the view is momentarily gone -- the instant this script drops
113-
* that view a few statements down. All three are default-enabled (origin),
114-
* so setting session_replication_role = replica suppresses them for the
115-
* structural section below.
108+
* New: _object_reference.exec(), the permanent counterpart to
109+
* __object_reference.exec() above, used by object__dependency__add() /
110+
* object_group__dependency__add() (unchanged since 0.1.0, but 0.1.0 never
111+
* created this permanent helper -- an existing gap this update closes) and
112+
* by internal_update__begin()/__end() below.
113+
*/
114+
SELECT __object_reference.create_function(
115+
'_object_reference.exec'
116+
, 'sql text'
117+
, 'void LANGUAGE plpgsql'
118+
, $body$
119+
BEGIN
120+
RAISE DEBUG 'sql = %', sql;
121+
EXECUTE sql;
122+
END
123+
$body$
124+
, 'Execute arbitrary SQL with logging.'
125+
);
126+
127+
/*
128+
* New: refuse to track objects that are themselves members of the
129+
* object_reference extension (see the guard added to
130+
* _object_v__for_update() below).
131+
*/
132+
SELECT __object_reference.create_function(
133+
'_object_reference._is_own_object'
134+
, $args$
135+
classid oid
136+
, objid oid
137+
$args$
138+
, 'boolean LANGUAGE sql STABLE'
139+
, $body$
140+
SELECT EXISTS(
141+
SELECT 1
142+
FROM pg_catalog.pg_depend d
143+
WHERE d.classid = _is_own_object.classid
144+
AND d.objid = _is_own_object.objid
145+
AND d.deptype = 'e'
146+
AND d.refclassid = 'pg_catalog.pg_extension'::regclass
147+
AND d.refobjid = (SELECT oid FROM pg_catalog.pg_extension WHERE extname = 'object_reference')
148+
)
149+
$body$
150+
, 'Is the object a member of the object_reference extension itself? (pg_depend deptype = e membership, not just co-installation.)'
151+
);
152+
153+
/*
154+
* New: internal update-time disable/enable mechanism, replacing the
155+
* session_replication_role trick 0.1.0 had no equivalent of. 0.1.0 already
156+
* installed this extension's own event triggers, and they stay active for
157+
* the rest of THIS session while the structural changes below run.
158+
* zzz__object_reference_drop in particular queries _object_reference._object_v
159+
* inside its own body, so it would fire -- and error, since the view is
160+
* momentarily gone -- the instant this script drops that view a few
161+
* statements down. The other two event triggers self-recognize and skip our
162+
* own script's DDL instead (see _etg_fix_identity/_etg_capture below), so
163+
* only zzz__object_reference_drop needs to be disabled here.
116164
*
117-
* This script is not necessarily the only thing running in its transaction
118-
* -- ALTER EXTENSION UPDATE can be issued as one statement among several in
119-
* a caller-managed transaction -- so session_replication_role cannot simply
120-
* be left disturbed for "the rest of the transaction" to sort out, and
121-
* whatever it's restored to afterward must be the caller's actual prior
122-
* value, not an assumed 'origin' default (the caller may already have it set
123-
* to something else for their own reasons). Stashed in a placeholder GUC
124-
* (there's no other way to carry a value between separate top-level
125-
* statements in a plain multi-statement SQL script -- this isn't a single
126-
* PL/pgSQL block) and restored explicitly right after the cleanup at the end
127-
* of this script, once every object the event triggers reference is back in
128-
* its final, current-source shape. A fresh install never hits this: it
129-
* creates these event triggers only at the very end, once nothing they
130-
* reference is still being modified.
165+
* These are created now, ahead of the structural section, specifically so
166+
* this script itself can call internal_update__begin() below -- a fresh
167+
* install only ever needs these for FUTURE update scripts.
131168
*/
132-
SELECT set_config('object_reference.saved_session_replication_role', current_setting('session_replication_role'), true);
133-
SET LOCAL session_replication_role = replica;
169+
SELECT __object_reference.create_function(
170+
'_object_reference.internal_update__begin'
171+
, $args$
172+
event_trigger_names name[] DEFAULT '{zzz__object_reference_drop}'
173+
$args$
174+
, 'void LANGUAGE plpgsql'
175+
, $body$
176+
DECLARE
177+
v_name name;
178+
BEGIN
179+
BEGIN
180+
CREATE TEMP TABLE __object_reference__internal_update(
181+
evtname name PRIMARY KEY
182+
, evtenabled "char" NOT NULL
183+
);
184+
EXCEPTION WHEN duplicate_table THEN
185+
RAISE 'internal_update__begin() called while already in an internal update'
186+
USING HINT = 'A previous internal_update__end() call may have been skipped.'
187+
;
188+
END;
189+
190+
FOREACH v_name IN ARRAY event_trigger_names LOOP
191+
INSERT INTO pg_temp.__object_reference__internal_update(evtname, evtenabled)
192+
SELECT evtname, evtenabled FROM pg_catalog.pg_event_trigger WHERE evtname = v_name;
193+
194+
IF NOT FOUND THEN
195+
RAISE 'event trigger "%" does not exist', v_name;
196+
END IF;
197+
198+
PERFORM _object_reference.exec(format('ALTER EVENT TRIGGER %I DISABLE', v_name));
199+
END LOOP;
200+
END
201+
$body$
202+
, 'Disable the given (or default) event triggers for the duration of this extension''s own internal update; pair with internal_update__end().'
203+
);
204+
SELECT __object_reference.create_function(
205+
'_object_reference.internal_update__end'
206+
, ''
207+
, 'void LANGUAGE plpgsql'
208+
, $body$
209+
DECLARE
210+
r record;
211+
BEGIN
212+
FOR r IN SELECT evtname, evtenabled FROM pg_temp.__object_reference__internal_update LOOP
213+
PERFORM _object_reference.exec(format(
214+
'ALTER EVENT TRIGGER %I %s'
215+
, r.evtname
216+
, CASE r.evtenabled
217+
WHEN 'O' THEN 'ENABLE'
218+
WHEN 'R' THEN 'ENABLE REPLICA'
219+
WHEN 'A' THEN 'ENABLE ALWAYS'
220+
WHEN 'D' THEN 'DISABLE'
221+
END
222+
));
223+
END LOOP;
224+
225+
DROP TABLE pg_temp.__object_reference__internal_update;
226+
EXCEPTION WHEN undefined_table THEN
227+
RAISE 'internal_update__end() called without a matching internal_update__begin()';
228+
END
229+
$body$
230+
, 'Restore event triggers disabled by internal_update__begin() to their prior enabled state.'
231+
);
232+
233+
SELECT _object_reference.internal_update__begin();
134234

135235
/*
136236
* _object_reference.object: no column changes, just a missing
@@ -293,6 +393,14 @@ BEGIN
293393
;
294394
END IF;
295395

396+
-- Refuse to track objects that are themselves members of this extension
397+
IF _object_reference._is_own_object(c_classid, objid) THEN
398+
RAISE 'cannot track an object that is a member of the object_reference extension itself'
399+
USING DETAIL = format('object %s belongs to the object_reference extension', r_identity.identity)
400+
, ERRCODE = 'feature_not_supported'
401+
;
402+
END IF;
403+
296404
-- Ensure the object record exists
297405
SELECT INTO r_object_v
298406
*
@@ -416,6 +524,120 @@ $body$
416524
, 'Check the sanity of object and _object_oid'
417525
);
418526

527+
/*
528+
* _etg_fix_identity/_etg_capture: gain a self-recognition guard so they skip
529+
* DDL issued by any extension's own install/update script (ours included)
530+
* instead of reacting to it -- see internal_update__begin()/__end() above
531+
* for why zzz__object_reference_drop needs a different mechanism. Same
532+
* signatures as 0.1.0, so a plain CREATE OR REPLACE (via create_function) is
533+
* enough -- no DROP needed.
534+
*/
535+
SELECT __object_reference.create_function(
536+
'_object_reference._etg_fix_identity'
537+
, ''
538+
, 'event_trigger SECURITY DEFINER LANGUAGE plpgsql'
539+
, $body$
540+
DECLARE
541+
r_ddl record;
542+
r record;
543+
BEGIN
544+
/*
545+
* Self-recognition: skip DDL issued by any extension's own install/update
546+
* script (ours included); pg_event_trigger_ddl_commands() marks this via
547+
* in_extension, unlike pg_event_trigger_dropped_objects() (see _etg_drop).
548+
*/
549+
IF EXISTS(SELECT 1 FROM pg_catalog.pg_event_trigger_ddl_commands() WHERE in_extension) THEN
550+
RETURN;
551+
END IF;
552+
553+
/*
554+
* It's tempting to use pg_event_trigger_ddl_commands() to find exactly what
555+
* items have changed and worry about only those. That won't work because an
556+
* object_names array can depend on multiple names (ie: a column depends on
557+
* the name of it's table, as well as the name of the schema the table is in.
558+
* You might think we could simply recurse through pg_depend to handle this,
559+
* but not every name dependency gets enumerated that way. For example,
560+
* columns are not marked as dependent on their table.
561+
*
562+
* Rather than trying to be cute about this, we just do a brute-force check
563+
* for any names that have changed.
564+
*/
565+
566+
/*
567+
* Presumably there's no way for an objects type/classid to change, but be
568+
* safe and attempt the update to object_type. If it actually does change the
569+
* constraint on the table should catch it.
570+
*/
571+
FOR r IN
572+
UPDATE _object_reference.object
573+
SET object_type = (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)).type::cat_tools.object_type
574+
, object_names = (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)).object_names
575+
, object_args = (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)).object_args
576+
FROM _object_reference._object_oid oo
577+
WHERE
578+
oo.object_id = object.object_id
579+
AND (object_type::text, object_names, object_args) IS DISTINCT FROM
580+
(pg_catalog.pg_identify_object_as_address(classid, objid, objsubid))
581+
RETURNING *
582+
LOOP
583+
RAISE DEBUG 'modified_objects(): %', r;
584+
END LOOP;
585+
END
586+
$body$
587+
, 'Event trigger function to update any records with object names or args that have changed.'
588+
);
589+
SELECT __object_reference.create_function(
590+
'_object_reference._etg_capture'
591+
, ''
592+
, 'event_trigger SECURITY DEFINER LANGUAGE plpgsql'
593+
, $body$
594+
DECLARE
595+
c_group_id CONSTANT int := object_group_id FROM object_reference.capture__get_current();
596+
r record;
597+
BEGIN
598+
599+
IF c_group_id IS NOT NULL THEN -- Would be NULL if table is empty
600+
RAISE DEBUG E'\n\n*** START ***';
601+
BEGIN
602+
FOR r IN
603+
SELECT classid, objid, objsubid, command_tag, object_type, schema_name, object_identity, in_extension
604+
-- Have to manually exclude command field :/
605+
FROM pg_catalog.pg_event_trigger_ddl_commands()
606+
LOOP
607+
RAISE DEBUG 'ddl: %', row_to_json(r);
608+
END LOOP;
609+
END;
610+
611+
FOR r IN SELECT
612+
_object_reference._object_v__for_update(
613+
object_type::cat_tools.object_type
614+
, objid, objsubid
615+
, c_group_id
616+
, classid
617+
)
618+
, classid, objid, objsubid, command_tag, object_type, schema_name, object_identity, in_extension
619+
FROM pg_catalog.pg_event_trigger_ddl_commands()
620+
WHERE command_tag ~ '^CREATE' --'^(ALTER|CREATE)'
621+
AND NOT object_reference.unsupported(object_type::cat_tools.object_type)
622+
AND (schema_name IS NULL
623+
OR schema_name NOT LIKE 'pg_temp%' -- pg_my_temp_schema() doesn't seem worth it...
624+
)
625+
/*
626+
* Self-recognition: skip DDL issued by any extension's own
627+
* install/update script (ours included) rather than trying to
628+
* capture it.
629+
*/
630+
AND NOT in_extension
631+
LOOP
632+
RAISE DEBUG 'registered %', row_to_json(r);
633+
END LOOP;
634+
RAISE DEBUG E'*** END ***\n\n';
635+
END IF;
636+
END
637+
$body$
638+
, 'Event trigger function to capture newly created objects in an object group.'
639+
);
640+
419641
/*
420642
* object_reference.unsupported(): additionally exclude "partitioned
421643
* table"/"partitioned index" (pg_get_object_address() only recognizes the
@@ -679,10 +901,10 @@ DROP FUNCTION __object_reference.exec(
679901
DROP SCHEMA __object_reference;
680902

681903
/*
682-
* Restore session_replication_role to the caller's actual prior value
683-
* (saved near the top of this script), now that the structural section and
684-
* its cleanup are both done.
904+
* Re-enable zzz__object_reference_drop (to its actual prior state, saved by
905+
* internal_update__begin() near the top of this script), now that the
906+
* structural section and its cleanup are both done.
685907
*/
686-
SELECT set_config('session_replication_role', current_setting('object_reference.saved_session_replication_role'), true);
908+
SELECT _object_reference.internal_update__end();
687909

688910
-- vi: expandtab sw=2 ts=2

0 commit comments

Comments
 (0)