Event triggers
Use [PgEventTrigger] on a synchronous static method returning void with one
nonnull PgEventTriggerContext parameter. Ankus generates a zero-argument SQL
function returning event_trigger. Add [PgFunction] to configure its name,
schema, security, search path, and dependency identifiers.
[PgEventTrigger][PgFunction(Id = "report-table-creation-function")]public static void ReportTableCreation(PgEventTriggerContext context){ foreach (PgDdlCommand command in context.GetDdlCommands()) { if (command.ObjectType == "table") { PgLog.Write(PgLogLevel.Notice, $"{command.CommandTag}: {command.ObjectIdentity}"); } }}Attach the callback with PostgreSQL SQL, optionally in a dependency-ordered installation block:
[assembly: PgSql("report-table-creation", """ CREATE EVENT TRIGGER ankus_report_table_creation ON ddl_command_end WHEN TAG IN ('CREATE TABLE') EXECUTE FUNCTION report_table_creation(); """, Requires = ["report-table-creation-function"], Relocatable = true)]The Ankus.Examples.EventTriggers sample uses this attachment; see also
custom SQL. Event triggers have database-wide names and scope;
PostgreSQL requires a superuser to create them. Their function bindings follow
extension relocation, and extension-owned event triggers are removed on uninstall.
PostgreSQL handles tag filters, alphabetical firing order, and enable/replica/always
settings. Event-trigger DDL itself does not invoke event triggers.
PgFunction.Sql can replace the event-trigger function declaration, including
RETURNS event_trigger; GenerateSql = false retains its native callback without
installing the function. Attach the event trigger separately with custom SQL.
See function SQL controls.
Events and helpers
Section titled “Events and helpers”context.Kind is a PgEventTriggerKind. Event preserves PostgreSQL’s event name,
and CommandTag preserves its command tag, such as CREATE TABLE or ALTER TABLE.
| Kind | Event | Timing and available helper |
|---|---|---|
DdlCommandStart |
ddl_command_start |
Before DDL execution and object-existence checks |
DdlCommandEnd |
ddl_command_end |
After successful DDL, before commit; GetDdlCommands() |
SqlDrop |
sql_drop |
After catalog deletion, before command end; GetDroppedObjects() |
TableRewrite |
table_rewrite |
Before an eligible table rewrite; GetTableRewrite() |
Login |
login |
On a new connection after authentication; PostgreSQL 17+ |
Event triggers apply to the commands supported by PostgreSQL, including some
ALTER commands that drop objects. A command that fails does not run its end
handlers. A metadata-only alteration need not rewrite a table; CLUSTER and
VACUUM do not invoke table_rewrite handlers.
Rows, SetMode, SQL parameter/result binding attributes, and ordinary SELECT
invocation do not apply to event callbacks. Row triggers use the separate
PgTriggerContext API.
Owned command and object metadata
Section titled “Owned command and object metadata”GetDdlCommands() returns immutable PgDdlCommand snapshots. One statement can
produce several base commands, and a successful no-op can produce an empty list.
Each snapshot includes the catalog class/object/subobject identity, command tag,
object type, optional schema and object identity, and InExtension. The latter
identifies commands executed inside an extension script. GRANT, REVOKE, and
default-privilege commands can have null catalog identities and names; zero and
an empty string are distinct values.
GetDroppedObjects() preserves every reported object, including implicit
dependencies. Original and Normal describe separate dependency properties.
The snapshots include IsTemporary, object identity, and ordered AddressNames
and AddressArguments. Optional values remain null, and empty argument lists
remain empty. Temporary names use PostgreSQL’s canonical pg_temp representation.
Dropped metadata remains useful after the catalog entries have disappeared.
GetTableRewrite() returns a PgTableRewrite with TableOid and a Reason
bitmap. PgTableRewriteReason defines AlterPersistence, DefaultValue,
ColumnRewrite, and AccessMethod. Multiple reasons can be combined; the numeric
bitmap preserves additional bits supplied by the server.
The ALTER TABLE ... SET ACCESS METHOD rewrite requires PostgreSQL 15 or later.
Earlier servers reject that statement with SQLSTATE 42601 before a rewrite
callback runs, leaving the table unchanged.
These helpers project PostgreSQL’s public metadata columns. They do not expose
the internal parse tree or opaque pg_ddl_command object. The native callback
does not supply the fired event trigger’s name or OID.
Lifetime and errors
Section titled “Lifetime and errors”Context properties and returned snapshots own their managed data. They remain readable after SPI disconnects, nested DDL, callback return, or transaction end. Address collections cannot be mutated through their public views.
Call each helper on the current invocation’s context and only in its matching
event phase. Calling through an old context, a suspended outer event context,
or a worker thread throws InvalidOperationException. Nested event handlers
restore their enclosing context when they finish. An event callback entered
from a row trigger has its own SPI environment; the outer row trigger’s
transition tables become available again when control returns to it.
Throw PgException to reject an operation with a PostgreSQL SQLSTATE and
diagnostics. Unhandled managed exceptions are reported through the same guarded
native boundary. An error in an end or drop handler rolls back the DDL and its
transactional side effects. Guarded SPI errors can be caught in the callback
when the operation should recover and continue.
Login handlers run for physical connections and can prevent connection startup if they fail. Keep a working administrative connection while developing one. They also run on standby servers, so account for read-only operation when writing a login handler. The sample uses DDL events and does not install a login handler.
PostgreSQL 13–16 rejects CREATE EVENT TRIGGER ... ON login with SQLSTATE
42601; no login handler is attached. DDL event handlers remain available.
Declaration diagnostics
Section titled “Declaration diagnostics”Row and event callbacks must run synchronously on the invoking PostgreSQL
backend thread. Each declaration error identifies its own contract and
highlights the relevant modifier, type, parameter, attribute or option.
Row callbacks return PgHeapTuple or PgHeapTuple?; event callbacks return
void. Their required context types are PgTriggerContext and
PgEventTriggerContext, respectively.
| Diagnostic | Correction |
|---|---|
ANKUS208 |
Declare a static callback. |
ANKUS209, ANKUS228, ANKUS229 |
Perform the work synchronously; callbacks cannot use async, return Task/ValueTask, or return IAsyncEnumerable. |
ANKUS210 |
Remove callback method type parameters. |
ANKUS211 |
Provide a concrete method implementation. |
ANKUS212, ANKUS214 |
Return the callback’s required result by value. |
ANKUS213, ANKUS223 |
Make the method and its enclosing types accessible to generated code: public, internal, or protected internal. |
ANKUS215 |
Declare exactly one required context parameter. |
ANKUS216, ANKUS217, ANKUS218 |
Pass that context by value without ref, in, out, params, a default, or Optional. |
ANKUS219, ANKUS220 |
Use the correct, nonnullable context type. |
ANKUS221, ANKUS222 |
Declare callbacks in non-generic types that are not file-local. |
ANKUS224 |
Choose one row/event callback or operator/cast export role. |
ANKUS225 |
Remove SQL value binding attributes from the context and result. |
ANKUS226, ANKUS227 |
Remove Rows and SetMode, which describe ordinary set-returning functions. |
ANKUS277 |
Remove [Conditional], so every invocation runs the callback. |
Editors offer Make static for ANKUS208, Make synchronous for
ANKUS209, Return by value for ANKUS212, Make internal for
ANKUS213, ANKUS222 and ANKUS223, and Remove for ANKUS225–ANKUS227
and ANKUS277. For the context parameter they offer Add the parameter
(ANKUS215), Remove the default value or Optional (ANKUS218),
Change the type to the non-nullable context (ANKUS219) and Pass by
value for an in or ref readonly context (ANKUS216).
Invalid callback declarations do not prevent unrelated valid functions from
receiving their generated dispatchers. Shared [PgFunction] names, schemas,
security and dependency options retain their ordinary declaration diagnostics.