Trigger Architecture¶
4.1 Generated Trigger Functions¶
_create_triggers(p_auto_mv_id uuid) generates one PL/pgSQL function per watch table per auto_mv. Each function is created via EXECUTE with dynamic SQL built using format() and %I/%L quoting — never string concatenation with raw identifier values.
Naming convention:
Function: pgauto_mv._trig_{id_safe}_{schema_safe}_{table_safe}()
DML trigger: pg_auto_mv_{id_safe}
TRUNCATE trigger: pg_auto_mv_{id_safe}_trunc
UUIDs contain hyphens which are illegal in unquoted identifiers. v_id_safe := replace(auto_mv_id::text, '-', '_') is applied to every generated name. Schema and table names are sanitised with regexp_replace(name, '[^a-zA-Z0-9]', '_', 'g').
ID tag in prosrc: Every generated function body contains the comment -- pgauto_mv_id:{uuid} (raw UUID with hyphens). This is the recovery anchor for clean_mv(): it can locate functions by scanning pg_proc.prosrc even after a DBA has manually renamed the function away from its canonical name.
Generated function body (all events, both trigger types):
1. v_event_at := clock_timestamp()
2. SELECT is_pending INTO v_was_pending FROM mv_state WHERE auto_mv_id = v_auto_mv_id
3. INSERT INTO mv_events (action = 'opened' or 'updated')
4. UPDATE mv_state:
is_pending = true
latest_event_at = v_event_at
first_event_at = CASE WHEN NOT is_pending THEN v_event_at ELSE first_event_at END
last_watch_schema / last_watch_table from TG_TABLE_SCHEMA / TG_TABLE_NAME
5. PERFORM pgrelay.notify('pg_auto_mv.refresh_materialised_view', v_auto_mv_id::text)
[6. If fire_on_replica=true and TG_OP='TRUNCATE':
INSERT INTO mv_truncate_signals (auto_mv_id, watch_schema, watch_table)]
7. RETURN NULL
No computed columns are written to mv_state from the trigger. The trigger writes only raw timestamps and flags. All timing derivations live in mv_state_v.
4.2 Trigger Modes¶
Controlled by fire_on_replica on the registry row:
fire_on_replica |
DML trigger type | Enabled | TRUNCATE trigger |
|---|---|---|---|
false (default) |
FOR EACH STATEMENT |
ORIGIN (default) |
FOR EACH STATEMENT, default |
true |
FOR EACH ROW |
ALWAYS (via ALTER TABLE ... ENABLE ALWAYS TRIGGER) |
FOR EACH STATEMENT, default |
TRUNCATE triggers are always statement-level — PostgreSQL does not permit FOR EACH ROW triggers on TRUNCATE. The ENABLE ALWAYS flag cannot override the statement-level TRUNCATE restriction during logical apply.
The TRUNCATE trigger (_trunc) is only created when watch_delete = true for that watch table row.
4.3 _drop_triggers(p_auto_mv_id uuid)¶
Iterates watch_tables for the auto_mv. For each watch table it:
- Checks the table still exists in
pg_class(watch table may have been dropped) before attempting trigger drops. - Executes
DROP TRIGGER IF EXISTSfor both the DML and TRUNCATE trigger names. - Executes
DROP FUNCTION IF EXISTSfor the generated function.
4.4 clean_mv(p_auto_mv_id uuid DEFAULT NULL)¶
Maintenance tool that rebuilds triggers from scratch. For each auto_mv (or one specific one when a UUID is supplied):
- Checks
mv_oidinpg_class. If missing, returns amv_missingrow and skips trigger work. - Finds all trigger functions via two independent searches:
- Canonical name:
starts_with(proname, '_trig_' || id_safe || '_') - ID tag in prosrc:
prosrc LIKE '%-- pgauto_mv_id:{uuid}%'Functions found only by tag (renamed by a DBA) are reported asrecovered_function. - For each found function, locates all triggers using it via
pg_trigger.tgfoidand drops them, then drops the function. - Drops canonical-named triggers that may linger without a matching function.
- Calls
_create_triggers()to recreate from the currentwatch_tablesrows. - Returns a
triggers_recreatedrow.