Skip to content

Commit 8f9eef0

Browse files
committed
sql: populate pg_catalog.pg_trigger
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. Fixes cockroachdb#143534 Release note (sql change): The pg_catalog.pg_trigger table now returns metadata about database triggers.
1 parent faeec16 commit 8f9eef0

File tree

4 files changed

+221
-15
lines changed

4 files changed

+221
-15
lines changed

pkg/ccl/logictestccl/testdata/logic_test/triggers

Lines changed: 98 additions & 5 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,15 +4601,15 @@ 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.
4604+
# TODO(#126362, #135655): Enable tests for statement-level triggers when supported.
46054605
# statement ok
46064606
# CREATE TRIGGER after_update_transition
46074607
# AFTER UPDATE ON test_triggers
46084608
# REFERENCING OLD TABLE AS old_data NEW TABLE AS new_data
46094609
# FOR EACH STATEMENT
46104610
# EXECUTE FUNCTION trigger_func3();
46114611

4612-
# TODO(sql-queries): Enable tests for statement-level triggers when supported.
4612+
# TODO(#126362, #135655): Enable tests for statement-level triggers when supported.
46134613
# query TTTTT colnames
46144614
# SELECT
46154615
# trigger_name,
@@ -4671,6 +4671,99 @@ trigger_schema trigger_count
46714671
other_schema 1
46724672
public 7
46734673

4674+
# Create a trigger with arguments to test tgnargs and tgargs.
4675+
statement ok
4676+
CREATE TRIGGER trigger_with_args
4677+
AFTER INSERT ON test_triggers
4678+
FOR EACH ROW
4679+
EXECUTE FUNCTION trigger_func1('arg1', 'arg2', 'test value with spaces');
4680+
4681+
# Test pg_catalog.pg_trigger.
4682+
query TIBITTT colnames
4683+
SELECT
4684+
tgname,
4685+
tgtype,
4686+
tgfoid > 0 AS has_func_oid,
4687+
tgnargs,
4688+
tgenabled,
4689+
tgoldtable,
4690+
tgnewtable
4691+
FROM pg_catalog.pg_trigger
4692+
WHERE tgrelid = 'test_triggers'::regclass
4693+
ORDER BY tgname;
4694+
----
4695+
tgname tgtype has_func_oid tgnargs tgenabled tgoldtable tgnewtable
4696+
after_delete_row 9 true 0 A NULL NULL
4697+
after_insert_row 5 true 0 A NULL NULL
4698+
before_update_row 19 true 0 A NULL NULL
4699+
multi_event_trigger 31 true 0 A NULL NULL
4700+
trigger_with_args 5 true 3 A NULL NULL
4701+
4702+
# Test tgtype bitmap calculation
4703+
# Bit 0: FOR EACH ROW (1) or FOR EACH STATEMENT (0)
4704+
# Bit 1: BEFORE (2) or AFTER (0)
4705+
# Bits 2-4: INSERT (4), DELETE (8), UPDATE (16)
4706+
# TODO(#126363, #135657): Add tests for INSTEAD OF and TRUNCATE triggers here when supported.
4707+
query TBBBBB colnames
4708+
SELECT
4709+
tgname,
4710+
(tgtype::INT & 1) > 0 AS is_row_level,
4711+
(tgtype::INT & 2) > 0 AS is_before,
4712+
(tgtype::INT & 4) > 0 AS has_insert,
4713+
(tgtype::INT & 8) > 0 AS has_delete,
4714+
(tgtype::INT & 16) > 0 AS has_update
4715+
FROM pg_catalog.pg_trigger
4716+
WHERE tgrelid = 'test_triggers'::regclass
4717+
ORDER BY tgname;
4718+
----
4719+
tgname is_row_level is_before has_insert has_delete has_update
4720+
after_delete_row true false false true false
4721+
after_insert_row true false true false false
4722+
before_update_row true true false false true
4723+
multi_event_trigger true true true true true
4724+
trigger_with_args true false true false false
4725+
4726+
# Test trigger with arguments.
4727+
query TIT colnames
4728+
SELECT
4729+
tgname,
4730+
tgnargs,
4731+
encode(tgargs, 'escape') AS tgargs
4732+
FROM pg_catalog.pg_trigger
4733+
WHERE tgrelid = 'test_triggers'::regclass
4734+
ORDER BY tgname;
4735+
----
4736+
tgname tgnargs tgargs
4737+
after_delete_row 0 ·
4738+
after_insert_row 0 ·
4739+
before_update_row 0 ·
4740+
multi_event_trigger 0 ·
4741+
trigger_with_args 3 arg1\000arg2\000test value with spaces\000
4742+
4743+
# Test trigger with WHEN condition.
4744+
query TTT colnames
4745+
SELECT
4746+
tgname,
4747+
tgqual,
4748+
tgattr
4749+
FROM pg_catalog.pg_trigger
4750+
WHERE tgrelid = 'test_triggers'::regclass AND tgqual IS NOT NULL;
4751+
----
4752+
tgname tgqual tgattr
4753+
before_update_row ((new).value > 100) ·
4754+
4755+
# Test cross-schema trigger.
4756+
query TTT colnames
4757+
SELECT
4758+
tgname,
4759+
tgrelid::regclass::text AS table_name,
4760+
tgfoid::regproc::text AS trigger_func_name
4761+
FROM pg_catalog.pg_trigger
4762+
WHERE tgrelid = 'other_schema.test_table'::regclass;
4763+
----
4764+
tgname table_name trigger_func_name
4765+
other_schema_trigger test_table trigger_func1
4766+
46744767
# Clean up
46754768
statement ok
46764769
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/pg_catalog.go

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3302,14 +3302,127 @@ 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+
3319+
// Generate a unique OID for the trigger.
3320+
h.writeTable(descCtx.table.GetID())
3321+
h.writeUInt32(uint32(trigger.ID))
3322+
triggerOid := h.getOid()
3323+
3324+
// Calculate tgtype bitmap. The bits are defined in Postgres source:
3325+
// https://github.com/postgres/postgres/blob/44ce4e1593b1821005b29ffaa19d9cbdd80747b2/src/include/catalog/pg_trigger.h#L92-L99
3326+
// Bit 0: BEFORE (set) or AFTER (unset)
3327+
// Bit 1: FOR EACH ROW (set) or FOR EACH STATEMENT (unset)
3328+
// Bits 2-4: One bit for each event type (INSERT=4, DELETE=8, UPDATE=16)
3329+
// Bit 5: TRUNCATE (64)
3330+
// Bit 6: INSTEAD OF (128)
3331+
const tgtypeRow = 1
3332+
const tgtypeBefore = 1 << 1
3333+
const tgtypeInsert = 1 << 2
3334+
const tgtypeDelete = 1 << 3
3335+
const tgtypeUpdate = 1 << 4
3336+
const tgtypeTruncate = 1 << 5
3337+
const tgtypeInstead = 1 << 6
3338+
3339+
tgtype := int16(0)
3340+
3341+
// Timing bits.
3342+
switch trigger.ActionTime {
3343+
case semenumpb.TriggerActionTime_BEFORE:
3344+
tgtype |= tgtypeBefore
3345+
case semenumpb.TriggerActionTime_INSTEAD_OF:
3346+
tgtype |= tgtypeInstead
3347+
}
3348+
3349+
// Row/Statement bit.
3350+
if trigger.ForEachRow {
3351+
tgtype |= tgtypeRow
3352+
}
3353+
3354+
// Event type bits.
3355+
for _, event := range trigger.Events {
3356+
switch event.Type {
3357+
case semenumpb.TriggerEventType_INSERT:
3358+
tgtype |= tgtypeInsert
3359+
case semenumpb.TriggerEventType_DELETE:
3360+
tgtype |= tgtypeDelete
3361+
case semenumpb.TriggerEventType_UPDATE:
3362+
tgtype |= tgtypeUpdate
3363+
case semenumpb.TriggerEventType_TRUNCATE:
3364+
tgtype |= tgtypeTruncate
3365+
}
3366+
}
3367+
3368+
// tgenabled: O = origin and local, D = disabled, R = replica, A = always
3369+
tgenabled := tree.NewDString("A")
3370+
if !trigger.Enabled {
3371+
tgenabled = tree.NewDString("D")
3372+
}
3373+
3374+
// tgargs: Function arguments as a bytea array.
3375+
// Format: arg1\000arg2\000...argN\000
3376+
var tgargs []byte
3377+
for _, arg := range trigger.FuncArgs {
3378+
tgargs = append(tgargs, []byte(arg)...)
3379+
tgargs = append(tgargs, 0) // null terminator
3380+
}
3381+
3382+
// tgattr: Column numbers for UPDATE OF - not implemented.
3383+
tgattr := tree.NewDIntVectorFromDArray(tree.NewDArray(types.Int2))
3384+
// tgoldtable/tgnewtable: Transition table names.
3385+
var oldTableName, newTableName tree.Datum = tree.DNull, tree.DNull
3386+
if trigger.OldTransitionAlias != "" {
3387+
oldTableName = tree.NewDName(trigger.OldTransitionAlias)
3388+
}
3389+
if trigger.NewTransitionAlias != "" {
3390+
newTableName = tree.NewDName(trigger.NewTransitionAlias)
3391+
}
3392+
3393+
// tgqual: WHEN condition expression (internal format).
3394+
var tgqual tree.Datum = tree.DNull
3395+
if trigger.WhenExpr != "" {
3396+
tgqual = tree.NewDString(trigger.WhenExpr)
3397+
}
3398+
3399+
if err := addRow(
3400+
triggerOid, // oid
3401+
tableOid, // tgrelid
3402+
oidZero, // tgparentid (partitioning not supported)
3403+
tree.NewDName(trigger.Name), // tgname
3404+
tree.NewDOid(catid.FuncIDToOID(trigger.FuncID)), // tgfoid
3405+
tree.NewDInt(tree.DInt(tgtype)), // tgtype
3406+
tgenabled, // tgenabled
3407+
tree.DBoolFalse, // tgisinternal
3408+
oidZero, // tgconstrrelid (foreign key table)
3409+
oidZero, // tgconstrindid (constraint index)
3410+
oidZero, // tgconstraint (constraint oid)
3411+
tree.DBoolFalse, // tgdeferrable
3412+
tree.DBoolFalse, // tginitdeferred
3413+
tree.NewDInt(tree.DInt(len(trigger.FuncArgs))), // tgnargs
3414+
tgattr, // tgattr
3415+
tree.NewDBytes(tree.DBytes(tgargs)), // tgargs
3416+
tgqual, // tgqual
3417+
oldTableName, // tgoldtable
3418+
newTableName, // tgnewtable
3419+
); err != nil {
3420+
return err
3421+
}
3422+
}
3423+
return nil
3424+
})
33113425
},
3312-
unimplemented: true,
33133426
}
33143427

33153428
var (

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://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)