From 1c01dd86e36678ce7f67b65219309fe0ec1809a2 Mon Sep 17 00:00:00 2001 From: Phil Alger Date: Mon, 3 Nov 2025 17:49:10 -0600 Subject: [PATCH v3] Add pretty formatting to pg_get_triggerdef This patch adds pretty formatting to the pg_get_triggerdef function. Currently, when the function is called with the pretty flag, it will not display the schema and the format is displayed as a single line. This patch modifies this function to be consistent with functions like pg_get_viewdef, pg_get_ruledef, and pg_get_indexdef, which display their output as pretty printed SQL. This is an example of the new format: 1. pg_get_triggerdef(oid, true) - New Pretty Format** postgres=# select pg_get_triggerdef(12345, true); pg_get_triggerdef -------------------------------------------------- CREATE TRIGGER some_trig_foobar AFTER UPDATE + ON some_t + FOR EACH ROW + WHEN (NOT new.some_col) + EXECUTE FUNCTION dummy_update_func('foobar') (1 row) 2. pg_get_triggerdef(oid, false) - Existing Format When pretty printing is false, or no flag is provided, the output is unchanged from the original function: postgres=# select pg_get_triggerdef(12345, false); pg_get_triggerdef --------------------------------------------------------------------------------------------------------------------------------------------------- CREATE TRIGGER some_trig_foobar AFTER UPDATE ON public.some_t FOR EACH ROW WHEN ((NOT new.some_col)) EXECUTE FUNCTION dummy_update_func('foobar') (1 row) 3. New Function: pg_get_triggerdef_string(oid) This patch also adds a new function, pg_get_triggerdef_string, which returns trigger definitions formatted as a single-line without schema qualification. This is specifically designed for the psql \d command. postgres=# select pg_get_triggerdef_string(12345); pg_get_triggerdef_string -------------------------------------------------------------------------------------------------------------------------------------------- CREATE TRIGGER some_trig_foobar AFTER UPDATE ON some_t FOR EACH ROW WHEN ((NOT new.some_col)) EXECUTE FUNCTION dummy_update_func('foobar') (1 row) This change is due to the \d command's behavior, which shows a table's triggers without schema qualification. Prior to this patch, pg_get_triggerdef(oid, true) was used because its current behavior removed the schema name, but at the cost of being un-pretty. The new function allows \d to use a single-line output that lacks the schema name, while allowing the original function's 'pretty' output to correctly include the schema name and formatting. Here's an example: postgres=# \d main_table Table "public.main_table" Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------- a | integer | | | b | integer | | | Indexes: "main_table_a_key" UNIQUE CONSTRAINT, btree (a) Triggers: foofoo AFTER INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('foo_bar') foo_bar BEFORE INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('foo_bar') bar_fooAFTER DELETE ON main_table FOR EACH ROW WHEN ((old.a = 123)) EXECUTE FUNCTION trigger_func('foo_bar') Author: Phil Alger --- src/backend/utils/adt/ruleutils.c | 111 +++++++++++++++++-------- src/bin/psql/describe.c | 2 +- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/triggers.out | 58 +++++++++++-- src/test/regress/sql/triggers.sql | 8 ++ 5 files changed, 139 insertions(+), 43 deletions(-) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 79ec136231..89b1c57e8e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -352,7 +352,7 @@ static char *deparse_expression_pretty(Node *expr, List *dpcontext, int prettyFlags, int startIndent); static char *pg_get_viewdef_worker(Oid viewoid, int prettyFlags, int wrapColumn); -static char *pg_get_triggerdef_worker(Oid trigid, bool pretty); +static char *pg_get_triggerdef_worker(Oid trigid, int prettyFlags); static int decompile_column_index_array(Datum column_index_array, Oid relId, bool withPeriod, StringInfo buf); static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags); @@ -873,7 +873,7 @@ pg_get_triggerdef(PG_FUNCTION_ARGS) Oid trigid = PG_GETARG_OID(0); char *res; - res = pg_get_triggerdef_worker(trigid, false); + res = pg_get_triggerdef_worker(trigid, 0); if (res == NULL) PG_RETURN_NULL(); @@ -886,9 +886,34 @@ pg_get_triggerdef_ext(PG_FUNCTION_ARGS) { Oid trigid = PG_GETARG_OID(0); bool pretty = PG_GETARG_BOOL(1); + int prettyFlags; + char *res; + + prettyFlags = pretty ? GET_PRETTY_FLAGS(pretty) : 0; + + res = pg_get_triggerdef_worker(trigid, prettyFlags); + + if (res == NULL) + PG_RETURN_NULL(); + + PG_RETURN_TEXT_P(string_to_text(res)); +} + +/* + * pg_get_triggerdef_string + * Returns trigger definition as a string formatted as a single-line + * without schema qualification designed for the psql \d command. + */ +Datum +pg_get_triggerdef_string(PG_FUNCTION_ARGS) +{ + Oid trigid = PG_GETARG_OID(0); + int prettyFlags; char *res; - res = pg_get_triggerdef_worker(trigid, pretty); + prettyFlags = PRETTYFLAG_SCHEMA; + + res = pg_get_triggerdef_worker(trigid, prettyFlags); if (res == NULL) PG_RETURN_NULL(); @@ -897,11 +922,12 @@ pg_get_triggerdef_ext(PG_FUNCTION_ARGS) } static char * -pg_get_triggerdef_worker(Oid trigid, bool pretty) +pg_get_triggerdef_worker(Oid trigid, int prettyFlags) { HeapTuple ht_trig; Form_pg_trigger trigrec; StringInfoData buf; + deparse_context context; Relation tgrel; ScanKeyData skey[1]; SysScanDesc tgscan; @@ -942,6 +968,12 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) */ initStringInfo(&buf); + context.buf = &buf; + context.prettyFlags = prettyFlags; + context.wrapColumn = WRAP_COLUMN_DEFAULT; + context.indentLevel = 0; + context.colNamesVisible = true; + tgname = NameStr(trigrec->tgname); appendStringInfo(&buf, "CREATE %sTRIGGER %s ", OidIsValid(trigrec->tgconstraint) ? "CONSTRAINT " : "", @@ -1007,23 +1039,27 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) * In non-pretty mode, always schema-qualify the target table name for * safety. In pretty mode, schema-qualify only if not visible. */ - appendStringInfo(&buf, " ON %s ", - pretty ? + appendContextKeyword(&context, " ON ", 0, 0, PRETTYINDENT_VAR - 1); + appendStringInfo(&buf, "%s", + (prettyFlags & PRETTYFLAG_SCHEMA) ? generate_relation_name(trigrec->tgrelid, NIL) : generate_qualified_relation_name(trigrec->tgrelid)); if (OidIsValid(trigrec->tgconstraint)) { if (OidIsValid(trigrec->tgconstrrelid)) - appendStringInfo(&buf, "FROM %s ", - generate_relation_name(trigrec->tgconstrrelid, NIL)); + { + appendContextKeyword(&context, " FROM ", 0, 0, PRETTYINDENT_VAR - 1); + appendStringInfoString(&buf, generate_relation_name(trigrec->tgconstrrelid, NIL)); + } if (!trigrec->tgdeferrable) - appendStringInfoString(&buf, "NOT "); - appendStringInfoString(&buf, "DEFERRABLE INITIALLY "); + appendContextKeyword(&context, " NOT DEFERRABLE INITIALLY ", 0, 0, PRETTYINDENT_VAR - 1); + else + appendContextKeyword(&context, " DEFERRABLE INITIALLY ", 0, 0, PRETTYINDENT_VAR - 1); if (trigrec->tginitdeferred) - appendStringInfoString(&buf, "DEFERRED "); + appendStringInfoString(&buf, "DEFERRED"); else - appendStringInfoString(&buf, "IMMEDIATE "); + appendStringInfoString(&buf, "IMMEDIATE"); } value = fastgetattr(ht_trig, Anum_pg_trigger_tgoldtable, @@ -1040,19 +1076,21 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) tgnewtable = NULL; if (tgoldtable != NULL || tgnewtable != NULL) { - appendStringInfoString(&buf, "REFERENCING "); + appendContextKeyword(&context, " REFERENCING", 0, 0, PRETTYINDENT_VAR - 1); if (tgoldtable != NULL) - appendStringInfo(&buf, "OLD TABLE AS %s ", + appendStringInfo(&buf, " OLD TABLE AS %s", quote_identifier(tgoldtable)); if (tgnewtable != NULL) - appendStringInfo(&buf, "NEW TABLE AS %s ", + appendStringInfo(&buf, " NEW TABLE AS %s", quote_identifier(tgnewtable)); } + appendContextKeyword(&context, " FOR EACH ", 0, 0, PRETTYINDENT_VAR - 1); + if (TRIGGER_FOR_ROW(trigrec->tgtype)) - appendStringInfoString(&buf, "FOR EACH ROW "); + appendStringInfoString(&buf, "ROW"); else - appendStringInfoString(&buf, "FOR EACH STATEMENT "); + appendStringInfoString(&buf, "STATEMENT"); /* If the trigger has a WHEN qualification, add that */ value = fastgetattr(ht_trig, Anum_pg_trigger_tgqual, @@ -1061,12 +1099,12 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) { Node *qual; char relkind; - deparse_context context; + deparse_context whenContext; deparse_namespace dpns; RangeTblEntry *oldrte; RangeTblEntry *newrte; - appendStringInfoString(&buf, "WHEN ("); + appendContextKeyword(&context, " WHEN (", 0, 0, PRETTYINDENT_VAR - 1); qual = stringToNode(TextDatumGetCString(value)); @@ -1105,26 +1143,27 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) set_simple_column_names(&dpns); /* Set up context with one-deep namespace stack */ - context.buf = &buf; - context.namespaces = list_make1(&dpns); - context.resultDesc = NULL; - context.targetList = NIL; - context.windowClause = NIL; - context.varprefix = true; - context.prettyFlags = GET_PRETTY_FLAGS(pretty); - context.wrapColumn = WRAP_COLUMN_DEFAULT; - context.indentLevel = PRETTYINDENT_STD; - context.colNamesVisible = true; - context.inGroupBy = false; - context.varInOrderBy = false; - context.appendparents = NULL; + whenContext.buf = &buf; + whenContext.namespaces = list_make1(&dpns); + whenContext.resultDesc = NULL; + whenContext.targetList = NIL; + whenContext.windowClause = NIL; + whenContext.varprefix = true; + whenContext.prettyFlags = prettyFlags; + whenContext.wrapColumn = WRAP_COLUMN_DEFAULT; + whenContext.indentLevel = PRETTYINDENT_STD; + whenContext.colNamesVisible = true; + whenContext.inGroupBy = false; + whenContext.varInOrderBy = false; + whenContext.appendparents = NULL; + + get_rule_expr(qual, &whenContext, false); - get_rule_expr(qual, &context, false); - - appendStringInfoString(&buf, ") "); + appendStringInfoChar(&buf, ')'); } - appendStringInfo(&buf, "EXECUTE FUNCTION %s(", + appendContextKeyword(&context, " EXECUTE FUNCTION ", 0, 0, PRETTYINDENT_VAR - 1); + appendStringInfo(&buf, "%s(", generate_function_name(trigrec->tgfoid, 0, NIL, NULL, false, NULL, false)); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 36f2450284..daaeaf3a6b 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -3251,7 +3251,7 @@ describeOneTableDetails(const char *schemaname, printfPQExpBuffer(&buf, "SELECT t.tgname, " - "pg_catalog.pg_get_triggerdef(t.oid, true), " + "pg_catalog.pg_get_triggerdef_string(t.oid), " "t.tgenabled, t.tgisinternal,\n"); /* diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 9121a382f7..a4c1f61bb0 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8555,6 +8555,9 @@ { oid => '2730', descr => 'trigger description with pretty-print option', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid bool', prosrc => 'pg_get_triggerdef_ext' }, +{ oid => '9750', descr => 'trigger description as a single-line string', + proname => 'pg_get_triggerdef_string', provolatile => 's', prorettype => 'text', + proargtypes => 'oid', prosrc => 'pg_get_triggerdef_string' }, # asynchronous notifications { oid => '3035', diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 1eb8fba095..774d9f1405 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -299,9 +299,13 @@ SELECT * FROM main_table ORDER BY a, b; (8 rows) SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_a'; - pg_get_triggerdef -------------------------------------------------------------------------------------------------------------------------------------------- - CREATE TRIGGER modified_a BEFORE UPDATE OF a ON main_table FOR EACH ROW WHEN (old.a <> new.a) EXECUTE FUNCTION trigger_func('modified_a') + pg_get_triggerdef +------------------------------------------------- + CREATE TRIGGER modified_a BEFORE UPDATE OF a + + ON main_table + + FOR EACH ROW + + WHEN (old.a <> new.a) + + EXECUTE FUNCTION trigger_func('modified_a') (1 row) SELECT pg_get_triggerdef(oid, false) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_a'; @@ -311,9 +315,13 @@ SELECT pg_get_triggerdef(oid, false) FROM pg_trigger WHERE tgrelid = 'main_table (1 row) SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_any'; - pg_get_triggerdef -------------------------------------------------------------------------------------------------------------------------------------------------------------- - CREATE TRIGGER modified_any BEFORE UPDATE OF a ON main_table FOR EACH ROW WHEN (old.* IS DISTINCT FROM new.*) EXECUTE FUNCTION trigger_func('modified_any') + pg_get_triggerdef +--------------------------------------------------- + CREATE TRIGGER modified_any BEFORE UPDATE OF a + + ON main_table + + FOR EACH ROW + + WHEN (old.* IS DISTINCT FROM new.*) + + EXECUTE FUNCTION trigger_func('modified_any') (1 row) -- Test RENAME TRIGGER @@ -364,6 +372,15 @@ SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regc CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b ON public.main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row') (1 row) +SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig'; + pg_get_triggerdef +------------------------------------------------------------ + CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b+ + ON main_table + + FOR EACH ROW + + EXECUTE FUNCTION trigger_func('after_upd_a_b_row') +(1 row) + UPDATE main_table SET a = 50; NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW @@ -2318,6 +2335,24 @@ create constraint trigger parted_trig_two after insert on parted_constr deferrable initially deferred enforced for each row when (bark(new.b) AND new.a % 2 = 1) execute procedure trigger_notice_ab(); +-- Test that the constraint trigger is formatted correctly +SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'parted_constr'::regclass and tgname = 'parted_trig_two'; + pg_get_triggerdef +-------------------------------------------------------- + CREATE CONSTRAINT TRIGGER parted_trig_two AFTER INSERT+ + ON parted_constr + + DEFERRABLE INITIALLY DEFERRED + + FOR EACH ROW + + WHEN (bark(new.b) AND (new.a % 2) = 1) + + EXECUTE FUNCTION trigger_notice_ab() +(1 row) + +SELECT pg_get_triggerdef(oid, false) FROM pg_trigger WHERE tgrelid = 'parted_constr'::regclass and tgname = 'parted_trig_two'; + pg_get_triggerdef +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE CONSTRAINT TRIGGER parted_trig_two AFTER INSERT ON public.parted_constr DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN ((bark(new.b) AND ((new.a % 2) = 1))) EXECUTE FUNCTION trigger_notice_ab() +(1 row) + -- The immediate constraint is fired immediately; the WHEN clause of the -- deferred constraint is also called immediately. The deferred constraint -- is fired at commit time. @@ -2739,6 +2774,17 @@ SELECT trigger_name, event_manipulation, event_object_schema, event_object_table parent_update_trig | UPDATE | public | parent | 1 | | STATEMENT | AFTER | old_table | new_table (12 rows) +-- Test that the trigger is formatted correctly +SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'child3'::regclass AND tgname = 'child3_insert_trig'; + pg_get_triggerdef +------------------------------------------------ + CREATE TRIGGER child3_insert_trig AFTER INSERT+ + ON child3 + + REFERENCING NEW TABLE AS new_table + + FOR EACH STATEMENT + + EXECUTE FUNCTION dump_insert() +(1 row) + -- insert directly into children sees respective child-format tuples insert into child1 values ('AAA', 42); NOTICE: trigger = child1_insert_trig, new table = (AAA,42) diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 5f7f75d7ba..ad71c91d22 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -230,6 +230,7 @@ CREATE TRIGGER after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_upd_b_stmt'); SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig'; +SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig'; UPDATE main_table SET a = 50; UPDATE main_table SET b = 10; @@ -1608,6 +1609,10 @@ create constraint trigger parted_trig_two after insert on parted_constr for each row when (bark(new.b) AND new.a % 2 = 1) execute procedure trigger_notice_ab(); +-- Test that the constraint trigger is formatted correctly +SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'parted_constr'::regclass and tgname = 'parted_trig_two'; +SELECT pg_get_triggerdef(oid, false) FROM pg_trigger WHERE tgrelid = 'parted_constr'::regclass and tgname = 'parted_trig_two'; + -- The immediate constraint is fired immediately; the WHEN clause of the -- deferred constraint is also called immediately. The deferred constraint -- is fired at commit time. @@ -1907,6 +1912,9 @@ SELECT trigger_name, event_manipulation, event_object_schema, event_object_table WHERE event_object_table IN ('parent', 'child1', 'child2', 'child3') ORDER BY trigger_name COLLATE "C", 2; +-- Test that the trigger is formatted correctly +SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'child3'::regclass AND tgname = 'child3_insert_trig'; + -- insert directly into children sees respective child-format tuples insert into child1 values ('AAA', 42); insert into child2 values ('BBB', 42); -- 2.50.1 (Apple Git-155)