Skip to content

Commit 4ed111a

Browse files
committed
sql: populate information_schema.triggers
Previously, the information_schema.triggers table was unimplemented and always returned zero rows. This made it impossible to query trigger metadata through standard SQL information schema views. This change implements the populate function for information_schema.triggers, allowing users to query trigger information. The implementation: Note that transition tables (action_reference_old_table/new_table) will only be populated once statement-level triggers are supported. Currently, these columns return NULL for row-level triggers. Fixes #143534 Release note (sql change): The information_schema.triggers table is now populated with trigger metadata. Users can query this table to see all triggers defined in their database, including the trigger name, timing (BEFORE/AFTER), event type (INSERT/UPDATE/DELETE), and associated function. Each trigger event appears as a separate row in the table.
1 parent 0ef38b1 commit 4ed111a

File tree

2 files changed

+287
-5
lines changed

2 files changed

+287
-5
lines changed

pkg/ccl/logictestccl/testdata/logic_test/triggers

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4499,3 +4499,192 @@ statement ok
44994499
DROP FUNCTION f1;
45004500

45014501
subtest end
4502+
4503+
# ==============================================================================
4504+
# Test information_schema.triggers
4505+
# ==============================================================================
4506+
4507+
subtest information_schema_triggers
4508+
4509+
# Create test tables and trigger functions
4510+
statement ok
4511+
CREATE TABLE test_triggers (
4512+
id INT PRIMARY KEY,
4513+
name TEXT,
4514+
value INT
4515+
);
4516+
4517+
statement ok
4518+
CREATE TABLE audit_log (
4519+
id SERIAL PRIMARY KEY,
4520+
table_name TEXT,
4521+
operation TEXT,
4522+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
4523+
);
4524+
4525+
statement ok
4526+
CREATE FUNCTION trigger_func1() RETURNS TRIGGER AS $$
4527+
BEGIN
4528+
INSERT INTO audit_log (table_name, operation)
4529+
VALUES (TG_TABLE_NAME, TG_OP);
4530+
RETURN NEW;
4531+
END;
4532+
$$ LANGUAGE PLpgSQL;
4533+
4534+
statement ok
4535+
CREATE FUNCTION trigger_func2() RETURNS TRIGGER AS $$
4536+
BEGIN
4537+
RAISE NOTICE 'Trigger fired';
4538+
RETURN NEW;
4539+
END;
4540+
$$ LANGUAGE PLpgSQL;
4541+
4542+
statement ok
4543+
CREATE TRIGGER after_insert_row
4544+
AFTER INSERT ON test_triggers
4545+
FOR EACH ROW
4546+
EXECUTE FUNCTION trigger_func1();
4547+
4548+
statement ok
4549+
CREATE TRIGGER before_update_row
4550+
BEFORE UPDATE ON test_triggers
4551+
FOR EACH ROW
4552+
WHEN ((NEW).value > 100)
4553+
EXECUTE FUNCTION trigger_func2();
4554+
4555+
statement ok
4556+
CREATE TRIGGER after_delete_row
4557+
AFTER DELETE ON test_triggers
4558+
FOR EACH ROW
4559+
EXECUTE FUNCTION trigger_func1();
4560+
4561+
query TTTTTTITTTTT colnames
4562+
SELECT
4563+
trigger_catalog,
4564+
trigger_schema,
4565+
trigger_name,
4566+
event_manipulation,
4567+
event_object_schema,
4568+
event_object_table,
4569+
action_order,
4570+
action_orientation,
4571+
action_timing,
4572+
action_reference_old_table,
4573+
action_reference_new_table,
4574+
created
4575+
FROM information_schema.triggers
4576+
WHERE event_object_table = 'test_triggers'
4577+
ORDER BY trigger_name;
4578+
----
4579+
trigger_catalog trigger_schema trigger_name event_manipulation event_object_schema event_object_table action_order action_orientation action_timing action_reference_old_table action_reference_new_table created
4580+
test public after_delete_row DELETE public test_triggers NULL ROW AFTER NULL NULL NULL
4581+
test public after_insert_row INSERT public test_triggers NULL ROW AFTER NULL NULL NULL
4582+
test public before_update_row UPDATE public test_triggers NULL ROW BEFORE NULL NULL NULL
4583+
4584+
# Test trigger with WHEN condition.
4585+
query TTT colnames
4586+
SELECT
4587+
trigger_name,
4588+
action_condition,
4589+
action_statement
4590+
FROM information_schema.triggers
4591+
WHERE trigger_name = 'before_update_row';
4592+
----
4593+
trigger_name action_condition action_statement
4594+
before_update_row ((new).value > 100) EXECUTE FUNCTION trigger_func2()
4595+
4596+
# Create trigger with transition tables.
4597+
statement ok
4598+
CREATE FUNCTION trigger_func3() RETURNS TRIGGER AS $$
4599+
BEGIN
4600+
RETURN NULL;
4601+
END;
4602+
$$ LANGUAGE PLpgSQL;
4603+
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+
# ----
4623+
# trigger_name action_orientation action_reference_old_table action_reference_new_table action_reference_old_row
4624+
# after_update_transition STATEMENT old_data new_data NULL
4625+
4626+
# Test multiple event types (create a trigger for multiple events).
4627+
statement ok
4628+
CREATE TRIGGER multi_event_trigger
4629+
BEFORE INSERT OR UPDATE OR DELETE ON test_triggers
4630+
FOR EACH ROW
4631+
EXECUTE FUNCTION trigger_func1();
4632+
4633+
# Each event should appear as a separate row.
4634+
query TTT colnames
4635+
SELECT
4636+
trigger_name,
4637+
event_manipulation,
4638+
action_timing
4639+
FROM information_schema.triggers
4640+
WHERE trigger_name = 'multi_event_trigger'
4641+
ORDER BY event_manipulation;
4642+
----
4643+
trigger_name event_manipulation action_timing
4644+
multi_event_trigger DELETE BEFORE
4645+
multi_event_trigger INSERT BEFORE
4646+
multi_event_trigger UPDATE BEFORE
4647+
4648+
# Create a schema and add a trigger there.
4649+
statement ok
4650+
CREATE SCHEMA other_schema;
4651+
4652+
statement ok
4653+
CREATE TABLE other_schema.test_table (id INT PRIMARY KEY, data TEXT);
4654+
4655+
statement ok
4656+
CREATE TRIGGER other_schema_trigger
4657+
AFTER INSERT ON other_schema.test_table
4658+
FOR EACH ROW
4659+
EXECUTE FUNCTION trigger_func1();
4660+
4661+
query TI colnames
4662+
SELECT
4663+
trigger_schema,
4664+
count(*) AS trigger_count
4665+
FROM information_schema.triggers
4666+
WHERE trigger_catalog = 'test'
4667+
GROUP BY trigger_schema
4668+
ORDER BY trigger_schema;
4669+
----
4670+
trigger_schema trigger_count
4671+
other_schema 1
4672+
public 7
4673+
4674+
# Clean up
4675+
statement ok
4676+
DROP TABLE test_triggers CASCADE;
4677+
4678+
statement ok
4679+
DROP TABLE other_schema.test_table CASCADE;
4680+
4681+
statement ok
4682+
DROP SCHEMA other_schema;
4683+
4684+
statement ok
4685+
DROP TABLE audit_log CASCADE;
4686+
4687+
statement ok
4688+
DROP FUNCTION trigger_func1, trigger_func2, trigger_func3;
4689+
4690+
subtest end

pkg/sql/information_schema.go

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2403,12 +2403,105 @@ var informationSchemaTriggeredUpdateColumnsTable = virtualSchemaTable{
24032403
}
24042404

24052405
var informationSchemaTriggersTable = virtualSchemaTable{
2406-
comment: "triggers was created for compatibility and is currently unimplemented",
2407-
schema: vtable.InformationSchemaTriggers,
2408-
populate: func(ctx context.Context, p *planner, _ catalog.DatabaseDescriptor, addRow func(...tree.Datum) error) error {
2409-
return nil
2406+
comment: `triggers contains information about triggers
2407+
https://www.postgresql.org/docs/current/infoschema-triggers.html`,
2408+
schema: vtable.InformationSchemaTriggers,
2409+
populate: func(ctx context.Context, p *planner, dbContext catalog.DatabaseDescriptor, addRow func(...tree.Datum) error) error {
2410+
opts := forEachTableDescOptions{virtualOpts: hideVirtual} /* virtual schemas have no triggers */
2411+
return forEachTableDesc(ctx, p, dbContext, opts,
2412+
func(ctx context.Context, descCtx tableDescContext) error {
2413+
db, sc, table := descCtx.database, descCtx.schema, descCtx.table
2414+
dbNameStr := tree.NewDString(db.GetName())
2415+
scNameStr := tree.NewDString(sc.GetName())
2416+
tbNameStr := tree.NewDString(table.GetName())
2417+
2418+
triggers := table.GetTriggers()
2419+
for i := range triggers {
2420+
trigger := &triggers[i]
2421+
2422+
// Process each event for the trigger.
2423+
for _, event := range trigger.Events {
2424+
var eventManipulation string
2425+
switch event.Type {
2426+
case semenumpb.TriggerEventType_INSERT:
2427+
eventManipulation = "INSERT"
2428+
case semenumpb.TriggerEventType_UPDATE:
2429+
eventManipulation = "UPDATE"
2430+
case semenumpb.TriggerEventType_DELETE:
2431+
eventManipulation = "DELETE"
2432+
case semenumpb.TriggerEventType_TRUNCATE:
2433+
eventManipulation = "TRUNCATE"
2434+
}
2435+
2436+
var actionTiming string
2437+
switch trigger.ActionTime {
2438+
case semenumpb.TriggerActionTime_BEFORE:
2439+
actionTiming = "BEFORE"
2440+
case semenumpb.TriggerActionTime_AFTER:
2441+
actionTiming = "AFTER"
2442+
case semenumpb.TriggerActionTime_INSTEAD_OF:
2443+
actionTiming = "INSTEAD OF"
2444+
}
2445+
2446+
actionOrientation := "STATEMENT"
2447+
if trigger.ForEachRow {
2448+
actionOrientation = "ROW"
2449+
}
2450+
2451+
// Build the action statement.
2452+
funcDesc, err := p.Descriptors().ByIDWithLeased(p.Txn()).Get().Function(ctx, trigger.FuncID)
2453+
if err != nil {
2454+
return err
2455+
}
2456+
funcName := tree.Name(funcDesc.GetName())
2457+
actionStatement := fmt.Sprintf(`EXECUTE FUNCTION %s`, funcName.String())
2458+
if len(trigger.FuncArgs) > 0 {
2459+
actionStatement += fmt.Sprintf("(%s)", strings.Join(trigger.FuncArgs, ", "))
2460+
} else {
2461+
actionStatement += "()"
2462+
}
2463+
2464+
// Handle action condition (WHEN clause).
2465+
var actionCondition tree.Datum = tree.DNull
2466+
if trigger.WhenExpr != "" {
2467+
actionCondition = tree.NewDString(trigger.WhenExpr)
2468+
}
2469+
2470+
// Handle transition table names
2471+
var oldTableName, newTableName tree.Datum = tree.DNull, tree.DNull
2472+
if trigger.OldTransitionAlias != "" {
2473+
oldTableName = tree.NewDString(trigger.OldTransitionAlias)
2474+
}
2475+
if trigger.NewTransitionAlias != "" {
2476+
newTableName = tree.NewDString(trigger.NewTransitionAlias)
2477+
}
2478+
2479+
if err := addRow(
2480+
dbNameStr, // trigger_catalog
2481+
scNameStr, // trigger_schema
2482+
tree.NewDString(trigger.Name), // trigger_name
2483+
tree.NewDString(eventManipulation), // event_manipulation
2484+
dbNameStr, // event_object_catalog
2485+
scNameStr, // event_object_schema
2486+
tbNameStr, // event_object_table
2487+
tree.DNull, // action_order
2488+
actionCondition, // action_condition
2489+
tree.NewDString(actionStatement), // action_statement
2490+
tree.NewDString(actionOrientation), // action_orientation
2491+
tree.NewDString(actionTiming), // action_timing
2492+
oldTableName, // action_reference_old_table
2493+
newTableName, // action_reference_new_table
2494+
tree.DNull, // action_reference_old_row
2495+
tree.DNull, // action_reference_new_row
2496+
tree.DNull, // created
2497+
); err != nil {
2498+
return err
2499+
}
2500+
}
2501+
}
2502+
return nil
2503+
})
24102504
},
2411-
unimplemented: true,
24122505
}
24132506

24142507
var informationSchemaTablesExtensionsTable = virtualSchemaTable{

0 commit comments

Comments
 (0)