cockroachdb / cockroachdb/cockroach
sql/plpgsql: support dynamic EXECUTE in trigger functions
- Dominant language
- Go
- Stars
- 32.5k
- Forks
- 4.1k
- PR merge metrics
- PR metrics pending
Description
**Is your feature request related to a problem? Please describe.**
PostgreSQL places no restriction on `EXECUTE` in a PL/pgSQL trigger
function — a trigger function is an ordinary PL/pgSQL function returning
`trigger`, and dynamic SQL behaves there as it does anywhere else. It is
the basis of the common generic-audit and row-routing trigger patterns:
```sql
CREATE FUNCTION audit_trg() RETURNS trigger AS $$
BEGIN
EXECUTE format('INSERT INTO %I SELECT ($1).*', TG_TABLE_NAME || '_audit')
USING NEW;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
```
CockroachDB rejects this. After #172439, trigger functions are the only
PL/pgSQL routine kind where dynamic `EXECUTE` is still blocked — stored
procedures, UDFs, and DO blocks are all supported — and they fail with a
dedicated `unimplemented` error pointing at #169581.
**Describe the solution you'd like**
Allow `*ast.DynamicExecute` in a trigger function body, subject to the
same statement allowlist and the same `SECURITY DEFINER` restriction that
apply to every other routine kind. Points to work out:
- **`USING NEW` / `USING OLD`.** These are the interesting arguments to
pass, and they are records. `USING` binding substitutes each `$n` with a
cast of the argument to the `USING` expression's type, so tuple-typed
arguments have to survive that path, and `($1).*` has to work in the
command string. Worth verifying rather than assuming it comes for free.
- **Trigger return handling.** A trigger function must return a row or
`NULL`. The rest-of-body build after a dynamic `EXECUTE` is deferred to
execution time, so a trigger function that falls off the end after an
`EXECUTE` would raise at runtime rather than at `CREATE` time, as a UDF
does today. Confirm the trigger-specific return validation composes with
the deferred tail.
- **Trigger-function build caching.** Trigger function bodies are built
and cached per trigger during optbuilding
(`pkg/sql/opt/optbuilder/trigger.go`), while the dynamic-`EXECUTE` body
builder rebuilds its statement on each invocation rather than reusing a
cached plan. Confirm the two do not interact badly.
- **Privileges.** Trigger functions honor their declared security mode, so
a `SECURITY DEFINER` trigger function is already covered by the runtime
elevated-privileges gate and the `CREATE`-time check from #172404. No new
rule is needed, but it needs test coverage.
**Describe alternatives you've considered**
Leaving trigger functions blocked. The block is an implementation gap, not
a deliberate semantic divergence: triggers run as the invoking user unless
declared `SECURITY DEFINER`, so nothing about the trigger context makes
dynamic SQL more dangerous than it is in a UDF. Keeping it would leave a
parity gap in what is arguably the most common dynamic-SQL use case.
**Additional context**
Part of the dynamic-SQL EXECUTE effort tracked in #169581. Builds on
#172439, which lifted the restriction for UDFs and DO blocks and left
trigger functions as the remaining unsupported kind.
Epic CRDB-48117
Jira issue: CRDB-67883
Contributor guide
Research direction
Start with trigger-function building and caching in pkg/sql/opt/optbuilder/trigger.go, then compare the dynamic-EXECUTE support added for UDFs and DO blocks in #172439. Verify USING NEW/OLD records, deferred trigger return validation, per-invocation rebuilding, and SECURITY DEFINER behavior; done means trigger functions accept dynamic EXECUTE with coverage for these cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, sql
- Domain
- databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100