Skip to content

Commit 5aa19ef

Browse files
craig[bot]rafiss
craig[bot]
andcommitted
Merge #147248
147248: sql: populate pg_catalog.pg_trigger r=rafiss a=rafiss This commit populates the pg_catalog.pg_trigger virtual table, which provides metadata about database triggers. The implementation includes: - Correct calculation of the tgtype bitmap field according to PostgreSQL's specification, where bits represent: - Bit 0: FOR EACH ROW (1) or FOR EACH STATEMENT (0) - Bit 1: BEFORE timing - Bits 2-4: Event types (INSERT=4, DELETE=8, UPDATE=16) - Bit 5: TRUNCATE (32) - reserved for future use - Bit 6: INSTEAD OF (64) - reserved for future use - Support for trigger function arguments stored in tgargs as a null-separated bytea array. - WHEN clause conditions stored in tgqual The tgenabled field always returns 'A' (always enabled) as CockroachDB does not yet support trigger enable/disable states. Tests are added to verify the pg_trigger contents, including the tgtype bitmap calculation and triggers with function arguments. informs #143534 Epic: CRDB-42942 Release note (sql change): The pg_catalog.pg_trigger table now returns metadata about database triggers. Co-authored-by: Rafi Shamim <[email protected]>
2 parents 421cc69 + 1a88b92 commit 5aa19ef

File tree

5 files changed

+283
-33
lines changed

5 files changed

+283
-33
lines changed

pkg/ccl/logictestccl/testdata/logic_test/triggers

Lines changed: 154 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4501,12 +4501,12 @@ DROP FUNCTION f1;
45014501
subtest end
45024502

45034503
# ==============================================================================
4504-
# Test information_schema.triggers
4504+
# Test information_schema.triggers and pg_catalog.pg_trigger
45054505
# ==============================================================================
45064506

4507-
subtest information_schema_triggers
4507+
subtest trigger_introspection
45084508

4509-
# Create test tables and trigger functions
4509+
# Create test tables and trigger functions.
45104510
statement ok
45114511
CREATE TABLE test_triggers (
45124512
id INT PRIMARY KEY,
@@ -4601,27 +4601,40 @@ CREATE FUNCTION trigger_func3() RETURNS TRIGGER AS $$
46014601
END;
46024602
$$ LANGUAGE PLpgSQL;
46034603

4604-
# TODO(sql-queries): Enable tests for statement-level triggers when supported.
4605-
# statement ok
4606-
# CREATE TRIGGER after_update_transition
4607-
# AFTER UPDATE ON test_triggers
4608-
# REFERENCING OLD TABLE AS old_data NEW TABLE AS new_data
4609-
# FOR EACH STATEMENT
4610-
# EXECUTE FUNCTION trigger_func3();
4611-
4612-
# TODO(sql-queries): Enable tests for statement-level triggers when supported.
4613-
# query TTTTT colnames
4614-
# SELECT
4615-
# trigger_name,
4616-
# action_orientation,
4617-
# action_reference_old_table,
4618-
# action_reference_new_table,
4619-
# action_reference_old_row
4620-
# FROM information_schema.triggers
4621-
# WHERE trigger_name = 'after_update_transition';
4622-
# ----
4604+
# TODO(#126362, #135655): Enable tests for statement-level triggers when supported.
4605+
statement error statement-level triggers are not yet supported
4606+
CREATE TRIGGER after_update_transition
4607+
AFTER UPDATE ON test_triggers
4608+
REFERENCING OLD TABLE AS old_data NEW TABLE AS new_data
4609+
FOR EACH STATEMENT
4610+
EXECUTE FUNCTION trigger_func3();
4611+
4612+
# TODO(#126362, #135655): Enable tests for statement-level triggers when supported.
4613+
# The expected results are:
46234614
# trigger_name action_orientation action_reference_old_table action_reference_new_table action_reference_old_row
46244615
# after_update_transition STATEMENT old_data new_data NULL
4616+
query TTTTT colnames
4617+
SELECT
4618+
trigger_name,
4619+
action_orientation,
4620+
action_reference_old_table,
4621+
action_reference_new_table,
4622+
action_reference_old_row
4623+
FROM information_schema.triggers
4624+
WHERE trigger_name = 'after_update_transition';
4625+
----
4626+
trigger_name action_orientation action_reference_old_table action_reference_new_table action_reference_old_row
4627+
4628+
# TODO(#126362, #135655): Populate tgoldtable and tgnewtable.
4629+
# The expected results are:
4630+
# tgname tgoldtable tgnewtable
4631+
# after_update_transition old_data new_data
4632+
query TTT colnames
4633+
SELECT tgname, tgoldtable, tgnewtable
4634+
FROM pg_catalog.pg_trigger
4635+
WHERE tgname = 'after_update_transition';
4636+
----
4637+
tgname tgoldtable tgnewtable
46254638

46264639
# Test multiple event types (create a trigger for multiple events).
46274640
statement ok
@@ -4671,6 +4684,125 @@ trigger_schema trigger_count
46714684
other_schema 1
46724685
public 7
46734686

4687+
# Create a trigger with arguments to test tgnargs and tgargs.
4688+
statement ok
4689+
CREATE TRIGGER trigger_with_args
4690+
AFTER INSERT ON test_triggers
4691+
FOR EACH ROW
4692+
EXECUTE FUNCTION trigger_func1('arg1', 'arg2', 'test value with spaces');
4693+
4694+
# Test pg_catalog.pg_trigger.
4695+
query TIBITTT colnames
4696+
SELECT
4697+
tgname,
4698+
tgtype,
4699+
tgfoid > 0 AS has_func_oid,
4700+
tgnargs,
4701+
tgenabled,
4702+
tgoldtable,
4703+
tgnewtable
4704+
FROM pg_catalog.pg_trigger
4705+
WHERE tgrelid = 'test_triggers'::regclass
4706+
ORDER BY tgname;
4707+
----
4708+
tgname tgtype has_func_oid tgnargs tgenabled tgoldtable tgnewtable
4709+
after_delete_row 9 true 0 A NULL NULL
4710+
after_insert_row 5 true 0 A NULL NULL
4711+
before_update_row 19 true 0 A NULL NULL
4712+
multi_event_trigger 31 true 0 A NULL NULL
4713+
trigger_with_args 5 true 3 A NULL NULL
4714+
4715+
# Test INSTEAD OF triggers (not yet implemented)
4716+
statement error pgcode 42809 "test_triggers" is a table\nDETAIL: Tables cannot have INSTEAD OF triggers.
4717+
CREATE TRIGGER instead_of_trigger INSTEAD OF INSERT ON test_triggers
4718+
FOR EACH ROW EXECUTE FUNCTION trigger_func1();
4719+
4720+
statement error pgcode 42809 "test_triggers" is a table\nDETAIL: Tables cannot have INSTEAD OF triggers.
4721+
CREATE TRIGGER instead_of_update_trigger INSTEAD OF UPDATE ON test_triggers
4722+
FOR EACH ROW EXECUTE FUNCTION trigger_func1();
4723+
4724+
statement error pgcode 42809 "test_triggers" is a table\nDETAIL: Tables cannot have INSTEAD OF triggers.
4725+
CREATE TRIGGER instead_of_delete_trigger INSTEAD OF DELETE ON test_triggers
4726+
FOR EACH ROW EXECUTE FUNCTION trigger_func1();
4727+
4728+
# Test TRUNCATE triggers (not yet implemented)
4729+
statement error statement-level triggers are not yet supported
4730+
CREATE TRIGGER truncate_trigger AFTER TRUNCATE ON test_triggers
4731+
FOR EACH STATEMENT EXECUTE FUNCTION trigger_func1();
4732+
4733+
statement error statement-level triggers are not yet supported
4734+
CREATE TRIGGER before_truncate_trigger BEFORE TRUNCATE ON test_triggers
4735+
FOR EACH STATEMENT EXECUTE FUNCTION trigger_func1();
4736+
4737+
# Test tgtype bitmap calculation
4738+
# Bit 0: FOR EACH ROW (1) or FOR EACH STATEMENT (0)
4739+
# Bit 1: BEFORE (2) or AFTER (0)
4740+
# Bits 2-4: INSERT (4), DELETE (8), UPDATE (16)
4741+
# TODO(#126363, #135657): Add tests for INSTEAD OF and TRUNCATE triggers here when supported.
4742+
query TBBBBBBB colnames
4743+
SELECT
4744+
tgname,
4745+
(tgtype::INT & 1) > 0 AS is_row_level,
4746+
(tgtype::INT & 2) > 0 AS is_before,
4747+
(tgtype::INT & 4) > 0 AS has_insert,
4748+
(tgtype::INT & 8) > 0 AS has_delete,
4749+
(tgtype::INT & 16) > 0 AS has_update,
4750+
(tgtype::INT & 32) > 0 AS has_instead,
4751+
(tgtype::INT & 64) > 0 AS has_truncate
4752+
FROM pg_catalog.pg_trigger
4753+
WHERE tgrelid = 'test_triggers'::regclass
4754+
ORDER BY tgname;
4755+
----
4756+
tgname is_row_level is_before has_insert has_delete has_update has_instead has_truncate
4757+
after_delete_row true false false true false false false
4758+
after_insert_row true false true false false false false
4759+
before_update_row true true false false true false false
4760+
multi_event_trigger true true true true true false false
4761+
trigger_with_args true false true false false false false
4762+
4763+
# Check trigger arguments and column attributes.
4764+
# TODO(#135656): Populate the tgattr column.
4765+
query TITT colnames
4766+
SELECT
4767+
tgname,
4768+
tgnargs,
4769+
encode(tgargs, 'escape') AS tgargs,
4770+
tgattr
4771+
FROM pg_catalog.pg_trigger
4772+
WHERE tgrelid = 'test_triggers'::regclass
4773+
ORDER BY tgname;
4774+
----
4775+
tgname tgnargs tgargs tgattr
4776+
after_delete_row 0 · ·
4777+
after_insert_row 0 · ·
4778+
before_update_row 0 · ·
4779+
multi_event_trigger 0 · ·
4780+
trigger_with_args 3 arg1\000arg2\000test value with spaces\000 ·
4781+
4782+
# Test trigger with WHEN condition.
4783+
query TTT colnames
4784+
SELECT
4785+
tgname,
4786+
tgqual,
4787+
tgattr
4788+
FROM pg_catalog.pg_trigger
4789+
WHERE tgrelid = 'test_triggers'::regclass AND tgqual IS NOT NULL;
4790+
----
4791+
tgname tgqual tgattr
4792+
before_update_row ((new).value > 100) ·
4793+
4794+
# Test cross-schema trigger.
4795+
query TTT colnames
4796+
SELECT
4797+
tgname,
4798+
tgrelid::regclass::text AS table_name,
4799+
tgfoid::regproc::text AS trigger_func_name
4800+
FROM pg_catalog.pg_trigger
4801+
WHERE tgrelid = 'other_schema.test_table'::regclass;
4802+
----
4803+
tgname table_name trigger_func_name
4804+
other_schema_trigger test_table trigger_func1
4805+
46744806
# Clean up
46754807
statement ok
46764808
DROP TABLE test_triggers CASCADE;

pkg/cli/clisqlshell/testdata/describe

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,8 +655,8 @@ https://www.postgresql.org/docs/9.5/catalog-pg-tablespace.html"
655655
pg_catalog,pg_timezone_abbrevs,table,node,permanent,prefix,pg_timezone_abbrevs was created for compatibility and is currently unimplemented
656656
pg_catalog,pg_timezone_names,table,node,permanent,prefix,pg_timezone_names lists all the timezones that are supported by SET timezone
657657
pg_catalog,pg_transform,table,node,permanent,prefix,pg_transform was created for compatibility and is currently unimplemented
658-
pg_catalog,pg_trigger,table,node,permanent,prefix,"triggers (empty - feature does not exist)
659-
https://www.postgresql.org/docs/9.5/catalog-pg-trigger.html"
658+
pg_catalog,pg_trigger,table,node,permanent,prefix,"trigger definitions
659+
https://www.postgresql.org/docs/16/catalog-pg-trigger.html"
660660
pg_catalog,pg_ts_config,table,node,permanent,prefix,pg_ts_config was created for compatibility and is currently unimplemented
661661
pg_catalog,pg_ts_config_map,table,node,permanent,prefix,pg_ts_config_map was created for compatibility and is currently unimplemented
662662
pg_catalog,pg_ts_dict,table,node,permanent,prefix,pg_ts_dict was created for compatibility and is currently unimplemented

pkg/sql/logictest/testdata/logic_test/crdb_internal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ pg_tablespace false
196196
pg_timezone_abbrevs true
197197
pg_timezone_names false
198198
pg_transform true
199-
pg_trigger true
199+
pg_trigger false
200200
pg_ts_config true
201201
pg_ts_config_map true
202202
pg_ts_dict true

pkg/sql/pg_catalog.go

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3302,14 +3302,124 @@ https://www.postgresql.org/docs/9.5/catalog-pg-tablespace.html`,
33023302
}
33033303

33043304
var pgCatalogTriggerTable = virtualSchemaTable{
3305-
comment: `triggers (empty - feature does not exist)
3306-
https://www.postgresql.org/docs/9.5/catalog-pg-trigger.html`,
3305+
comment: `trigger definitions
3306+
https://www.postgresql.org/docs/16/catalog-pg-trigger.html`,
33073307
schema: vtable.PGCatalogTrigger,
33083308
populate: func(ctx context.Context, p *planner, dbContext catalog.DatabaseDescriptor, addRow func(...tree.Datum) error) error {
3309-
// Triggers are unsupported.
3310-
return nil
3309+
h := makeOidHasher()
3310+
opts := forEachTableDescOptions{virtualOpts: hideVirtual} /* virtual schemas have no triggers */
3311+
return forEachTableDesc(ctx, p, dbContext, opts,
3312+
func(ctx context.Context, descCtx tableDescContext) error {
3313+
tableOid := tableOid(descCtx.table.GetID())
3314+
3315+
triggers := descCtx.table.GetTriggers()
3316+
for i := range triggers {
3317+
trigger := &triggers[i]
3318+
triggerOid := h.TriggerOid(descCtx.table.GetID(), trigger.ID)
3319+
3320+
// Calculate tgtype bitmap. The bits are defined in Postgres source:
3321+
// https://github.com/postgres/postgres/blob/44ce4e1593b1821005b29ffaa19d9cbdd80747b2/src/include/catalog/pg_trigger.h#L92-L99
3322+
// Bit 0: FOR EACH ROW (set) or FOR EACH STATEMENT (unset)
3323+
// Bit 1: BEFORE (set) or AFTER (unset)
3324+
// Bits 2-4: One bit for each event type (INSERT=4, DELETE=8, UPDATE=16)
3325+
// Bit 5: TRUNCATE (64)
3326+
// Bit 6: INSTEAD OF (128)
3327+
const tgtypeRow = 1
3328+
const tgtypeBefore = 1 << 1
3329+
const tgtypeInsert = 1 << 2
3330+
const tgtypeDelete = 1 << 3
3331+
const tgtypeUpdate = 1 << 4
3332+
const tgtypeTruncate = 1 << 5
3333+
const tgtypeInstead = 1 << 6
3334+
3335+
tgtype := int16(0)
3336+
3337+
// Timing bits.
3338+
switch trigger.ActionTime {
3339+
case semenumpb.TriggerActionTime_BEFORE:
3340+
tgtype |= tgtypeBefore
3341+
case semenumpb.TriggerActionTime_INSTEAD_OF:
3342+
tgtype |= tgtypeInstead
3343+
}
3344+
3345+
// Row/Statement bit.
3346+
if trigger.ForEachRow {
3347+
tgtype |= tgtypeRow
3348+
}
3349+
3350+
// Event type bits.
3351+
for _, event := range trigger.Events {
3352+
switch event.Type {
3353+
case semenumpb.TriggerEventType_INSERT:
3354+
tgtype |= tgtypeInsert
3355+
case semenumpb.TriggerEventType_DELETE:
3356+
tgtype |= tgtypeDelete
3357+
case semenumpb.TriggerEventType_UPDATE:
3358+
tgtype |= tgtypeUpdate
3359+
case semenumpb.TriggerEventType_TRUNCATE:
3360+
tgtype |= tgtypeTruncate
3361+
}
3362+
}
3363+
3364+
// tgenabled: O = origin and local, D = disabled, R = replica, A = always
3365+
tgenabled := tree.NewDString("A")
3366+
if !trigger.Enabled {
3367+
tgenabled = tree.NewDString("D")
3368+
}
3369+
3370+
// tgargs: Function arguments as a bytea array.
3371+
// Format: arg1\000arg2\000...argN\000
3372+
var tgargs []byte
3373+
for _, arg := range trigger.FuncArgs {
3374+
tgargs = append(tgargs, []byte(arg)...)
3375+
tgargs = append(tgargs, 0) // null terminator
3376+
}
3377+
3378+
// tgattr: Column numbers for UPDATE OF - not implemented.
3379+
// TODO(#135656): Implement UPDATE OF column list for triggers.
3380+
tgattr := tree.NewDIntVectorFromDArray(tree.NewDArray(types.Int2))
3381+
// tgoldtable/tgnewtable: Transition table names.
3382+
var oldTableName, newTableName tree.Datum = tree.DNull, tree.DNull
3383+
if trigger.OldTransitionAlias != "" {
3384+
oldTableName = tree.NewDName(trigger.OldTransitionAlias)
3385+
}
3386+
if trigger.NewTransitionAlias != "" {
3387+
newTableName = tree.NewDName(trigger.NewTransitionAlias)
3388+
}
3389+
3390+
// tgqual: WHEN condition expression (internal format).
3391+
var tgqual tree.Datum = tree.DNull
3392+
if trigger.WhenExpr != "" {
3393+
tgqual = tree.NewDString(trigger.WhenExpr)
3394+
}
3395+
3396+
if err := addRow(
3397+
triggerOid, // oid
3398+
tableOid, // tgrelid
3399+
oidZero, // tgparentid (partitioning not supported)
3400+
tree.NewDName(trigger.Name), // tgname
3401+
tree.NewDOid(catid.FuncIDToOID(trigger.FuncID)), // tgfoid
3402+
tree.NewDInt(tree.DInt(tgtype)), // tgtype
3403+
tgenabled, // tgenabled
3404+
tree.DBoolFalse, // tgisinternal
3405+
oidZero, // tgconstrrelid (foreign key table)
3406+
oidZero, // tgconstrindid (constraint index)
3407+
oidZero, // tgconstraint (constraint oid)
3408+
tree.DBoolFalse, // tgdeferrable
3409+
tree.DBoolFalse, // tginitdeferred
3410+
tree.NewDInt(tree.DInt(len(trigger.FuncArgs))), // tgnargs
3411+
tgattr, // tgattr
3412+
tree.NewDBytes(tree.DBytes(tgargs)), // tgargs
3413+
tgqual, // tgqual
3414+
oldTableName, // tgoldtable
3415+
newTableName, // tgnewtable
3416+
); err != nil {
3417+
return err
3418+
}
3419+
}
3420+
return nil
3421+
})
33113422
},
3312-
unimplemented: true,
33133423
}
33143424

33153425
var (
@@ -5227,6 +5337,7 @@ const (
52275337
rewriteTypeTag
52285338
dbSchemaRoleTypeTag
52295339
castTypeTag
5340+
triggerTypeTag
52305341
)
52315342

52325343
func (h oidHasher) writeTypeTag(tag oidTypeTag) {
@@ -5426,6 +5537,13 @@ func (h oidHasher) CastOid(srcID oid.Oid, tgtID oid.Oid) *tree.DOid {
54265537
return h.getOid()
54275538
}
54285539

5540+
func (h oidHasher) TriggerOid(tableID descpb.ID, triggerID descpb.TriggerID) *tree.DOid {
5541+
h.writeTypeTag(triggerTypeTag)
5542+
h.writeTable(tableID)
5543+
h.writeUInt32(uint32(triggerID))
5544+
return h.getOid()
5545+
}
5546+
54295547
func funcVolatility(v catpb.Function_Volatility) string {
54305548
switch v {
54315549
case catpb.Function_IMMUTABLE:

pkg/sql/vtable/pg_catalog.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -759,11 +759,12 @@ CREATE TABLE pg_catalog.pg_tablespace (
759759
)`
760760

761761
// PGCatalogTrigger describes the schema of the pg_catalog.pg_trigger table.
762-
// https://www.postgresql.org/docs/9.5/catalog-pg-trigger.html,
762+
// https://www.postgresql.org/docs/16/catalog-pg-trigger.html
763763
const PGCatalogTrigger = `
764764
CREATE TABLE pg_catalog.pg_trigger (
765765
oid OID,
766766
tgrelid OID,
767+
tgparentid OID,
767768
tgname NAME,
768769
tgfoid OID,
769770
tgtype INT2,
@@ -779,8 +780,7 @@ CREATE TABLE pg_catalog.pg_trigger (
779780
tgargs BYTEA,
780781
tgqual TEXT,
781782
tgoldtable NAME,
782-
tgnewtable NAME,
783-
tgparentid OID
783+
tgnewtable NAME
784784
)`
785785

786786
// PGCatalogType describes the schema of the pg_catalog.pg_type table.

0 commit comments

Comments
 (0)