Skip to content

Commit 90e0f9f

Browse files
committed
Fix psql \d's query for identifying parent triggers.
The original coding (from c33869c) failed with "more than one row returned by a subquery used as an expression" if there were unrelated triggers of the same tgname on parent partitioned tables. (That's possible because statement-level triggers don't get inherited.) Fix by applying LIMIT 1 after sorting the candidates by inheritance level. Also, wrap the subquery in a CASE so that we don't have to execute it at all when the trigger is visibly non-inherited. Aside from saving some cycles, this avoids the need for a confusing and undocumented NULLIF(). While here, tweak the format of the emitted query to look a bit nicer for "psql -E", and add some explanation of this subquery, because it badly needs it. Report and patch by Justin Pryzby (with some editing by me). Back-patch to v13 where the faulty code came in. Discussion: https://postgr.es/m/20211217154356.GJ17618@telsasoft.com
1 parent d18ec31 commit 90e0f9f

File tree

3 files changed

+53
-11
lines changed

3 files changed

+53
-11
lines changed

src/bin/psql/describe.c

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2953,22 +2953,45 @@ describeOneTableDetails(const char *schemaname,
29532953
printfPQExpBuffer(&buf,
29542954
"SELECT t.tgname, "
29552955
"pg_catalog.pg_get_triggerdef(t.oid%s), "
2956-
"t.tgenabled, %s, %s\n"
2957-
"FROM pg_catalog.pg_trigger t\n"
2958-
"WHERE t.tgrelid = '%s' AND ",
2956+
"t.tgenabled, %s,\n",
29592957
(pset.sversion >= 90000 ? ", true" : ""),
29602958
(pset.sversion >= 90000 ? "t.tgisinternal" :
29612959
pset.sversion >= 80300 ?
29622960
"t.tgconstraint <> 0 AS tgisinternal" :
2963-
"false AS tgisinternal"),
2964-
(pset.sversion >= 130000 ?
2965-
"(SELECT (NULLIF(a.relid, t.tgrelid))::pg_catalog.regclass"
2966-
" FROM pg_catalog.pg_trigger AS u, "
2967-
" pg_catalog.pg_partition_ancestors(t.tgrelid) AS a"
2968-
" WHERE u.tgname = t.tgname AND u.tgrelid = a.relid"
2969-
" AND u.tgparentid = 0) AS parent" :
2970-
"NULL AS parent"),
2961+
"false AS tgisinternal"));
2962+
2963+
/*
2964+
* Detect whether each trigger is inherited, and if so, get the name
2965+
* of the topmost table it's inherited from. We have no easy way to
2966+
* do that pre-v13, for lack of the tgparentid column. Even with
2967+
* tgparentid, a straightforward search for the topmost parent would
2968+
* require a recursive CTE, which seems unduly expensive. We cheat a
2969+
* bit by assuming parent triggers will match by tgname; then, joining
2970+
* with pg_partition_ancestors() allows the planner to make use of
2971+
* pg_trigger_tgrelid_tgname_index if it wishes. We ensure we find
2972+
* the correct topmost parent by stopping at the first-in-partition-
2973+
* ancestry-order trigger that has tgparentid = 0. (There might be
2974+
* unrelated, non-inherited triggers with the same name further up the
2975+
* stack, so this is important.)
2976+
*/
2977+
if (pset.sversion >= 130000)
2978+
appendPQExpBufferStr(&buf,
2979+
" CASE WHEN t.tgparentid != 0 THEN\n"
2980+
" (SELECT u.tgrelid::pg_catalog.regclass\n"
2981+
" FROM pg_catalog.pg_trigger AS u,\n"
2982+
" pg_catalog.pg_partition_ancestors(t.tgrelid) WITH ORDINALITY AS a(relid, depth)\n"
2983+
" WHERE u.tgname = t.tgname AND u.tgrelid = a.relid\n"
2984+
" AND u.tgparentid = 0\n"
2985+
" ORDER BY a.depth LIMIT 1)\n"
2986+
" END AS parent\n");
2987+
else
2988+
appendPQExpBufferStr(&buf, " NULL AS parent\n");
2989+
2990+
appendPQExpBuffer(&buf,
2991+
"FROM pg_catalog.pg_trigger t\n"
2992+
"WHERE t.tgrelid = '%s' AND ",
29712993
oid);
2994+
29722995
if (pset.sversion >= 110000)
29732996
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D') \n"
29742997
" OR EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"

src/test/regress/expected/triggers.out

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2122,6 +2122,20 @@ Triggers:
21222122
alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000) to (3000); -- fail
21232123
ERROR: trigger "trg1" for relation "trigpart3" already exists
21242124
drop table trigpart3;
2125+
-- check display of unrelated triggers
2126+
create trigger samename after delete on trigpart execute function trigger_nothing();
2127+
create trigger samename after delete on trigpart1 execute function trigger_nothing();
2128+
\d trigpart1
2129+
Table "public.trigpart1"
2130+
Column | Type | Collation | Nullable | Default
2131+
--------+---------+-----------+----------+---------
2132+
a | integer | | |
2133+
b | integer | | |
2134+
Partition of: trigpart FOR VALUES FROM (0) TO (1000)
2135+
Triggers:
2136+
samename AFTER DELETE ON trigpart1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_nothing()
2137+
trg1 AFTER INSERT ON trigpart1 FOR EACH ROW EXECUTE FUNCTION trigger_nothing(), ON TABLE trigpart
2138+
21252139
drop table trigpart;
21262140
drop function trigger_nothing();
21272141
--

src/test/regress/sql/triggers.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,11 @@ create trigger trg1 after insert on trigpart3 for each row execute procedure tri
14281428
alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000) to (3000); -- fail
14291429
drop table trigpart3;
14301430

1431+
-- check display of unrelated triggers
1432+
create trigger samename after delete on trigpart execute function trigger_nothing();
1433+
create trigger samename after delete on trigpart1 execute function trigger_nothing();
1434+
\d trigpart1
1435+
14311436
drop table trigpart;
14321437
drop function trigger_nothing();
14331438

0 commit comments

Comments
 (0)