콘텐츠로 이동

(KO) PostgreSQL 이벤트 트리거 — DDL 훅과 명령 수집

목차

**트리거(trigger)**는 특정 사건이 발생할 때 코드를 자동으로 실행하는 데이터베이스 객체다. SQL 표준이 정의하는 전통적인 형태는 **행/구문 트리거(row/statement trigger)**로, INSERT, UPDATE, DELETE에 반응해 테이블에 묶인다. 그 메커니즘은 postgres-triggers.md에서 다룬다. 이 문서는 다른 종류인 **이벤트 트리거(event trigger)**를 다룬다. 이벤트 트리거는 데이터 변경이 아닌 스키마 변경세션 생명주기 이벤트에 반응한다.

개념적 차이를 명확히 하면, DML 트리거는 변경에 반응하고 이벤트 트리거는 카탈로그 변경 — CREATE TABLE, ALTER ... ADD COLUMN, DROP INDEX, GRANT — 또는 연결 세션 수립에 반응한다. DML 트리거는 SQL 표준에 있지만 DDL/이벤트 트리거는 표준 밖이다. 이 기능을 제공하는 모든 시스템이 각자의 어휘와 발화 모델을 만들었다.

이런 메커니즘이 필요한 이유는 네 가지 지속적인 사용 사례로 요약된다.

  1. 감사와 변경 포착. 스키마 변경을 로그 테이블에 기록해 “언제 누가 이 테이블을 바꿨는지”를 서버 로그 없이 답할 수 있게 한다.
  2. 정책 시행. 유지보수 창 밖에서 DROP TABLE을 금지하거나, 새 테이블에 기본 키를 강제하거나, 명명 규칙을 벗어난 객체 이름을 거부한다.
  3. 복제와 스키마 전파. 논리 복제는 데이터 변경을 전달하지만 스키마 변경은 전달하지 않는다. DDL 텍스트나 구조화된 표현을 포착하는 이벤트 트리거가 복제본에 스키마 변경을 전달하는 수단이 된다. 이것이 PostgreSQL의 DDL 명령 수집 기능의 동기다.
  4. 반응형 자동화. 새로 만들어진 모든 테이블에 자동으로 권한을 부여하거나 기본 COMMENT를 붙이거나 외부 메타데이터 서비스에 객체를 등록한다.

이벤트 트리거 설계자가 해결해야 할 이론적 긴장은 위치와 타이밍이다. DML 트리거는 자연스러운 단일 발화 지점이 있다. 스토리지 계층의 튜플 삽입/수정/삭제 루틴이다. DDL에는 그런 단일 지점이 없다. CREATE TABLE, ALTER TYPE, GRANT는 완전히 다른 코드 경로를 통하고, 하나의 유틸리티 명령이 재귀적으로 하위 명령을 낳는다. ALTER TABLE로 기본값이 있는 컬럼을 추가하면 테이블 재작성, TOAST 테이블 생성, 인덱스 빌드가 한꺼번에 일어날 수 있다. 설계자는 결정해야 한다. 명령 생명주기의 어디서 발화할지(파싱 전? 실행 전? 실행 후?), 트리거 함수에 어떤 컨텍스트를 줄지(명령 태그만? 파스 트리? 영향받은 객체 목록?), 그 컨텍스트가 DDL 실행의 재귀적 다단계 특성을 어떻게 버텨낼지.

Database System Concepts(Silberschatz, Korth, Sudarshan, 7판, 5장 “Advanced SQL”, §5.3 “Triggers”)는 일반 트리거 모델을 이벤트-조건-액션(ECA) 트리플로 정의한다. 이벤트(발생하는 것), 조건(액션 실행 여부를 결정하는 술어), 액션(코드)이다. 교재가 트리거를 두고 경고하는 사항 — 암묵적 실행, 연쇄 트리거의 추론 어려움, 버그 있는 트리거가 수정에 필요한 연산 자체를 막는 위험 — 은 이벤트 트리거에 더 강하게 적용된다. “이벤트”가 DDL 구문이기 때문에 깨진 이벤트 트리거는 데이터베이스를 변경 불가 상태로 만들 수 있다. ECA 용어로 PostgreSQL의 이벤트 트리거를 표현하면, 이벤트는 다섯 가지 이벤트 유형 중 하나, 조건WHEN tag IN (...) 필터와 세션 복제 역할 검사, 액션은 의사 타입 event_trigger를 반환하는 함수다. 단독 사용자(standalone) 모드에서 이벤트 트리거를 완전히 비활성화하고 슈퍼유저 전용 GUC 뒤에 두는 방어적 선택은 교재의 경고가 낳은 직접적 설계 결과다.

DDL/이벤트 트리거는 비표준 기능이라 B-트리 같은 표준 기능보다 수렴도가 낮다. 그럼에도 이 기능을 제공하는 시스템들은 공통된 설계 패턴을 공유한다. 그 패턴을 먼저 짚으면 PostgreSQL의 구체적 선택이 작은 설계 공간 안의 한 점으로 읽힌다.

이름 있는 이벤트 유형, 범용 훅 목록 없음

섹션 제목: “이름 있는 이벤트 유형, 범용 훅 목록 없음”

“임의의 DDL”을 잡는 단일 훅을 노출하는 대신, 시스템은 잘 정의된 순간에 발화하는 이벤트 유형 집합을 열거한다. Oracle은 BEFORE/AFTER 시스템 이벤트(CREATE, ALTER, DROP, LOGON, LOGOFF, SERVERERROR)를 갖고, SQL Server는 이벤트 그룹(DDL_TABLE_EVENTS, DDL_LOGIN_EVENTS, …)으로 키된 DDL 트리거를 갖는다. 열거 방식은 두 가지를 얻는다. 엔진은 정의된 이벤트에 대응하는 지점에만 발화 호출을 심으면 되고, 카탈로그는 CREATE 시점에 사용자가 실재하는 이벤트를 지정했는지 검증할 수 있다.

명령 처리 파이프라인에 고정된 발화 모델

섹션 제목: “명령 처리 파이프라인에 고정된 발화 모델”

DDL 트리거는 유틸리티 명령 경로의 어딘가에서 발화해야 한다. 보편적인 분리는 명령 (트리거가 거부하거나 의도를 기록할 수 있는 지점)과 명령 (트리거가 결과 카탈로그 상태를 볼 수 있는 지점)다. PostgreSQL의 ddl_command_start / ddl_command_end 쌍이 바로 이 분리이고, Oracle의 BEFORE / AFTER 시스템 트리거와 SQL Server의 INSTEAD OF / AFTER DDL 트리거가 그에 상응한다.

트리거 함수에 전달되는 컨텍스트 객체

섹션 제목: “트리거 함수에 전달되는 컨텍스트 객체”

트리거 함수는 무엇이 자신을 발화시켰는지 알아야 한다. 최소한 명령 태그(CREATE TABLE)가 필요하고, 풍부한 설계는 객체 이름, 스키마, 전체 구문 텍스트까지 전달한다. PostgreSQL은 이벤트 이름, 파스 트리, 명령 태그를 담은 EventTriggerData 노드를 넘기고, 추가로 pg_event_trigger_dropped_objectspg_event_trigger_ddl_commands 같은 집합 반환 함수(SRF)로 구조화된 설명을 제공한다. SQL Server의 EVENTDATA() XML 블롭이 이에 대응한다.

모든 것에 발화하지 않기 위한 필터/조건

섹션 제목: “모든 것에 발화하지 않기 위한 필터/조건”

모든 DDL에 발화하는 것은 비싸고 대개 불필요하다. 시스템은 트리거 범위를 좁히는 방법을 제공한다. SQL Server는 이벤트 그룹으로 범위를 좁히고, PostgreSQL은 선택적 WHEN tag IN (...) 목록으로 좁힌다. 이 목록은 카탈로그에 텍스트 배열로 저장되고 발화 시점에 빠른 소속 검사를 위한 Bitmapset으로 컴파일된다.

깨진 DDL 트리거가 스키마를 변경 불가 상태로 만들 수 있으므로 시스템은 우회 방법을 제공한다. PostgreSQL은 단독 사용자(standalone) 모드와 슈퍼유저 설정 가능 event_triggers GUC 뒤에서 이벤트 트리거를 완전히 비활성화한다.

개념PostgreSQL 이름
이벤트-조건-액션 트리플이벤트 유형 + WHEN tag 필터 + event_trigger 함수
열거된 이벤트 유형EventTriggerEvent 열거형 (EVT_DDLCommandStartEVT_Login)
명령 전 훅ddl_command_start (EventTriggerDDLCommandStart)
명령 후 훅ddl_command_end (EventTriggerDDLCommandEnd)
객체 드롭 훅sql_drop (EventTriggerSQLDrop)
테이블 재작성 훅table_rewrite (EventTriggerTableRewrite)
세션 로그인 훅login (EventTriggerOnLogin)
함수에 전달되는 컨텍스트 객체EventTriggerData 노드
CALLED_AS_EVENT_TRIGGER 가드event_trigger.h 매크로
카탈로그 행pg_event_trigger (evtevent, evtfoid, evttags, evtenabled)
컴파일된 트리거 목록syscache 기반 evtcacheEventTriggerCacheItem
태그 필터(컴파일된 형태)Bitmapset *tagset
명령별 임시 상태EventTriggerQueryState
드롭된 객체 사이드 채널SQLDropListpg_event_trigger_dropped_objects
명령 수집 사이드 채널CollectedCommand 목록 → pg_event_trigger_ddl_commands
비상 비활성화event_triggers GUC + standalone 모드 검사

다섯 가지 이벤트 유형, 수작업으로 배치된 발화 호출

섹션 제목: “다섯 가지 이벤트 유형, 수작업으로 배치된 발화 호출”

PostgreSQL에는 “임의의 DDL을 가로채는” 범용 디스패처가 없다. 대신 다섯 가지 특정 이벤트를 정의하고, 각각을 명시적으로 명명된 C 함수가 명령 처리 경로의 적절한 지점에서 직접 호출한다. 이벤트는 evtcache.h에 열거된다.

// EventTriggerEvent — src/include/utils/evtcache.h
typedef enum
{
EVT_DDLCommandStart,
EVT_DDLCommandEnd,
EVT_SQLDrop,
EVT_TableRewrite,
EVT_Login,
} EventTriggerEvent;

CreateEventTrigger는 사용자가 이 다섯 가지 중 하나를 정확히 지정했는지 검증하고 그 외는 거부한다.

// CreateEventTrigger (excerpt) — src/backend/commands/event_trigger.c
if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
strcmp(stmt->eventname, "ddl_command_end") != 0 &&
strcmp(stmt->eventname, "sql_drop") != 0 &&
strcmp(stmt->eventname, "login") != 0 &&
strcmp(stmt->eventname, "table_rewrite") != 0)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("unrecognized event name \"%s\"", stmt->eventname)));

CreateEventTrigger는 슈퍼유저를 요구하고, 트리거 함수가 event_trigger 의사 타입을 반환하는지 검증하며, WHEN tag 필터 목록을 이벤트 트리거를 지원하는 태그 집합(validate_ddl_tagscommand_tag_event_trigger_ok)과 대조해 검증한다. 카탈로그 행은 pg_event_trigger에 삽입되고, login 트리거의 경우 아래에서 설명할 빠른 경로 플래그도 설정한다.

트리거 함수가 실행될 때 일반 인자가 아닌 fmgr “컨텍스트”로 노드 하나를 받는다. 노드는 작다.

// EventTriggerData — src/include/commands/event_trigger.h
typedef struct EventTriggerData
{
NodeTag type;
const char *event; /* event name */
Node *parsetree; /* parse tree */
CommandTag tag;
} EventTriggerData;
#define CALLED_AS_EVENT_TRIGGER(fcinfo) \
((fcinfo)->context != NULL && IsA((fcinfo)->context, EventTriggerData))

C로 작성된 함수는 CALLED_AS_EVENT_TRIGGER(fcinfo)를 확인해 이벤트 트리거로 호출됐는지 직접 호출됐는지 구분한다. PL 언어(PL/pgSQL)는 같은 세 필드를 TG_EVENTTG_TAG로 노출한다. 이 세 필드보다 풍부한 정보 — 드롭된 객체 목록, 수집된 명령 목록, 재작성 중인 테이블 — 는 모두 트리거 본문에서 전용 SQL 함수를 호출해 명령별 임시 상태에서 꺼내야 한다.

명령별 임시 상태: EventTriggerQueryState

섹션 제목: “명령별 임시 상태: EventTriggerQueryState”

이벤트 트리거의 어려운 부분은 발화 자체가 아니라, 단일 DDL 명령의 재귀적 다단계 실행 전반에 걸쳐 충분한 컨텍스트를 살려두는 것이다. ddl_command_endsql_drop 트리거가 보고할 내용이 있으려면 그 컨텍스트가 필요하다. PostgreSQL은 최상위 유틸리티 명령 시작 시 스택에 올리고 끝날 때 꺼내는 명령별 상태 객체로 이 문제를 해결한다.

// EventTriggerQueryState — src/backend/commands/event_trigger.c
typedef struct EventTriggerQueryState
{
MemoryContext cxt; /* memory context for this state's objects */
/* sql_drop */
slist_head SQLDropList;
bool in_sql_drop;
/* table_rewrite */
Oid table_rewrite_oid;
int table_rewrite_reason;
/* Support for command collection */
bool commandCollectionInhibited;
CollectedCommand *currentCommand;
List *commandList; /* list of CollectedCommand */
struct EventTriggerQueryState *previous;
} EventTriggerQueryState;
static EventTriggerQueryState *currentEventTriggerState = NULL;

previous 포인터가 이것을 스택으로 만든다. 이벤트 트리거 안에서 실행된 DDL 명령은 바깥 상태에 연결된 자체 상태를 가져, 드롭된 객체와 수집된 명령이 올바른 명령 수준에 귀속된다. 상태는 자체 MemoryContext에 살아서 모든 SQLDropObject를 하나씩 해제하는 대신 MemoryContextDelete 한 번으로 정리된다.

명령을 앞뒤로 감싸는 두 DDL 이벤트 ddl_command_startddl_command_endutility.cProcessUtilitySlow 안에서 발화한다. “slow” 경로는 파싱, 잠금, 이벤트 트리거 지원이 필요한 명령을 처리하는 유틸리티 처리의 절반이다. standard_ProcessUtility가 DDL을 그쪽으로 보낸다. 감싸는 구조가 ProcessUtilitySlow의 상단과 하단에 보인다.

// ProcessUtilitySlow (excerpt) — src/backend/tcop/utility.c
bool isCompleteQuery = (context != PROCESS_UTILITY_SUBCOMMAND);
bool needCleanup;
/* All event trigger calls are done only when isCompleteQuery is true */
needCleanup = isCompleteQuery && EventTriggerBeginCompleteQuery();
PG_TRY();
{
if (isCompleteQuery)
EventTriggerDDLCommandStart(parsetree);
switch (nodeTag(parsetree))
{
/* ... one case per DDL statement type; each case executes the
command and calls EventTriggerCollectSimpleCommand (or a more
specific collector) for the affected object ... */
}
if (!commandCollected)
EventTriggerCollectSimpleCommand(address, secondaryObject, parsetree);
if (isCompleteQuery)
{
EventTriggerSQLDrop(parsetree);
EventTriggerDDLCommandEnd(parsetree);
}
}
PG_FINALLY();
{
if (needCleanup)
EventTriggerEndCompleteQuery();
}
PG_END_TRY();

세 가지가 핵심이다. 첫째, isCompleteQuery는 하위 명령(PROCESS_UTILITY_SUBCOMMAND)에서 false다. 내부적으로 CREATE INDEX를 발행하는 ALTER TABLE은 인덱스를 위한 이벤트 쌍을 다시 발화하지 않는다. 이벤트 트리거는 최상위 명령 경계에서 한 번만 발화한다. 둘째, 전체 본문이 PG_TRY/PG_FINALLY로 감싸져 명령이 오류를 내도 EventTriggerEndCompleteQuery가 실행된다. 셋째, sql_dropddl_command_end 전에 발화한다. 드롭 목록이 실행 중에 수집되고, sql_drop 트리거가 객체 메타데이터가 아직 재구성 가능한 동안 소비하기 때문이다.

table_rewriteutility.c에서 발화하지 않는다. ALTER TABLE 실행 깊숙이 테이블이 물리적으로 재작성되기 직전에 발화한다.

// ATRewriteTables (excerpt) — src/backend/commands/tablecmds.c
/* And fire it only once. */
if (parsetree)
EventTriggerTableRewrite((Node *) parsetree,
tab->relid,
tab->rewrite);

loginPostgresMain에서 인증 후 주 쿼리 루프 전에 한 번 발화한다.

// PostgresMain (excerpt) — src/backend/tcop/postgres.c
/* Fire any defined login event triggers, if appropriate */
EventTriggerOnLogin();
flowchart TB
  A["standard_ProcessUtility<br/>(DDL 명령)"] --> B["ProcessUtilitySlow"]
  B --> C["EventTriggerBeginCompleteQuery<br/>EventTriggerQueryState 스택에 올리기"]
  C --> D["EventTriggerDDLCommandStart<br/>ddl_command_start 발화"]
  D --> E["명령 실행<br/>(nodeTag 스위치)"]
  E --> F["EventTriggerCollect*<br/>CollectedCommand를 commandList에 추가"]
  E --> G["dependency.c 드롭<br/>EventTriggerSQLDropAddObject -> SQLDropList"]
  F --> H["EventTriggerSQLDrop<br/>sql_drop 발화, SQLDropList 소비"]
  G --> H
  H --> I["EventTriggerDDLCommandEnd<br/>ddl_command_end 발화, commandList 노출"]
  I --> J["EventTriggerEndCompleteQuery<br/>스택에서 꺼내기 + MemoryContextDelete (PG_FINALLY)"]

그림 1 — ProcessUtilitySlow 안에서 DDL 이벤트가 발화하는 위치. 상태는 최상위 명령마다 한 번 올라가고, 시작 이벤트가 실행 전에 발화하며, 드롭과 수집된 명령이 실행 중 쌓이고, 끝/드롭 이벤트가 이후에 발화한다. 상태는 항상 꺼내지도록 PG_TRY로 감싼다. (utility.cProcessUtilitySlowevent_trigger.c의 흐름.)

공통 발화 경로: EventTriggerCommonSetup + EventTriggerInvoke

섹션 제목: “공통 발화 경로: EventTriggerCommonSetup + EventTriggerInvoke”

다섯 개의 EventTrigger* 발화 함수는 모두 EventTriggerCommonSetup을 통한다. 이 함수가 캐시에서 관련 트리거를 조회하고, 필터링하고, EventTriggerData 노드를 만든다. 살아남은 함수 OID는 EventTriggerInvoke가 실행한다. 조회는 이벤트 유형으로 키된 전용 syscache 기반 캐시(evtcache)가 서비스한다. 일반적인 경우 — 이 이벤트에 트리거 없음 — 는 해시 프로브 한 번과 조기 반환이다.

// EventTriggerCommonSetup (excerpt) — src/backend/commands/event_trigger.c
/* Use cache to find triggers for this event; fast exit if none. */
cachelist = EventCacheLookup(event);
if (cachelist == NIL)
return NIL;
tag = EventTriggerGetTag(parsetree, event);
foreach(lc, cachelist)
{
EventTriggerCacheItem *item = lfirst(lc);
if (unfiltered || filter_event_trigger(tag, item))
runlist = lappend_oid(runlist, item->fnoid);
}
if (runlist == NIL)
return NIL;
trigdata->type = T_EventTriggerData;
trigdata->event = eventstr;
trigdata->parsetree = parsetree;
trigdata->tag = tag;
return runlist;

filter_event_trigger는 두 조건을 적용한다. 세션 복제 역할(ENABLE REPLICA/ENABLE ALWAYS 구분으로 트리거를 논리 복제본이나 원본에서만 발화시킬 수 있다)과, 컴파일된 Bitmapset에 대한 WHEN tag 소속 검사다.

// filter_event_trigger (excerpt) — src/backend/commands/event_trigger.c
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
{
if (item->enabled == TRIGGER_FIRES_ON_ORIGIN)
return false;
}
else
{
if (item->enabled == TRIGGER_FIRES_ON_REPLICA)
return false;
}
/* Filter by tags, if any were specified. */
if (!bms_is_empty(item->tagset) && !bms_is_member(tag, item->tagset))
return false;
return true;

EventTriggerInvoke는 살아남은 각 함수를 새 메모리 컨텍스트에서 실행한다(함수 사이에 초기화해 누수를 제한). fmgr 기계로 EventTriggerData 노드를 컨텍스트로 건네고 일반 인자 없이 호출한다.

// EventTriggerInvoke (excerpt) — src/backend/commands/event_trigger.c
context = AllocSetContextCreate(CurrentMemoryContext,
"event trigger context",
ALLOCSET_DEFAULT_SIZES);
oldcontext = MemoryContextSwitchTo(context);
foreach(lc, fn_oid_list)
{
LOCAL_FCINFO(fcinfo, 0);
Oid fnoid = lfirst_oid(lc);
FmgrInfo flinfo;
if (first)
first = false;
else
CommandCounterIncrement(); /* each trigger sees prior trigger's work */
fmgr_info(fnoid, &flinfo);
InitFunctionCallInfoData(*fcinfo, &flinfo, 0,
InvalidOid, (Node *) trigdata, NULL);
FunctionCallInvoke(fcinfo);
MemoryContextReset(context);
}

함수 사이의 CommandCounterIncrement가 중요하다. 각 트리거 함수가 이전 트리거의 카탈로그 효과를 볼 수 있게 해, 트리거 체인이 하위 명령의 순서처럼 동작한다.

EventCacheLookupEventTriggerCommonSetup이 호출하는 빠른 경로 조회다. 모든 DDL 명령마다 pg_event_trigger를 스캔하는 대신, 백엔드별 소형 캐시가 이벤트 유형별로 컴파일된 EventTriggerCacheItem 목록(함수 OID, 활성화 플래그, 컴파일된 태그 Bitmapset)을 보관한다. 캐시는 지연 빌드되고 pg_event_trigger의 syscache 콜백으로 무효화된다.

// EventCacheLookup — src/backend/utils/cache/evtcache.c
List *
EventCacheLookup(EventTriggerEvent event)
{
EventTriggerCacheEntry *entry;
if (EventTriggerCacheState != ETCS_VALID)
BuildEventTriggerCache();
entry = hash_search(EventTriggerCache, &event, HASH_FIND, NULL);
return entry != NULL ? entry->triggerlist : NIL;
}

BuildEventTriggerCachepg_event_trigger를 이름 순으로 스캔해(발화 순서를 결정론적으로 만들기 위해) 비활성화된 트리거를 건너뛰고, 이벤트 이름을 EventTriggerEvent로 디코딩하고, 저장된 evttags 텍스트 배열을 DecodeTextArrayToBitmapset으로 Bitmapset에 컴파일한다. 이름 순 스캔에 systable_beginscan_ordered가 필요하므로 인덱스가 온전해야 한다. 이것이 이벤트 트리거가 standalone 모드에서 비활성화되는 이유다.

sql_drop과 드롭된 객체 사이드 채널

섹션 제목: “sql_drop과 드롭된 객체 사이드 채널”

sql_drop 이벤트는 “이 명령이 무엇을 드롭했는가?”라는 질문에 답한다. dependency.c가 드롭을 마치면 카탈로그 행이 사라지므로, 그 답은 실행 중에 포착해야 한다. 메커니즘은 사이드 채널이다. 명령이 실행되는 동안 dependency.c가 드롭하는 객체마다 EventTriggerSQLDropAddObject를 호출해 SQLDropObject를 현재 상태의 SQLDropList에 추가한다. 수집기가 행이 사라지기 전에 객체의 신원, 유형, 스키마, 이름을 포착한다.

// EventTriggerSQLDropAddObject (excerpt) — src/backend/commands/event_trigger.c
if (!currentEventTriggerState)
return;
Assert(EventTriggerSupportsObject(object));
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
obj = palloc0(sizeof(SQLDropObject));
obj->address = *object;
obj->original = original;
obj->normal = normal;
/* ... special-case temp namespaces, column defaults, triggers, policies ... */
/* object identity, objname and objargs */
obj->objidentity =
getObjectIdentityParts(&obj->address, &obj->addrnames, &obj->addrargs, false);
obj->objecttype = getObjectTypeDescription(&obj->address, false);
slist_push_head(&(currentEventTriggerState->SQLDropList), &obj->next);
MemoryContextSwitchTo(oldcxt);

EventTriggerSQLDrop이 발화할 때 in_sql_drop = true를 설정(PG_TRY 아래에서 항상 초기화)하고 트리거를 실행한다. 트리거 함수는 pg_event_trigger_dropped_objects를 호출해 수집된 목록에 접근한다. 이 함수는 sql_drop 트리거 안에서 호출되지 않으면 오류를 낸다.

// pg_event_trigger_dropped_objects (excerpt) — src/backend/commands/event_trigger.c
if (!currentEventTriggerState ||
!currentEventTriggerState->in_sql_drop)
ereport(ERROR,
(errcode(ERRCODE_E_R_I_E_EVENT_TRIGGER_PROTOCOL_VIOLATED),
errmsg("%s can only be called in a sql_drop event trigger function",
"pg_event_trigger_dropped_objects()")));
InitMaterializedSRF(fcinfo, 0);
slist_foreach(iter, &(currentEventTriggerState->SQLDropList))
{
SQLDropObject *obj = slist_container(SQLDropObject, next, iter.cur);
/* emit (classid, objid, objsubid, original, normal, is_temporary,
object_type, schema_name, object_name, object_identity,
address_names, address_args) */
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
}

DDL 명령 수집과 pg_event_trigger_ddl_commands

섹션 제목: “DDL 명령 수집과 pg_event_trigger_ddl_commands”

가장 풍부한 기능은 명령 수집이다. 각 DDL 명령의 구조화된 표현을 포착해 ddl_command_end 트리거가 어떤 객체가 어떻게 변경됐는지 검사할 수 있게 한다. 이것이 범용 스키마 복제를 가능하게 하는 기반이다. 포착 단위는 CollectedCommand로, 별개 처리가 필요한 명령 종류를 태그된 공용체로 표현한다.

// CollectedCommand (excerpt) — src/include/tcop/deparse_utility.h
typedef enum CollectedCommandType
{
SCT_Simple, SCT_AlterTable, SCT_Grant, SCT_AlterOpFamily,
SCT_AlterDefaultPrivileges, SCT_CreateOpClass, SCT_AlterTSConfig,
} CollectedCommandType;
typedef struct CollectedCommand
{
CollectedCommandType type;
bool in_extension;
Node *parsetree;
union {
struct { ObjectAddress address; ObjectAddress secondaryObject; } simple;
struct { Oid objectId; Oid classId; List *subcmds; } alterTable;
struct { InternalGrant *istmt; } grant;
/* ... opfam, createopc, atscfg, defprivs ... */
} d;
struct CollectedCommand *parent; /* when nested */
} CollectedCommand;

대부분의 명령은 단순 경로를 밟는다. ProcessUtilitySlow가 명령이 생성하거나 변경한 것의 ObjectAddress와 함께 EventTriggerCollectSimpleCommand를 호출한다. 수집기가 파스 트리를 복사(명령 이후에도 살아있도록)하고 SCT_Simple 레코드를 commandList에 추가한다.

// EventTriggerCollectSimpleCommand (excerpt) — src/backend/commands/event_trigger.c
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
command = palloc(sizeof(CollectedCommand));
command->type = SCT_Simple;
command->in_extension = creating_extension;
command->d.simple.address = address;
command->d.simple.secondaryObject = secondaryObject;
command->parsetree = copyObject(parsetree);
currentEventTriggerState->commandList =
lappend(currentEventTriggerState->commandList, command);
MemoryContextSwitchTo(oldcxt);

ALTER TABLE은 특별하다. 하나의 구문이 여러 하위 명령(컬럼 추가, 기본값 설정, 제약 추가)을 담고, 관계 OID가 처리 도중에야 알려질 수 있다. 세 단계 프로토콜을 사용한다. EventTriggerAlterTableStartSCT_AlterTable 레코드를 currentCommand(목록에 아직 없는 대기 슬롯)에 올리고, EventTriggerAlterTableRelid가 OID를 채우고, EventTriggerCollectAlterTableSubcmd가 각 하위 명령의 ObjectAddress를 추가하고, EventTriggerAlterTableEnd가 하위 명령이 실제로 있는 경우에만 레코드를 commandList로 옮긴다.

// EventTriggerAlterTableEnd (excerpt) — src/backend/commands/event_trigger.c
parent = currentEventTriggerState->currentCommand->parent;
/* If no subcommands, don't collect */
if (currentEventTriggerState->currentCommand->d.alterTable.subcmds != NIL)
{
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
currentEventTriggerState->commandList =
lappend(currentEventTriggerState->commandList,
currentEventTriggerState->currentCommand);
MemoryContextSwitchTo(oldcxt);
}
else
pfree(currentEventTriggerState->currentCommand);
currentEventTriggerState->currentCommand = parent;

parent 포인터가 currentCommand도 스택으로 만든다. 중첩된 ALTER TABLE이 올바르게 중첩된다. 단순 또는 alter-table 형태에 맞지 않는 명령은 자체 수집기가 있다. EventTriggerCollectGrant(GRANT/REVOKE), EventTriggerCollectAlterOpFam, EventTriggerCollectCreateOpClass, EventTriggerCollectAlterTSConfig, EventTriggerCollectAlterDefPrivs다. 이중 계산을 방지해야 하는 코드 경로를 위한 금지/해제 쌍(EventTriggerInhibitCommandCollection)도 있다.

ddl_command_end 트리거는 pg_event_trigger_ddl_commands를 호출해 전체 목록을 읽는다. 이 함수가 commandList를 순회하며 각 레코드의 객체 신원, 유형, 스키마를 재구성하고 마지막 컬럼에 불투명한 pg_ddl_command 값(CollectedCommand 포인터 자체)을 담은 행을 출력한다. 확장의 디파스(deparse) 함수가 이를 실행 가능한 SQL로 변환한다.

// pg_event_trigger_ddl_commands (excerpt) — src/backend/commands/event_trigger.c
if (!currentEventTriggerState)
ereport(ERROR,
(errcode(ERRCODE_E_R_I_E_EVENT_TRIGGER_PROTOCOL_VIOLATED),
errmsg("%s can only be called in an event trigger function",
"pg_event_trigger_ddl_commands()")));
InitMaterializedSRF(fcinfo, 0);
foreach(lc, currentEventTriggerState->commandList)
{
CollectedCommand *cmd = lfirst(lc);
/* IF NOT EXISTS no-op: skip records with InvalidOid object */
if (cmd->type == SCT_Simple && !OidIsValid(cmd->d.simple.address.objectId))
continue;
switch (cmd->type) { /* SCT_Simple/AlterTable/... vs Grant vs DefPrivs */ }
/* last value is PointerGetDatum(cmd) -> type pg_ddl_command */
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
}
flowchart TB
  A["EventTriggerBeginCompleteQuery<br/>드롭 추적 필요 시 상태 올리기"] --> B["DDL 실행"]
  B --> C["단순 명령<br/>EventTriggerCollectSimpleCommand<br/>SCT_Simple -> commandList"]
  B --> D["ALTER TABLE<br/>AlterTableStart / Relid /<br/>CollectAlterTableSubcmd / AlterTableEnd"]
  B --> E["GRANT, OPFAMILY, OPCLASS,<br/>TS CONFIG, DEF PRIVS<br/>전용 수집기"]
  C --> F["ddl_command_end 트리거가<br/>pg_event_trigger_ddl_commands 호출"]
  D --> F
  E --> F
  F --> G["commandList 순회<br/>pg_ddl_command 값 포함 행 출력"]
  G --> H["확장 디파스 함수가<br/>pg_ddl_command를 SQL 텍스트로 변환"]

그림 2 — DDL 명령 수집. 실행 중 각 명령 유형이 해당 EventTriggerCollect* 호출로 CollectedCommand를 명령별 commandList에 추가한다. ddl_command_end 트리거가 pg_event_trigger_ddl_commands로 전체 목록을 읽는다. (event_trigger.c의 수집 루틴과 utility.c의 호출 지점 흐름.)

login 이벤트와 빠른 경로 플래그

섹션 제목: “login 이벤트와 빠른 경로 플래그”

login 이벤트는 모든 새 세션에서 발화한다. 이것은 핫 경로이므로 PostgreSQL은 로그인 트리거가 없는 데이터베이스의 연결에서 이벤트 트리거 캐시 빌드 비용을 치르지 않는다. pg_database.dathasloginevt 불리언 컬럼이 현재 데이터베이스에 활성화된 로그인 트리거가 있는지 기록한다. EventTriggerOnLogin은 캐시된 MyDatabaseHasLoginEventTriggers 플래그를 확인하고 false면 즉시 반환한다.

// EventTriggerOnLogin (excerpt) — src/backend/commands/event_trigger.c
if (!IsUnderPostmaster || !event_triggers ||
!OidIsValid(MyDatabaseId) || !MyDatabaseHasLoginEventTriggers)
return;
StartTransactionCommand();
runlist = EventTriggerCommonSetup(NULL, EVT_Login, "login", &trigdata, false);
if (runlist != NIL)
{
PushActiveSnapshot(GetTransactionSnapshot());
EventTriggerInvoke(runlist, &trigdata);
list_free(runlist);
PopActiveSnapshot();
}

CreateEventTrigger(그리고 활성화 시 AlterEventTrigger)가 SetDatabaseHasLoginEventTriggersdathasloginevt 플래그를 설정한다. 플래그는 드롭 시 고착된다. 로그인 트리거가 드롭될 때 지워지지 않는다. 복수 트리거 케이스에서 드롭 경로를 복잡하게 만들기 때문이다. EventTriggerOnLogin이 플래그가 설정됐으나 실제 로그인 트리거가 없을 때 기회적으로 플래그를 지운다. 조건부 잠금을 취해 연결을 막지 않고, hot standby에서는 지우기를 완전히 건너뛴다(잠금을 취할 수 없고 플래그가 주 서버의 WAL 재생으로 수정되므로). 정확한 부기가 아닌 자가 치유 최적화다.

이벤트 트리거 기계는 거의 전부 파일 하나 — src/backend/commands/event_trigger.c(~2400행) — 와 syscache 기반 캐시인 evtcache.c에 들어 있다. 발화 호출 지점은 반대로 산재해 있다. DDL 이벤트가 의미론적으로 일어나는 곳 어디에나 있다(utility.c에 DDL 명령 괄호, tablecmds.c에 테이블 재작성, postgres.c에 로그인, dependency.c에 드롭). “라이브러리 하나, 수작업으로 배치된 다수의 호출 지점”이 서브시스템 전체를 관통하는 구조다. 워크스루는 그 분리를 기준으로 심볼을 묶는다.

카탈로그 DDL — 이벤트 트리거 생성과 변경 (event_trigger.c)

섹션 제목: “카탈로그 DDL — 이벤트 트리거 생성과 변경 (event_trigger.c)”

CreateEventTriggerCREATE EVENT TRIGGER의 진입점이다. 슈퍼유저를 강제하고, 이벤트 이름을 다섯 가지 유효 철자와 대조해 검증하고, WHEN 필터 목록을 검증(validate_ddl_tags, 태그별로 command_tag_event_trigger_ok에 위임)하고, 트리거 함수의 반환 타입이 event_trigger 의사 타입인지 확인하고, insert_event_trigger_tuplepg_event_trigger 행을 삽입한다. login 이벤트에는 추가로 SetDatabaseHasLoginEventTriggers를 호출해 pg_database.dathasloginevt 빠른 경로 플래그를 설정한다.

// CreateEventTrigger / insert_event_trigger_tuple (excerpt) — src/backend/commands/event_trigger.c
/* Login event triggers are flagged on the database. */
if (strcmp(stmt->eventname, "login") == 0)
SetDatabaseHasLoginEventTriggers();
return insert_event_trigger_tuple(stmt->trigname, stmt->eventname,
evtowner, funcoid, tags);

AlterEventTriggerALTER EVENT TRIGGER ... ENABLE/DISABLE을 처리한다. evtenabledTRIGGER_DISABLED, TRIGGER_FIRES_ON_ORIGIN, TRIGGER_FIRES_ON_REPLICA, TRIGGER_FIRES_ALWAYS 사이에서 전환한다. 이것은 일반 트리거에도 쓰이는 'D'/'O'/'R'/'A' 인코딩이고, filter_event_triggerSessionReplicationRole과 비교하는 값이다. 로그인 트리거를 활성화할 때도 데이터베이스 플래그를 설정한다.

발화 함수와 공통 경로 (event_trigger.c)

섹션 제목: “발화 함수와 공통 경로 (event_trigger.c)”

다섯 가지 공개 EventTrigger* 발화 함수는 각각 이벤트별 전처리를 수행한 뒤 EventTriggerCommonSetup으로 수렴한다.

  • EventTriggerDDLCommandStart — 카탈로그 준비 전(currentEventTriggerState 검사)에 실행되지 않도록 가드한 뒤 EVT_DDLCommandStart를 발화한다.
  • EventTriggerDDLCommandEndEVT_DDLCommandEnd를 발화한다. 이 이벤트의 트리거가 pg_event_trigger_ddl_commands를 호출할 수 있다.
  • EventTriggerSQLDropin_sql_drop을 설정하고 EVT_SQLDrop을 발화하며 PG_TRY 아래에서 항상 플래그를 초기화한다.
  • EventTriggerTableRewritepg_event_trigger_table_rewrite_oid / _reason이 읽을 수 있도록 table_rewrite_oid / table_rewrite_reason을 보관한 뒤 EVT_TableRewrite를 발화한다.
  • EventTriggerOnLogin — 위에서 설명한 조기 반환 빠른 경로. 로그인이 일반 구문 루프 밖에서 발화하므로 자체 트랜잭션에서 실행된다.

EventTriggerCommonSetup(캐시 프로브 + 필터 + 노드 빌드)과 EventTriggerInvoke(함수 사이 CommandCounterIncrement를 포함한 fmgr 루프)가 공유 핵심이다. filter_event_trigger가 트리거별 술어고, EventTriggerGetTag가 파스 트리를 필터 검사용 CommandTag로 해석한다(로그인 이벤트는 명령이 없으므로 CMDTAG_UNKNOWN을 반환).

명령별 상태 생명주기 (event_trigger.c)

섹션 제목: “명령별 상태 생명주기 (event_trigger.c)”

EventTriggerBeginCompleteQuery가 새 EventTriggerQueryStatecurrentEventTriggerState 스택에 올리고(자체 MemoryContext 할당) 정리가 필요한지 반환한다. EventTriggerEndCompleteQuery가 꺼내고 컨텍스트를 MemoryContextDelete한다. 올리기는 조건부다. 완전한 최상위 쿼리에서만 일어나므로 하위 명령은 외부 상태를 재사용한다. 쌍은 항상 호출 지점의 PG_TRY/PG_FINALLY 안에 놓인다. 명령 본문에서 오류가 나도 상태가 누출되지 않는다.

// EventTriggerBeginCompleteQuery (excerpt) — src/backend/commands/event_trigger.c
/* Currently, we don't allow nested event-trigger query states. */
if (currentEventTriggerState)
return false;
cxt = AllocSetContextCreate(TopMemoryContext, "event trigger state",
ALLOCSET_DEFAULT_SIZES);
state = MemoryContextAllocZero(cxt, sizeof(EventTriggerQueryState));
state->cxt = cxt;
slist_init(&(state->SQLDropList));
state->in_sql_drop = false;
state->table_rewrite_oid = InvalidOid;
state->commandList = NIL;
state->previous = currentEventTriggerState;
currentEventTriggerState = state;
return true;

SQL 함수가 읽는 사이드 채널 (event_trigger.c)

섹션 제목: “SQL 함수가 읽는 사이드 채널 (event_trigger.c)”

트리거에서 보이는 SQL 함수들이 두 사이드 채널의 읽기 측이다.

  • pg_event_trigger_dropped_objectsSQLDropList를 순회한다. in_sql_drop 검사로 sql_drop 이벤트에서만 호출 가능하다.
  • pg_event_trigger_ddl_commandscommandList를 순회한다. 살아있는 currentEventTriggerState가 필요하다.
  • pg_event_trigger_table_rewrite_oid / pg_event_trigger_table_rewrite_reason — 보관된 테이블 재작성 OID/이유를 반환한다. table_rewrite 이벤트에서만 호출 가능하다.

쓰기 측은 EventTriggerSQLDropAddObject(객체 드롭마다 dependency.c에서 호출)와 EventTriggerCollect* 패밀리(영향받은 객체마다 DDL 실행 경로에서 호출)다.

명령 수집 (event_trigger.c + deparse_utility.h)

섹션 제목: “명령 수집 (event_trigger.c + deparse_utility.h)”

EventTriggerCollectSimpleCommand가 일반 수집기다. ALTER TABLE 프로토콜은 EventTriggerAlterTableStartEventTriggerAlterTableRelidEventTriggerCollectAlterTableSubcmdEventTriggerAlterTableEnd를 사용한다. 특수 형태 수집기는 EventTriggerCollectGrant, EventTriggerCollectAlterOpFam, EventTriggerCollectCreateOpClass, EventTriggerCollectAlterTSConfig, EventTriggerCollectAlterDefPrivs다. EventTriggerInhibitCommandCollection / EventTriggerUndoInhibitCommandCollection이 이중 계산 방지가 필요한 코드 경로를 괄호로 감싼다. CollectedCommand 태그된 공용체는 deparse_utility.h에 있다.

EventCacheLookup이 유일한 공개 진입점이다. 백엔드별 캐시가 오래됐으면 BuildEventTriggerCache를 지연 호출한다. BuildEventTriggerCachepg_event_trigger를 이름 순으로 스캔하고(systable_beginscan_ordered 사용, 온전한 인덱스 필요, standalone 모드에서 이벤트 트리거가 꺼지는 이유), 비활성화된 행을 건너뛰고, 이벤트 이름을 EventTriggerEvent에 매핑하고, evttagsDecodeTextArrayToBitmapset으로 Bitmapset에 컴파일한다. InvalidateEventCacheCallback(pg_event_trigger syscache에 등록)이 캐시를 ETCS_NEEDS_REBUILD로 전환해 다음 조회 시 재빌드한다.

// InvalidateEventCacheCallback (excerpt) — src/backend/utils/cache/evtcache.c
static void
InvalidateEventCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
{
/*
* If the cache isn't valid, then there might be a rebuild in progress, so
* we can't immediately blow it away. But if it is valid, then recursive
* rebuilds are impossible, so we can immediately reset it.
*/
if (EventTriggerCacheState == ETCS_VALID)
EventTriggerCacheState = ETCS_NEEDS_REBUILD;
}

발화 호출 지점 (event_trigger.c 바깥)

섹션 제목: “발화 호출 지점 (event_trigger.c 바깥)”

“이벤트가 실제로 어디서 일어나는가”에 해당하는 네 호출 지점이 독자가 가장 놓치기 쉬운 부분이므로 명시적 위치 힌트를 준다.

  • ProcessUtilitySlow (utility.c) — ddl_command_start / sql_drop / ddl_command_end 괄호와 대부분의 EventTriggerCollect* 호출을 소유한다. 모두 하나의 PG_TRY 안에 있다.
  • ATRewriteTables (tablecmds.c) — 힙 재작성 직전에 한 번 발화하는 단일 EventTriggerTableRewrite 호출이다.
  • PostgresMain (postgres.c) — 인증 후 메시지 루프 전의 단일 EventTriggerOnLogin 호출이다.
  • deleteOneObject / 드롭 경로 (dependency.c) — EventTriggerSQLDropAddObject를 호출한다. postgres-ddl-execution.md가 소유한다.

위치 힌트 (2026-06-05 기준, REL_18 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심볼파일
EventTriggerEvent (enum)src/include/utils/evtcache.h20
EventTriggerCacheItemsrc/include/utils/evtcache.h29
EventCacheLookup (decl)src/include/utils/evtcache.h36
EventTriggerDatasrc/include/commands/event_trigger.h24
CALLED_AS_EVENT_TRIGGERsrc/include/commands/event_trigger.h49
CreateEventTriggersrc/backend/commands/event_trigger.c124
validate_ddl_tagssrc/backend/commands/event_trigger.c216
SetDatabaseHasLoginEventTriggerssrc/backend/commands/event_trigger.c390
AlterEventTriggersrc/backend/commands/event_trigger.c427
filter_event_triggersrc/backend/commands/event_trigger.c598
EventTriggerCommonSetupsrc/backend/commands/event_trigger.c638
EventTriggerDDLCommandStartsrc/backend/commands/event_trigger.c725
EventTriggerDDLCommandEndsrc/backend/commands/event_trigger.c776
EventTriggerSQLDropsrc/backend/commands/event_trigger.c824
EventTriggerOnLoginsrc/backend/commands/event_trigger.c897
EventTriggerTableRewritesrc/backend/commands/event_trigger.c1011
EventTriggerInvokesrc/backend/commands/event_trigger.c1076
EventTriggerBeginCompleteQuerysrc/backend/commands/event_trigger.c1191
EventTriggerEndCompleteQuerysrc/backend/commands/event_trigger.c1235
EventTriggerSQLDropAddObjectsrc/backend/commands/event_trigger.c1285
pg_event_trigger_dropped_objectssrc/backend/commands/event_trigger.c1532
EventTriggerCollectSimpleCommandsrc/backend/commands/event_trigger.c1723
EventTriggerAlterTableStartsrc/backend/commands/event_trigger.c1761
EventTriggerAlterTableEndsrc/backend/commands/event_trigger.c1848
EventTriggerCollectGrantsrc/backend/commands/event_trigger.c1886
pg_event_trigger_ddl_commandssrc/backend/commands/event_trigger.c2063
BuildEventTriggerCachesrc/backend/utils/cache/evtcache.c77
EventCacheLookupsrc/backend/utils/cache/evtcache.c63
EventTriggerBeginCompleteQuery (call)src/backend/tcop/utility.c1110
EventTriggerDDLCommandStart (call)src/backend/tcop/utility.c1116
EventTriggerSQLDrop (call)src/backend/tcop/utility.c1932
EventTriggerDDLCommandEnd (call)src/backend/tcop/utility.c1933
EventTriggerEndCompleteQuery (call)src/backend/tcop/utility.c1939
EventTriggerTableRewrite (call)src/backend/commands/tablecmds.c5962
EventTriggerOnLogin (call)src/backend/tcop/postgres.c4373
  • 이벤트 유형은 정확히 다섯 가지이며 evtcache.h에 열거된다. EventTriggerEvent 열거형(EVT_DDLCommandStart, EVT_DDLCommandEnd, EVT_SQLDrop, EVT_TableRewrite, EVT_Login)을 읽고, CreateEventTrigger가 다섯 SQL 철자(ddl_command_start, ddl_command_end, sql_drop, login, table_rewrite) 외의 eventname을 거부함을 확인했다. PG19 전용 이벤트 없음. 이 집합은 REL_18 기준이다.

  • EventTriggerDataNodeTag 외에 정확히 세 개의 페이로드 필드를 갖는다. event_trigger.h에서 확인: event(이름), parsetree, tag. 더 풍부한 데이터는 SQL 함수를 직접 호출해야만 접근 가능하다. CALLED_AS_EVENT_TRIGGERIsA(context, EventTriggerData)를 검사한다.

  • DDL 괄호는 범용 디스패처가 아닌 ProcessUtilitySlow에서 발화한다. utility.c에서 확인: 상단에 EventTriggerBeginCompleteQuery, isCompleteQuery로 가드된 EventTriggerDDLCommandStart, 그 뒤에 EventTriggerSQLDropEventTriggerDDLCommandEnd가 나란히, PG_FINALLYEventTriggerEndCompleteQuery. sql_dropddl_command_end 전에 발화한다(인접한 1932/1933행).

  • isCompleteQuery가 하위 명령의 이벤트 발화를 억제한다. 확인됨: isCompleteQuery = (context != PROCESS_UTILITY_SUBCOMMAND), 블록 안의 모든 발화 호출이 이것으로 가드된다. 재귀적으로 발행된 하위 명령은 괄호를 다시 발화하지 않는다.

  • table_rewriteATRewriteTables에서 정확히 한 번 발화한다. tablecmds.c에서 확인: 호출 앞에 “And fire it only once” 주석이 있고 tab->relidtab->rewrite를 OID와 이유로 전달한다.

  • login은 인증 후 PostgresMain에서 한 번 발화한다. postgres.c에서 확인: MemoryContextSwitchTo(TopMemoryContext) 이후 단일 EventTriggerOnLogin() 호출. EventTriggerOnLogin 자체는 IsUnderPostmaster && event_triggers && OidIsValid(MyDatabaseId) && MyDatabaseHasLoginEventTriggers가 아니면 조기 반환한다.

  • 공통 경로는 캐시 프로브 → 필터 → 호출이다. EventTriggerCommonSetupEventCacheLookup(event)를 호출하고 트리거가 없으면 즉시 NIL을 반환한다. 그렇지 않으면 SessionReplicationRoleWHEN tag Bitmapset(filter_event_trigger)으로 필터링하고 EventTriggerData 노드를 빌드한다. EventTriggerInvoke가 각 살아남은 함수를 사이에 CommandCounterIncrement를 두고 실행한다.

  • 캐시는 이벤트별, 이름 순, syscache 무효화 방식이다. evtcache.c에서 확인: BuildEventTriggerCachepg_event_trigger를 순서대로 스캔(결정론적 발화 순서)하고, InvalidateEventCacheCallbackETCS_VALIDETCS_NEEDS_REBUILD로 전환한다(진행 중인 재빌드에서 살아남기 위해 그 자리에서 해제하지 않음).

  • 두 데이터 사이드 채널은 실행 중 쓰고 트리거에서 읽는다. EventTriggerSQLDropAddObjectdependency.c가 객체를 드롭하는 동안 SQLDropList에 추가하고, EventTriggerCollectSimpleCommand / ALTER TABLE 프로토콜 / 특수 수집기들이 commandListCollectedCommand를 추가한다. pg_event_trigger_dropped_objectspg_event_trigger_ddl_commands가 읽으며, 각각 이벤트 위반 시 ERRCODE_E_R_I_E_EVENT_TRIGGER_PROTOCOL_VIOLATED 오류를 낸다.

  • login 트리거에는 데이터베이스 수준 빠른 경로 플래그가 있다. 확인됨: pg_database.dathasloginevtCreateEventTrigger/AlterEventTriggerSetDatabaseHasLoginEventTriggers로 설정되고, 백엔드별로 MyDatabaseHasLoginEventTriggers에 캐시되며, EventTriggerOnLogin에서 기회적으로 자가 치유된다(조건부 잠금 지우기, standby에서 건너뜀).

  1. 경쟁 상황에서 dathasloginevt 자가 치유 지우기의 최악 비용. EventTriggerOnLogin이 오래된 플래그를 지우기 위해 조건부 잠금을 취하고 실패 시 조용히 건너뛴다. 따라서 마지막 로그인 트리거를 방금 드롭한 데이터베이스에 동시 로그인이 몰리면, 플래그가 조건부 잠금을 따낸 하나가 나올 때까지 설정된 채로 남아 모든 로그인이 캐시 빌드 비용을 치를 수 있다. 실제 발생 빈도는 측정하지 않았다. 조사 방향: 연결 폭풍 워크로드 아래에서 지우기 경로를 계측한다.

  2. 이색적인 DDL에 대한 명령 수집 커버리지. CollectedCommand 공용체는 ALTER TABLE, GRANT, 연산자 패밀리/클래스, 텍스트 검색 설정, 기본 권한을 특별 케이스로 처리하고 나머지는 SCT_Simple 경로로 단일 ObjectAddress를 쓴다. 카탈로그를 건드리는 모든 REL_18 유틸리티 구문이 신뢰할 수 있는 레코드를 만드는지(vs. pg_event_trigger_ddl_commands가 건너뛰는 InvalidOid 주소 no-op) 철저히 검증하지 않았다. 조사 방향: ProcessUtilitySlownodeTag 케이스와 수집기 호출을 대조한다.

  3. 중첩된 이벤트 트리거 상태와 명령 수집의 상호작용. EventTriggerBeginCompleteQuery가 현재 중첩을 거부하므로(if (currentEventTriggerState) return false;), 트리거 안에서 발행된 DDL 명령은 새 상태를 올리지 않고 외부 상태를 재사용한다. 그러나 EventTriggerQueryState.previousCollectedCommand.parent 모두 중첩이 지원되는 것처럼 존재한다. REL_18에서 previous/parent 체인이 실제로 실행되는 정확한 상황을 집중적으로 추적할 필요가 있다.

PostgreSQL 너머 — 비교 설계와 연구 전선

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 전선”
  • Oracle 시스템/DDL 트리거. Oracle의 BEFORE/AFTER 시스템 이벤트(CREATE, ALTER, DROP, LOGON, LOGOFF, SERVERERROR, STARTUP/SHUTDOWN)가 가장 유사한 대응물이다. LOGON 이벤트는 PostgreSQL의 login에 직접 대응한다. Oracle은 PostgreSQL의 pg_event_trigger_* SRF와 같은 역할을 하는 속성 함수(ora_dict_obj_name, ora_sysevent)를 노출한다. Oracle의 속성 함수 모델과 PostgreSQL의 사이드 채널 + SRF 모델을 나란히 놓으면 “컨텍스트를 트리거에 의사 컬럼으로 푸시”와 “트리거가 명령별 상태에서 구조화된 컨텍스트를 풀(pull)“의 트레이드오프가 선명해진다.

  • SQL Server DDL 트리거와 EVENTDATA(). SQL Server는 DDL 트리거를 이벤트 그룹(DDL_TABLE_EVENTS, DDL_LOGIN_EVENTS, …)으로 키하고 타입 있는 노드 대신 EVENTDATA() XML 블롭을 건넨다. PostgreSQL의 선택 — 세 개의 저렴한 필드에 타입 있는 EventTriggerData 노드를, 비싼 목록에 지연 구체화 SRF — 은 의도적으로 다른 지점이다. 트리거가 검사하지 않을 수 있는 설명을 직렬화하는 비용을 피한다.

  • DDL 복제와 스키마의 논리적 디코딩. 명령 수집은 범용 스키마 복제를 가능하게 하기 위해 존재한다. 확장이 pg_event_trigger_ddl_commands를 읽고, 각 pg_ddl_command를 SQL 텍스트로 디파스하고, 전달한다. 이것이 정확히 Kleppmann의 Designing Data-Intensive Applications(11장, change capture; raw/system/textbooks/)가 로그 기반 복제에서 지목한 스키마 변경 포착 간극이다. 논리적 DDL 복제에 대한 프런티어 작업(PostgreSQL 논리 복제 로드맵과 여러 확장에 내장)이 자연스러운 계속이다. 디파스 절반은 deparse_utility.h / ddl_deparse가 소유하며 여기서는 다루지 않는다.

  • System R과 카탈로그-테이블 계보. 이벤트 트리거는 카탈로그 변경에 발화한다. 카탈로그가 변경 가능하고 관찰 가능한 이유는 System R의 메타데이터를 일반 릴레이션에 저장하기로 한 결정까지 거슬러 올라간다(Astrahan et al. 1976; dbms-papers/systemr.md). 이벤트 트리거는 어떤 의미에서 카탈로그 행 단위가 아닌 DDL 명령 단위로 표현된 시스템 카탈로그에 대한 DML 트리거다. 이 프레임이 이 문서를 postgres-triggers.md의 ECA 트리거 이론과 연결한다.

  • ECA 규칙과 능동 데이터베이스. 이벤트-조건-액션 모델(Silberschatz 7판 §5.3)은 1990년대 “능동 데이터베이스” 연구 문헌(HiPAC, Ariel, Starburst)의 주제였다. 이벤트 트리거는 그 비전의 좁고 실전에서 단련된 단면이다. 이벤트는 DDL과 세션 생명주기로, 조건은 태그 필터와 복제 역할로 제한된다. PostgreSQL의 다섯 이벤트를 일반 ECA 분류법에 매핑하고(그리고 의도적으로 생략된 것 — 시간 이벤트, 복합 이벤트, 결합 모드 — 을 명시하면) 깔끔한 이론 동반 문서가 될 것이다.

소스 파일 (REL_18_STABLE, commit 273fe94)

섹션 제목: “소스 파일 (REL_18_STABLE, commit 273fe94)”
  • src/backend/commands/event_trigger.c — 전체 서브시스템: CreateEventTrigger/AlterEventTrigger, 다섯 EventTrigger* 발화 함수, EventTriggerCommonSetup/EventTriggerInvoke/filter_event_trigger, EventTriggerQueryState 생명주기(Begin/EndCompleteQuery), 드롭 사이드 채널(EventTriggerSQLDropAddObject, pg_event_trigger_dropped_objects), 명령 수집(EventTriggerCollect*/AlterTable* 패밀리, pg_event_trigger_ddl_commands).
  • src/include/commands/event_trigger.hEventTriggerDataCALLED_AS_EVENT_TRIGGER 가드 매크로.
  • src/include/utils/evtcache.hEventTriggerEvent 열거형, EventTriggerCacheItem, EventCacheLookup 선언.
  • src/backend/utils/cache/evtcache.cEventCacheLookup, BuildEventTriggerCache, DecodeTextArrayToBitmapset, InvalidateEventCacheCallback.
  • src/include/tcop/deparse_utility.hCollectedCommand 태그된 공용체와 CollectedCommandType.
  • src/backend/tcop/utility.cProcessUtilitySlow: DDL 괄호 호출 지점과 대부분의 EventTriggerCollect* 호출, 하나의 PG_TRY 안에.
  • src/backend/commands/tablecmds.cATRewriteTables: 단일 EventTriggerTableRewrite 호출 지점.
  • src/backend/tcop/postgres.cPostgresMain: 단일 EventTriggerOnLogin 호출 지점.
  • Database System Concepts (Silberschatz, Korth, Sudarshan, 7판), 5장 “Advanced SQL”, §5.3 “Triggers” — 이벤트-조건-액션 모델과 연쇄/암묵적 트리거 경고 (knowledge/research/dbms-general/).
  • Astrahan, M. M. et al. (1976). “System R: Relational Approach to Database Management.” ACM TODS 1(2):97-137 — 카탈로그-릴레이션, 관찰 가능한 카탈로그 변경의 계보 (knowledge/research/dbms-papers/systemr.md).
  • Kleppmann, M. (2017). Designing Data-Intensive Applications, 11장(change capture) — DDL 명령 수집이 답하는 스키마 변경 복제 간극 (raw/system/textbooks/).

형제 문서 (상호 참조 — 소유권은 해당 문서에)

섹션 제목: “형제 문서 (상호 참조 — 소유권은 해당 문서에)”
  • postgres-triggers.md — 행/구문(DML) 트리거: TriggerData, ExecCallTriggerFunc, 이벤트 트리거가 의도적으로 아닌 SQL 표준 BEFORE/AFTER 모델.
  • postgres-ddl-execution.mdProcessUtility/ProcessUtilitySlow 디스패치, 구문별 DDL 실행 경로, EventTriggerSQLDropAddObject에 데이터를 공급하는 dependency.c 드롭 기계.
  • postgres-architecture-overview.md — 전체 백엔드 파이프라인에서 유틸리티 명령 처리와 카탈로그의 위치.