(KO) CUBRID 트리거 — ECA 능동 규칙, 문장 vs 행 단위, 그리고 locator 인접 firing 경로
목차
학술적 배경
섹션 제목: “학술적 배경”트리거는 스키마가 데이터에 반응 하게 만드는 장치다. Widom과
Ceri가 Active Database Systems (1996) 에서 정리한 ECA 모델
이 그 형식을 굳혔다. 능동 규칙은 이벤트 (INSERT, UPDATE,
DELETE, COMMIT 등), 조건 (행 또는 트랜잭션 상태에 대한 불리언
식), 그리고 액션 (SQL 문, 프로시저 호출, 또는 REJECT 같은
메타심볼) 세 요소의 묶음이다. Database System Concepts
(Silberschatz et al., 7판, 5장) 도 같은 삼중 구조를 SQL-99 문법
으로 표현한다. CREATE TRIGGER … BEFORE/AFTER … FOR EACH ROW WHEN … BEGIN … END.
교과서는 다섯 가지 재료가 모든 구현을 빚는다고 정리한다.
-
단위 — 행 vs 문장. 행 단위 트리거는 영향을 받은 행마다 한 번씩,
OLD/NEW바인딩과 함께 발화된다. 문장 단위 트리거 는 행 개수와 무관하게 DML 문장당 한 번 발화된다. SQL-99 는FOR EACH ROW/FOR EACH STATEMENT로 가른다. MySQL은 8.0 까지 행 단위만 지원했고, Oracle, SQL Server, CUBRID은 firing 루프 안에서 둘을 구분한다. -
시점 — BEFORE, AFTER, INSTEAD OF, DEFERRED. 시점이 액션이 할 수 있는 일을 바꾼다. BEFORE 액션은 DML을 REJECT 할 수 있고, AFTER 액션은 변경된 행을 본다. INSTEAD OF 는 DML을 대체하며 (보통 view용), DEFERRED 는 COMMIT 시점에 실행된다.
-
재귀와 Mutating-Table 문제. 트리거 액션이 다시 다른 트리거 를 발화시킬 수 있고, 같은 테이블로 되돌아 올 수도 있다. 폭주 재귀를 막는 고전적 방법은 깊이 제한 (DB2의 16, Postgres의 50) 이다. 더 미묘한 Mutating-Table 문제 (Oracle의 명명) 는 행 단위 트리거의 액션이 문장 한가운데에서 자기 테이블을 조회하는 경우다. Oracle은 ORA-04091 을 던지고, Postgres는 deferred constraint 로 우회하며, CUBRID은 액션이 워크스페이스의 절반쯤 변경된 상태를 읽도록 허용하되 STATEMENT 단위 재귀는 OID 스택 으로 막는다.
-
즉시 vs. 지연 발화. 지연 액션은 트랜잭션마다 큐에 쌓였다가 COMMIT 시점에 의존성 순서로 비워지고, ROLLBACK 시 폐기된다. 여러 관련 행의 최종 상태를 액션이 봐야 할 때 본질적이다.
-
카탈로그 상주 vs. 컴파일. 트리거 메타데이터는 카탈로그 행에 산다 (
pg_trigger,mysql.triggers,_db_trigger). 액션 본문은 발화마다 파싱·계획되어야 하므로, 모든 엔진이 컴파 일된 형태를 캐시한다. CUBRID은SM_CLASS::triggers에 매달린TR_SCHEMA_CACHE를 두고, 첫 발화에서 액션의PT_NODE파스 트리를 lazy 컴파일한다.
여기서 두 엔지니어링 불변식이 떠오른다. 트리거는 발화하지
않는 비용이 싸야 한다 (CUBRID의 sm_active_triggers 가 트리거
없는 경우를 단락 처리한다). 그리고 한 이벤트에 N개 트리거가
달려 있다면 결정론적 순서로 정렬되어야 한다 (CUBRID은 숫자
priority 를 노출하고 이벤트별 리스트를 정렬해 둔다).
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”SQL-99 가 문법을 준다면, 이 절은 거의 모든 구현이 채택하는 엔지니어링 패턴을 호명한다. CUBRID의 구체적 선택은 그 공유 설계 공간 안의 한 다이얼 세트로 읽어야 한다.
Postgres
섹션 제목: “Postgres”PostgreSQL은 액션 본문을 트리거 바깥에 둔다. CREATE FUNCTION
이 trigger 타입을 반환하는 PL/pgSQL 함수를 만들고, CREATE TRIGGER 가 (이벤트, 시점, 단위, 함수) 를 테이블에 묶는다.
WHEN 절은 함수 호출 전에 executor 가 평가한다. 문장 단위 트리거
는 10.x 부터 transition table 로 NEW/OLD 를 본다. 저장소는
pg_trigger 행 + pg_proc 의 함수 본문이다. 글로벌 재귀 상한은
없다. 스택 오버플로까지 간다.
MySQL
섹션 제목: “MySQL”트리거가 문법적으로 인라인이다 (CREATE TRIGGER … FOR EACH ROW BEGIN … END). 8.0 이전에는 문장 단위 단위가 없었다. OLD/NEW
는 컬럼 이름 기반의 의사 행이다. 한 테이블의 (이벤트, 시점) 쌍
당 트리거가 하나만 허용된다. 유연성을 포기하고 순서 문제를
단순화한 선택이다.
Oracle
섹션 제목: “Oracle”CREATE OR REPLACE TRIGGER 가 BEFORE STATEMENT, BEFORE EACH ROW,
AFTER EACH ROW, AFTER STATEMENT 를 한 compound 트리거에 모은다. 패키지 상태를 공유하는 네 본문이다. mutating-table 문제에 대한
가장 깔끔한 답이다. Oracle은 행 단위 트리거의 액션이 자기 테이블
을 조회하면 ORA-04091 도 던진다.
SQL Server
섹션 제목: “SQL Server”INSTEAD OF 트리거는 BEFORE/AFTER 가 아니라 DML 자체 를 대체
한다. updatable view 의 메커니즘이다. CUBRID은 이를 지원하지
않는다. tr_create_trigger 가 가상 클래스를 거부한다 (db_is_vclass
가 ER_TR_NO_VCLASSES 를 반환).
CUBRID이 서 있는 자리
섹션 제목: “CUBRID이 서 있는 자리”CUBRID의 혈통은 OODB UniSQL/X 다. 그 함의는 다음과 같다.
- 트리거는 서버가 아니라 클라이언트 워크스페이스에서 발화한다.
모든 트리거 진입점이
src/object/에 산다.src/transaction/locator_sr.c에는 없다. dirty 객체가 LC_COPYAREA 에 패킹되기 전 에 발화가 일어난다. 같은 관찰을 반대편에서 본 것은cubrid-locator.md다. - 현재 객체는 행 식별자가 아니라 인메모리 MOP 다.
OLD/NEW가DB_OBJECT *핸들에 묶인다. 액션은 컴파일 시점 에 (obj, new, old) 같은 상관 이름을 참조하도록 재작성되며, 파서가 이 이름을 임시 템플릿으로 해석한다. - 문장 단위 트리거 (
TR_EVENT_STATEMENT_*) 는 OID 스택 (tr_check_recursivity) 으로 재귀가 통제된다. 재귀 발화를 조용히 건너뛸 뿐 에러는 띄우지 않는다. - DEFERRED 는 실제 시점이다. 트랜잭션마다
TR_DEFERRED_CONTEXT리스트가 쌓이고 COMMIT에서 비워진다. - 트리거가 DML을 거부할 수 있다.
TR_ACT_REJECT는 그 DML을 취소하고,TR_ACT_INVALIDATE는 트랜잭션 전체를 오염시킨다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론 개념 | CUBRID 명칭 |
|---|---|
| 능동 규칙 (카탈로그 행) | _db_trigger 인스턴스 (CT_TRIGGER_NAME) |
| 인메모리 규칙 디스크립터 | TR_TRIGGER (trigger_manager.h) |
| 클래스별 규칙 캐시 | TR_SCHEMA_CACHE — SM_CLASS::triggers 에 매달린 |
| 캐시 안의 이벤트별 리스트 | TR_TRIGLIST *triggers[1] (DB_TRIGGER_EVENT 로 색인) |
| ECA 이벤트 | DB_TRIGGER_EVENT (TR_EVENT_INSERT, TR_EVENT_UPDATE …) |
| ECA 조건 | TR_TRIGGER::condition (TR_ACTIVITY 타입) |
| ECA 액션 | TR_TRIGGER::action (TR_ACTIVITY 타입) |
| 단위 (행 vs 문장) | 이벤트 상수 자체에 인코딩됨 — TR_EVENT_STATEMENT_* |
| 액티비티 시점 (BEFORE/AFTER/DEFERRED) | TR_ACTIVITY 위 DB_TRIGGER_TIME |
| 액션 카테고리 | DB_TRIGGER_ACTION (TR_ACT_EXPRESSION, TR_ACT_REJECT …) |
| 발화별 transient 상태 | TR_STATE — 살아 있는 TR_TRIGLIST 를 들고 있다 |
| 지연 큐 | tr_Deferred_activities — TR_DEFERRED_CONTEXT 체인 |
| 재귀 깊이 카운터 | tr_Current_depth vs tr_Maximum_depth |
| 재귀 스택 (문장 트리거) | tr_Stack[TR_MAX_RECURSION_LEVEL] |
| OLD/NEW 상관 이름 | OBJ_REFERENCE_NAME, NEW_REFERENCE_NAME, OLD_REFERENCE_NAME |
| 액션 파스 트리 컴파일 캐시 | TR_ACTIVITY::parser + TR_ACTIVITY::statement |
| 사용자 트리거 vs 클래스 트리거 분리 | IS_USER_EVENT vs IS_CLASS_EVENT 매크로 |
| 트리거 객체 맵 (MOP → struct) | tr_object_map MHT_TABLE |
| 글로벌 enable/disable | tr_Execution_enabled (loader 가 토글) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”트리거 서브시스템은 trigger_manager.h (401 줄, 공개 API) 와
trigger_manager.c (7547 줄) 안에 산다. .c 의 대부분은
카탈로그 ↔ 인메모리 변환 배관이다. 개념적 핵심 — tr_prepare_class,
tr_before_object, tr_after_object, execute_activity,
eval_condition, eval_action, tr_check_recursivity — 은 수백
줄에 그친다. 이 절은 카탈로그 모델, 인메모리 형태, 발화 파이프라
인, 재귀, 실패 의미, COMMIT/ROLLBACK 순서로 차례로 살핀다.
카탈로그 모델 — _db_trigger
섹션 제목: “카탈로그 모델 — _db_trigger”트리거는 시스템 클래스 _db_trigger 의 한 행이다. 이 클래스는
schema_system_catalog_install.cpp 가 설치한다. 컬럼은
trigger_manager.c 에 선언된 TR_ATT_* 상수와 짝을 이룬다.
// trigger_class_columns — schema_system_catalog_install.cpp (excerpt){TR_ATT_UNIQUE_NAME, "string"}, {TR_ATT_OWNER, AU_USER_CLASS_NAME},{TR_ATT_NAME, "string"}, {TR_ATT_STATUS, "integer", ...TR_STATUS_ACTIVE},{TR_ATT_PRIORITY, "double", ...TR_LOWEST_PRIORITY},{TR_ATT_EVENT, "integer", ...TR_EVENT_NULL},{TR_ATT_CLASS, "object"}, {TR_ATT_ATTRIBUTE, "string"},{TR_ATT_CONDITION_TYPE, "integer"}, {TR_ATT_CONDITION, "string"},{TR_ATT_CONDITION_TIME, "integer"},{TR_ATT_ACTION_TYPE, "integer"}, {TR_ATT_ACTION, "string"},{TR_ATT_ACTION_TIME, "integer"},{TR_ATT_COMMENT, format_varchar (1024)},{TR_ATT_CREATED_TIME, "datetime"}, {TR_ATT_UPDATED_TIME, "datetime"}트리거 행에 닿는 길은 일반 카탈로그 경로다. db_find_class(_db_trigger)
→ 리스트 순회 → obj_get. 행은 트리거를 차갑게 재구성하는 데
필요한 모든 것 — 대상 클래스, attribute, 이벤트, 시점, 그리고
액션 소스를 문자열 로 — 들고 있다. 미리 컴파일된 액션 트리는
첫 발화에서 lazy 하게 다시 만들어진다. 파스 트리에는 MOP 포인터
가 들어 있어서 영속화할 수 없기 때문이다. 사용자 가시 투영은
앞 밑줄이 빠진 view db_trigger 다.
인메모리 형태 — TR_TRIGGER, TR_ACTIVITY, TR_TRIGLIST
섹션 제목: “인메모리 형태 — TR_TRIGGER, TR_ACTIVITY, TR_TRIGLIST”object_to_trigger 가 fetch 된 카탈로그 행으로부터 인메모리
TR_TRIGGER 를 짓는다. 헤더는 plain C 다. 서버 모드는
#error 가드로 금지된다.
// TR_TRIGGER — trigger_manager.htypedef struct tr_trigger { DB_OBJECT *owner; // user MOP DB_OBJECT *object; // back-pointer to catalog instance char *name; double priority; DB_TRIGGER_STATUS status; // ACTIVE / INACTIVE / INVALID DB_TRIGGER_EVENT event; DB_OBJECT *class_mop; // target class char *attribute; // optional column binding struct tr_activity *condition; struct tr_activity *action; char *current_refname; // "obj" by default char *temp_refname; // "new" / "old" by default int chn; // cache-coherency number → revalidate // ... comment, timestamps ...} TR_TRIGGER;
// TR_ACTIVITY — body (condition or action)struct tr_activity { DB_TRIGGER_ACTION type; // EXPRESSION / REJECT / INVALIDATE / PRINT DB_TRIGGER_TIME time; // BEFORE / AFTER / DEFERRED char *source; // raw SQL text void *parser; // lazy PARSER_CONTEXT* void *statement; // compiled PT_NODE* int exec_cnt; // periodic parser-reset counter};TR_SCHEMA_CACHE 는 헤더에, 이벤트마다 한 개씩 TR_TRIGLIST *
가 붙는 가변 길이 꼬리가 달린 모양이다.
// TR_SCHEMA_CACHE — trigger_manager.htypedef struct tr_schema_cache { struct tr_schema_cache *next; // global cache list DB_OBJLIST *objects; // flat MOP list short compiled; // 1 once tr_validate_schema_cache ran unsigned short array_length; // 8 (class) or 2 (attribute) TR_TRIGLIST *triggers[1]; // variable-length tail, indexed by event} TR_SCHEMA_CACHE;TR_CACHE_TYPE 이 클래스 캐시 (8 슬롯, 모든 클래스 단위 이벤트)
와 attribute 캐시 (2 슬롯, 컬럼에 묶인 UPDATE 와 STATEMENT_UPDATE
만) 사이에서 고른다. 각 슬롯은 trigger->priority 내림차순으로
정렬된 TR_TRIGLIST 다.
// insert_trigger_list — trigger_manager.c (condensed)for (t = *list; t && t->trigger->priority > trigger->priority; prev = t, t = t->next) ;new_tr = (TR_TRIGLIST *) db_ws_alloc (sizeof (TR_TRIGLIST));// link new_tr between prev and tmalloc 이 아니라 db_ws_alloc (workspace) 이라는 점이 짚어
둘 만하다. 트리거 리스트는 클래스가 decache 될 때 회수되며, 이로
인해 트리거 캐시 수명이 클래스 수명에 묶인다.
클래스 바인딩 — SM_CLASS::triggers
섹션 제목: “클래스 바인딩 — SM_CLASS::triggers”클래스에서 트리거로 가는 링크는 단일 포인터다. 클래스 단위는
SM_CLASS::triggers, attribute 단위는 SM_ATTRIBUTE::triggers
이며, 둘 다 tr_schema_cache * 다. 카탈로그에서 클래스가 로드
될 때, catcls_* 가 SM_CLASS 그래프를 짓고 tr_make_schema_cache
가 캐시를 할당한다. 트리거 객체는 cache->objects 안에 평탄한
MOP 리스트로 저장된다. 이벤트별 배열은 첫 tr_validate_schema_cache
호출에서 lazy 하게 채워진다.
// tr_validate_schema_cache — trigger_manager.c (condensed)if (cache == NULL || cache->compiled) return NO_ERROR;
for (object_list = cache->objects; object_list != NULL; object_list = next) { next = object_list->next; trigger = tr_map_trigger (object_list->op, 1);
if (trigger != NULL && trigger->event < cache->array_length) { if (insert_trigger_list (&(cache->triggers[trigger->event]), trigger)) return error; } // else: deleted trigger — silently drop from objects list}cache->compiled = 1;이 호출 후 cache->triggers[event] 는 그 이벤트에 대한 우선순위
정렬 리스트가 된다. compiled 플래그가 클래스 로드당 트리거
리스트 순회를 최대 1 회로 보장한다.
발화 경로 — locator 인접, 서버 측이 아니다
섹션 제목: “발화 경로 — locator 인접, 서버 측이 아니다”트리거는 서버가 아니라 클라이언트 측에서 발화한다. 서버의
locator_*_force 는 트리거를 모른다. dirty 객체가 LC_COPYAREA 에
도달하기 전에 발화가 일어난다. 호출 지점은 object_template.c
(INSERT/UPDATE) 와 object_accessor.c (DELETE) 다. 둘 다 같은
3 단계 패턴을 따른다.
flowchart LR DML["obj_template / obj_delete 안의 DML"] --> Q["sm_active_triggers?"] Q -- "no" --> APPLY["heap mutation 만 수행"] Q -- "yes" --> PREP["tr_prepare_class → TR_STATE"] PREP --> BEF["tr_before_object (BEFORE)"] BEF --> APPLY2["heap mutation"] APPLY2 --> AFT["tr_after_object (AFTER)"] AFT --> DEFER["DEFERRED 큐잉 → tr_Deferred_activities"]
빠른 경로의 게이트는 sm_active_triggers 다. 요청된 이벤트
타입의 활성 트리거가 클래스에 없으면 — 가장 흔한 경우 — 0 을
반환하여 어떤 리스트 순회도 일어나지 않게 단락 처리한다.
// obt_apply_assignments — object_template.c (condensed)trigstate = sm_active_triggers (..., class_, TR_EVENT_ALL);event = (template_ptr->object == NULL) ? TR_EVENT_INSERT : TR_EVENT_UPDATE;
if (event != TR_EVENT_NULL) tr_prepare_class (&trstate, class_->triggers, OBT_BASE_CLASSOBJ (template_ptr), event); // attribute-level triggers too, for UPDATE트리거가 있다면 tr_prepare_class 가 TR_STATE 를 만들고
start_state (깊이 검사) 와 merge_trigger_list 를 호출한다.
// tr_prepare_class — trigger_manager.c (condensed)if (!TR_EXECUTION_ENABLED) { *state_p = NULL; return NO_ERROR; }if (cache == NULL) return NO_ERROR;
AU_DISABLE (save); // owner identity will be swapped // in by execute_activityif (tr_validate_schema_cache (cache, class_mop) == NO_ERROR && event < cache->array_length) { triggers = cache->triggers[event]; state = start_state (state_p, triggers ? triggers->trigger->name : NULL); if (state != NULL) merge_trigger_list (&state->triggers, triggers, 0);}AU_ENABLE (save);리스트 구성 동안 권한 검사가 비활성화되는 이유는, 트리거가 다른
사용자의 view 를 거쳐 정의되었을 수 있기 때문이다. 액션은 나중에
execute_activity 안의 AU_SET_USER 로 소유자 의 신원으로
실행된다.
state 가 BEFORE/AFTER 쌍을 구동한다.
// tr_before_object / tr_after_object — trigger_manager.c (condensed)int tr_before_object (TR_STATE *state, DB_OBJECT *current, DB_OBJECT *temp) { if (state) { error = tr_execute_activities (state, TR_TIME_BEFORE, current, temp); if (error) tr_abort (state); }}
int tr_after_object (TR_STATE *state, DB_OBJECT *current, DB_OBJECT *temp) { if (state) { error = tr_execute_activities (state, TR_TIME_AFTER, current, temp); if (error) tr_abort (state); else { // anything still on state->triggers is DEFERRED — flush to global queue if (state->triggers != NULL) { add_deferred_activities (state->triggers, current); state->triggers = NULL; } tr_finish (state); } }}state->triggers 는 발화되면서 소비된다. tr_execute_activities
는 시점이 일치하고 액션이 성공한 항목을 제거한다. AFTER 패스가
끝났을 때 남아 있는 것은 정의상 DEFERRED 집합이며, 글로벌 큐로
옮겨진다. (current, temp) 쌍은 살아 있는 MOP 와 템플릿 단계
shadow 를 함께 운반한다. INSERT BEFORE 는 current=NULL, DELETE
BEFORE 는 temp=NULL, UPDATE 는 둘 다 채워진다.
OLD/NEW 행 바인딩 — 컴파일 시 재작성되는 상관 이름
섹션 제목: “OLD/NEW 행 바인딩 — 컴파일 시 재작성되는 상관 이름”액션 식은 obj.name, new.salary, old.salary 같은 상관 이름
으로 적힌다. CUBRID은 OLD 와 NEW 를 런타임 객체로 직접 노출
하지 않는다. 컴파일 시점에 어떤 이름이 어떤 슬롯에 매핑되는지
파서에게 알려 주고, 파서가 PT_NAME 참조를 그에 맞게 다시 쓴다.
// get_reference_names — trigger_manager.c (default mode, condensed)case TR_EVENT_INSERT: if (time == BEFORE) tempname = "new"; else /* AFTER / DEFERRED */ curname = "obj"; break;case TR_EVENT_UPDATE: if (time == BEFORE) { curname = "obj"; tempname = "new"; } else if (time == AFTER) { curname = "obj"; tempname = "old"; } else /* DEFERRED */ curname = "obj"; break;case TR_EVENT_DELETE: if (time == BEFORE) curname = "obj"; break;대칭이 눈에 띈다. AFTER UPDATE 는 obj → 갱신된 행, old
→ 저장된 before 이미지로 묶고, BEFORE UPDATE 는 obj → 기존 행,
new → 적용 대기 중인 템플릿으로 묶는다. before 이미지는
obt_apply_assignments 가 슬롯을 덮어쓰기 전 에 캡쳐된다.
템플릿 할당의 pr_make_ext_value 가 만든 DB_VALUE 에 담긴다.
// obt_apply_assignments — object_template.c (excerpt)if (trstate != NULL && trstate->triggers != NULL && event == TR_EVENT_UPDATE) { a->old_value = pr_make_ext_value (); error = obj_get_value (object, a->att, mem, NULL, a->old_value); }MYSQL_TRIGGER_CORRELATION_NAMES 는 바인딩 모드를 토글하는
시스템 파라미터다. MySQL 호환 모드는 (AFTER INSERT 에서) 삽입된
행을 obj 가 아닌 new 로, (BEFORE UPDATE 에서) 살아 있는 행을
obj 가 아닌 old 로 노출한다. 그 외 동작은 같다.
트리거 컴파일 — lazy + 액티비티 단위 캐시
섹션 제목: “트리거 컴파일 — lazy + 액티비티 단위 캐시”compile_trigger_activity 가 트리거 본문을 파싱하는 단일 진입점
이다. 세 시점에서 동작한다. tr_create_trigger 시점 (조건과
액션을 두 번 — 문법 오류를 즉시 노출하기 위해), validate_trigger
가 카탈로그 행 변경을 감지한 시점, 그리고 파서가 이전에 해제
되었다면 첫 발화 시점에 lazy 하게.
// compile_trigger_activity — trigger_manager.c (condensed)if (with_evaluate) { text = malloc (length); strcpy (text, EVAL_PREFIX); // "EVALUATE ( " strcat (text, activity->source); strcat (text, EVAL_SUFFIX); // " ) "} else text = activity->source;
activity->parser = parser_create_parser ();get_reference_names (trigger, activity, &curname, &tempname);class_mop = ((curname == NULL && tempname == NULL) ? NULL : trigger->class_mop);
activity->statement = pt_compile_trigger_stmt (activity->parser, text, class_mop, curname, tempname, &activity->source, with_evaluate);조건은 EVALUATE ( ... ) 로 감싸 파서에게 최상위 문장을 보여
준다. 액션은 그대로 파싱된다. 파서는 발화 단위가 아니라 액티비
티 단위다. PT_NODE 는 트리거가 갱신되거나
PRM_ID_RESET_TR_PARSER exec count 에 도달하기 전까지 재사용된다.
tr_check_correlation 은 BEFORE-INSERT 트리거에 한 가지 추가
규칙을 적용한다. 컬럼 없는 맨 new 는 ER_TR_CORRELATION_ERROR
로 거부된다. 아직 할당되지 않은 OID 로 해석되어 버리기 때문이다.
재귀 제어 — 깊이 카운터 + OID 스택
섹션 제목: “재귀 제어 — 깊이 카운터 + OID 스택”CUBRID은 두 층의 재귀 가드를 쌓는다.
깊이 카운터 — tr_Current_depth vs tr_Maximum_depth,
기본값 TR_MAX_RECURSION_LEVEL = 32 — 는 start_state 에서
증가하고 tr_finish 에서 감소한다. 오버플로 시
ER_TR_EXCEEDS_MAX_REC_LEVEL 가 발생한다. 무한 행 단위 재귀를
잡는다.
OID 스택 — tr_Stack[TR_MAX_RECURSION_LEVEL + 1] — 은
STATEMENT 트리거에 대해서만 검사된다. eval_action 이
tr_Stack[tr_Current_depth - 1] 에 트리거의 OID 를 찍어 두고,
발화 전에 tr_check_recursivity 로 스택을 훑는다.
// tr_check_recursivity — trigger_manager.cif (!is_statement) return TR_DECISION_CONTINUE; // row triggers: depth counter handles it
for (i = 0; i < MIN (stack_size, TR_MAX_RECURSION_LEVEL); i++) { if (OID_EQ (&oid, &stack[i])) return TR_DECISION_DO_NOT_CONTINUE; // silently skip — no error}return TR_DECISION_CONTINUE;이층 구조는 의도적이다. 행 단위 트리거는 중첩 DML 안에서 정당 하게 재귀할 수 있다. 32 까지 허용한다. 문장 단위 트리거는 문장 당 한 번 발화되어야 한다. STATEMENT 트리거의 액션이 같은 문장 종류를 다시 발행하면, CUBRID은 에러 대신 조용히 건너 뛰고 안쪽 문장은 정상 동작하게 둔다.
stateDiagram-v2 [*] --> Idle Idle --> Ready: tr_prepare_class → start_state (depth++) Idle --> Idle: depth > MAX → ER_TR_EXCEEDS_MAX_REC_LEVEL Ready --> Firing: tr_before_object / tr_after_object Firing --> Firing: 우선순위 순으로 트리거별 발화; 문장 트리거 OID 스택 검사 Firing --> Deferring: AFTER 패스에 남은 (DEFERRED) 트리거 Firing --> Aborted: 액션 오류 → tr_abort Deferring --> Idle: add_deferred_activities + tr_finish (depth--) Aborted --> Idle: tr_finish (depth--)
실패 의미 — REJECT, INVALIDATE, 에러 전파
섹션 제목: “실패 의미 — REJECT, INVALIDATE, 에러 전파”트리거 액션은 세 가지 범주로 실패한다.
-
REJECT (
TR_ACT_REJECT) —eval_action이*reject = true를 세팅하고,tr_execute_activities가ER_TR_REJECTED를 발생시킨다. DML 은 자기 문장 경계로 롤백되고, AFTER 트리거는 실행되지 않으며, 트랜잭션은 계속된다.check_semantics가 AFTER 와 DEFERRED 시점에서의 REJECT 를 금지한다 (ER_TR_REJECT_AFTER_EVENT) — BEFORE 에서만 의미가 있다. -
INVALIDATE TRANSACTION (
TR_ACT_INVALIDATE) — 글로벌tr_Invalid_transaction플래그를 세우고 트리거 이름을 저장 한다. 현재 문장은 끝까지 가지만,tr_check_commit_triggers가ER_TR_TRANSACTION_INVALIDATED를 던져 COMMIT 을 abort 시킨다. 끈끈하게 남는 상태다. 이후 어떤 문장도 이 플래그를 지우지 못한다. -
평가 오류 —
eval_action의pt_exec_trigger_stmt가 음수 상태를 반환하면,signal_evaluation_error가 그 하위 에러를 트리거 이름과 함께ER_TR_ACTION_EVAL로 감싼다.tr_before_object/tr_after_object는tr_abort(state)를 호출하고 전파한다.signal_evaluation_error안의 재귀 가드er_errid () != error는 재귀 발화를 가로질러 트리거 이름이 반복적으로 쌓이는 것을 막는다.ER_LK_UNILATERALLY_ABORTED와ER_MVCC_SERIALIZABLE_CONFLICT는 abort 신호가 사라지지 않도록 그대로 통과시킨다.
지연 액티비티 — 트랜잭션당 BFS 큐
섹션 제목: “지연 액티비티 — 트랜잭션당 BFS 큐”time == TR_TIME_DEFERRED 인 트리거는 TR_DEFERRED_CONTEXT
구조체의 글로벌 큐 (tr_Deferred_activities) 에 쌓인다. 각
컨텍스트는 savepoint id 를 TR_TRIGLIST 항목 하위 리스트에
묶는다. add_deferred_activities (tr_after_object 가 호출)
가 각 항목에 대상 MOP 를 찍어 두어, 나중 지연 발화 시 어떤
OID 에 속하는지 알 수 있게 한다.
tr_check_commit_triggers 가 COMMIT 시점에 큐를 비운다.
// tr_check_commit_triggers — trigger_manager.c (condensed)if (run_user_triggers (TR_EVENT_COMMIT, time)) return error;
if (time == TR_TIME_BEFORE) { if (tr_execute_deferred_activities (NULL, NULL)) return error;
if (tr_Invalid_transaction) { error = ER_TR_TRANSACTION_INVALIDATED; er_set (..., tr_Invalid_transaction_trigger); } else if (tr_Uncommitted_triggers != NULL) { tr_free_trigger_list (tr_Uncommitted_triggers); tr_Uncommitted_triggers = NULL; }}tr_execute_deferred_activities 의 구현 주석이 통찰을 준다.
BEFORE/AFTER 트리거는 DFS 를 이룬다 (문장 안에서 즉시,
재귀적으로 실행). DEFERRED 트리거는 BFS 를 이룬다 (평탄한
큐로 모이고, COMMIT 한 번에 한꺼번에 비워진다). 큐 비우기 자체
가 중첩 트리거를 발화시킬 수 있지만 (지연 액티비티마다 깊이
카운터가 리셋·재증가) 큐 순서는 보존된다.
이벤트 커버리지와 사용자 vs 클래스 트리거
섹션 제목: “이벤트 커버리지와 사용자 vs 클래스 트리거”이벤트는 IS_USER_EVENT / IS_CLASS_EVENT 매크로로 세 그룹으로
갈린다.
- 클래스 이벤트 (DML):
TR_EVENT_INSERT,TR_EVENT_STATEMENT_INSERT,TR_EVENT_UPDATE,TR_EVENT_STATEMENT_UPDATE,TR_EVENT_DELETE,TR_EVENT_STATEMENT_DELETE. (ALTER/DROP 은 예약되어 있으나 미사용.) - 사용자 이벤트 (트랜잭션 범위):
TR_EVENT_COMMIT,TR_EVENT_ROLLBACK. (ABORT/TIMEOUT 예약, 미사용.) - 센티넬:
TR_EVENT_NULL,TR_EVENT_ALL.
사용자 트리거는 클래스에 붙지 않는다. Au_user 의 triggers
세트에 앉아 tr_check_commit_triggers / tr_check_rollback_triggers
에서 발화된다. tr_User_triggers 안에 산다. tr_update_user_cache
가 다시 짓는다.
결정적 뉘앙스 — 행 vs 문장 구분은 별도의 단위 플래그가 아니라
이벤트 상수 자체에 인코딩되어 있다. 발화 시점의 유일한 분기는
eval_action 의 is_statement 검사이며, 이것이 문장 트리거를
위해 tr_check_recursivity 의 OID 스택 순회를 구동한다.
트리거 lifecycle FSM
섹션 제목: “트리거 lifecycle FSM”stateDiagram-v2 [*] --> Compiled: tr_create_trigger → check_semantics + compile_trigger_activity Compiled --> Active: trigger_to_object + sm_add_trigger; status = TR_STATUS_ACTIVE Active --> Inactive: tr_set_status (INACTIVE) Inactive --> Active: tr_set_status (ACTIVE) Active --> Firing: locator 구동 prepare/before/after Firing --> Active: 성공 또는 오류 Active --> Invalidated: 대상 클래스 drop → status = TR_STATUS_INVALID Active --> Dropped: tr_drop_trigger 또는 tr_delete_triggers_for_class Dropped --> [*] Invalidated --> [*]
INVALID 상태는 대상 클래스가 drop 되었음을 뜻한다. _db_trigger
행은 여전히 존재하지만 발화는 조용히 건너뛴다. 명시적 DROP TRIGGER
가 마지막으로 행을 제거한다.
모듈 초기화와 비활성화
섹션 제목: “모듈 초기화와 비활성화”tr_init 이 글로벌 상태를 초기화하고, 발화를 가로지르며
TR_TRIGGER 할당을 소유하는 MOP→TR_TRIGGER memoisation 해시
tr_object_map 을 만든다. tr_final 은 이 맵을 순회하며 모든
엔트리를 해제한다. TR_SCHEMA_CACHE 인스턴스는 tr_final 이
*해제하지 않는다. 워크스페이스 안에 살며 워크스페이스와 함께
회수된다.
벌크 로더는 load 동안 트리거 발화를 꺼야 한다.
tr_set_execution_state(false) 가 tr_Execution_enabled 를
토글한다. 모든 공개 진입점 (tr_prepare_class, tr_before_object,
tr_after_object, tr_check_commit_triggers 등) 이 이 플래그
를 가장 먼저 검사하고, 꺼져 있으면 즉시 NO_ERROR 를 반환한다.
이 플래그는 트랜잭션 단위가 아니라 프로세스 단위다.
종합 — 구체적 UPDATE 따라가기
섹션 제목: “종합 — 구체적 UPDATE 따라가기”CREATE TRIGGER t1 BEFORE UPDATE ON emp WHEN new.salary < 0 EXECUTE REJECT 와 CREATE TRIGGER t2 AFTER UPDATE ON emp WHEN new.salary > 1000000 EXECUTE INSERT INTO audit VALUES (obj.id, 'big') 가 걸려 있다고 하자. UPDATE emp SET salary = X WHERE id = 7 의 흐름은 다음과 같다.
sequenceDiagram
participant ObjTpl as obt_apply_assignments
participant TR as trigger_manager
participant Workspace
ObjTpl->>TR: sm_active_triggers(emp, TR_EVENT_ALL) → 1
ObjTpl->>TR: tr_prepare_class(&trstate, emp.triggers, emp_mop, TR_EVENT_UPDATE)
TR->>TR: tr_validate_schema_cache + start_state (depth 0→1)
ObjTpl->>ObjTpl: 옛 salary 스냅샷 → a->old_value
ObjTpl->>TR: tr_before_object(trstate, row_7, temp)
TR->>TR: execute_activity(t1, BEFORE) → eval_condition (EVALUATE …)
alt salary < 0
TR-->>ObjTpl: ER_TR_REJECTED
else
ObjTpl->>Workspace: 새 salary 쓰기; ws_dirty(row_7)
ObjTpl->>TR: tr_after_object(trstate, row_7, temp)
TR->>TR: execute_activity(t2, AFTER) → INSERT INTO audit
Note over TR: audit 클래스에 대한 obt_apply_assignments 중첩<br/>depth 1→2 후 다시 1로 복귀
TR->>TR: tr_finish (depth 1→0)
end
짚을 점들. locator 는 트리거 실행 후의 dirty 행만 출하한다.
두 WHEN 절은 create 시점에 컴파일되었고 파스 트리는 재사용을
위해 캐시되어 있다. tr_Current_depth 는 0→1→2→1→0 으로 흘렀다.
AFTER 발화 시 temp 가 저장된 before 이미지를 운반했기에
old.salary 는 갱신 전 값으로, obj.salary 는 갱신 후 값으로
해석되었다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼은 서브시스템별로 묶었다. 라인 번호는 위치 힌트 표에 둔다. 여기 anchor 는 심볼명 이다. 그 이름으로 grep 하면 된다.
- 카탈로그 다리.
_db_trigger(시스템 클래스),CT_TRIGGER_NAME,TR_ATT_*컬럼 이름 상수,trigger_to_object/object_to_trigger,tr_set_trigger_timestamps. - 인메모리 구조.
TR_TRIGGER,TR_ACTIVITY,TR_TRIGLIST,TR_SCHEMA_CACHE,TR_STATE,TR_DEFERRED_CONTEXT,TR_RECURSION_DECISION. 할당/해제 —tr_make_trigger,tr_clear_trigger,free_trigger,make_activity,free_activity. - Lifecycle / DDL.
tr_create_trigger(execute_statement.c의do_create_trigger가 호출),tr_drop_trigger/tr_drop_trigger_internal,tr_rename_trigger,tr_set_status/tr_set_priority/tr_set_comment,check_semantics,check_target,validate_trigger,compile_trigger_activity,tr_check_correlation,tr_map_trigger/tr_unmap_trigger. - 스키마 캐시.
tr_make_schema_cache,tr_copy_schema_cache,tr_merge_schema_cache,tr_validate_schema_cache,tr_active_schema_cache,tr_add_cache_trigger/tr_drop_cache_trigger,tr_delete_schema_cache,tr_delete_triggers_for_class,tr_get_cache_objects,reorder_schema_caches. - 발화 경로.
sm_active_triggers(schema_manager.c안),tr_prepare_class,tr_prepare_statement,start_state,tr_before_object/tr_before,tr_after_object/tr_after,tr_finish,tr_abort,tr_execute_activities,execute_activity,eval_condition,eval_action,signal_evaluation_error,value_as_boolean,get_reference_names. - 재귀.
tr_Current_depth,tr_Maximum_depth,TR_MAX_RECURSION_LEVEL,tr_Stack,tr_check_recursivity,compare_recursion_levels,tr_get_depth/tr_set_depth. - 지연 큐.
tr_Deferred_activitieshead/tail,add_deferred_activity_context,add_deferred_activities,flush_deferred_activities,remove_deferred_activity,remove_deferred_context,tr_execute_deferred_activities,tr_drop_deferred_activities,its_deleted. - 트랜잭션 통합.
tr_check_commit_triggers,tr_check_rollback_triggers,tr_check_abort_triggers,tr_has_user_trigger,tr_Uncommitted_triggers,tr_Invalid_transaction/tr_Invalid_transaction_trigger. - 사용자 트리거.
IS_USER_EVENT/IS_CLASS_EVENT,tr_User_triggers패밀리,register_user_trigger/unregister_user_trigger,tr_update_user_cache/tr_invalidate_user_cache,run_user_triggers,get_user_trigger_objects. - 모듈 제어.
tr_init,tr_final,tr_dump,tr_get_execution_state/tr_set_execution_state,tr_object_map,tr_Schema_caches,tr_get_class_name.
위치 힌트 (2026-05-01 기준, 별도 표기 없으면 src/object/trigger_manager.c)
섹션 제목: “위치 힌트 (2026-05-01 기준, 별도 표기 없으면 src/object/trigger_manager.c)”헤더 trigger_manager.h:
| 심볼 | 라인 |
|---|---|
TR_TRIGGER 구조체 | 71 |
TR_TRIGLIST 구조체 | 102 |
TR_DEFERRED_CONTEXT 구조체 | 111 |
TR_ACTIVITY 구조체 | 132 |
TR_STATE 구조체 | 151 |
TR_SCHEMA_CACHE 구조체 | 158 |
TR_CACHE_TYPE / TR_RECURSION_DECISION enum | 178 / 193 |
TR_MAX_RECURSION_LEVEL 매크로 | 50 |
TR_ATT_* extern 선언 | 205-225 |
compat/dbtype_def.h 의 타입 enum:
| 심볼 | 라인 |
|---|---|
DB_TRIGGER_STATUS | 354 |
DB_TRIGGER_EVENT | 366 |
DB_TRIGGER_TIME | 398 |
DB_TRIGGER_ACTION | 407 |
구현 파일 trigger_manager.c:
| 심볼 | 라인 |
|---|---|
IS_USER_EVENT / IS_CLASS_EVENT | 66-73 |
EVAL_PREFIX / EVAL_SUFFIX | 117-118 |
TR_ATT_* 정의 | 120-140 |
tr_Current_depth / tr_Stack | 142 / 144 |
tr_Invalid_transaction | 146 |
tr_Deferred_activities | 151 |
tr_Schema_caches | 159 |
tr_Execution_enabled | 165 |
tr_object_map | 177 |
make_activity / free_activity | 320 / 348 |
tr_make_trigger / free_trigger | 380 / 466 |
tr_free_trigger_list | 488 |
insert_trigger_list / merge_trigger_list | 511 / 570 |
add_deferred_activities | 772 |
flush_deferred_activities | 817 |
trigger_to_object / object_to_trigger | 996 / 1175 |
get_reference_names | 1465 |
compile_trigger_activity | 1599 |
validate_trigger | 1761 |
tr_map_trigger / tr_unmap_trigger | 1835 / 1885 |
tr_make_schema_cache / tr_copy_schema_cache | 2272 / 2322 |
tr_merge_schema_cache / tr_free_schema_cache | 2396 / 2433 |
tr_add_cache_trigger / tr_drop_cache_trigger | 2491 / 2533 |
tr_validate_schema_cache | 2617 |
reorder_schema_caches / tr_active_schema_cache | 2770 / 2801 |
tr_delete_schema_cache / tr_delete_triggers_for_class | 2855 / 2929 |
check_semantics / tr_check_correlation | 3754 / 3883 |
tr_create_trigger | 3930 |
tr_drop_trigger_internal / tr_drop_trigger | 4414 / 4506 |
value_as_boolean / signal_evaluation_error | 4581 / 4658 |
eval_condition | 4696 |
tr_check_recursivity | 4801 |
eval_action | 4839 |
execute_activity | 5039 |
tr_execute_activities | 5145 |
run_user_triggers | 5201 |
start_state | 5293 |
tr_prepare_statement / tr_prepare_class | 5336 / 5521 |
tr_finish / tr_abort | 5586 / 5605 |
tr_before_object / tr_before | 5630 / 5662 |
tr_after_object / tr_after | 5677 / 5720 |
tr_has_user_trigger | 5731 |
tr_check_commit_triggers | 5784 |
tr_check_rollback_triggers | 5851 |
tr_check_abort_triggers | 5934 |
its_deleted | 5970 |
tr_execute_deferred_activities | 6026 |
tr_drop_deferred_activities | 6146 |
tr_init / tr_final | 7287 / 7330 |
tr_dump | 7364 |
tr_get_execution_state / tr_set_execution_state | 7394 / 7408 |
호출 지점 (다른 파일들):
| 심볼 | 파일 | 라인 |
|---|---|---|
obt_apply_assignments (호출자) | object_template.c | 2399 |
| INSERT/UPDATE BEFORE 호출 지점 | object_template.c | 2474 / 2490 |
| INSERT/UPDATE AFTER 호출 지점 | object_template.c | 2661 / 2667 |
| DELETE BEFORE / AFTER 호출 | object_accessor.c | 2174 / 2242 |
sm_get_trigger_cache | schema_manager.c | 4467 |
sm_active_triggers | schema_manager.c | 4557 |
sm_add_trigger / sm_drop_trigger | schema_manager.c | 4870 / 4902 |
SM_CLASS::triggers | class_object.h | 794 |
SM_ATTRIBUTE::triggers | class_object.h | 469 |
tr_check_commit_triggers 호출 | transaction_cl.c | 279 |
tr_check_rollback_triggers 호출 | transaction_cl.c | 428 |
tr_check_abort_triggers 호출 | transaction_cl.c | 551 |
do_create_trigger | query/execute_statement.c | 6661 |
tr_create_trigger 호출 | query/execute_statement.c | 6749 |
_db_trigger 카탈로그 설치 | schema_system_catalog_install.cpp | 803 |
CT_TRIGGER_NAME 매크로 | schema_system_catalog_constants.h | 49 |
교차 검증 노트
섹션 제목: “교차 검증 노트”-
locator 통합은 클라이언트 측 이다.
cubrid-locator.md의 독자는locator_*_force안에서 트리거 발화를 찾으려다 실패할 것이다. 서버 측locator_sr.c는 heap, lock, B-tree, FK, log, replication 을 다루지만 트리거는 절대 다루지 않는다. 트리거는 dirty MOP 가 LC_COPYAREA 에 들어가기 전 에obt_apply_assignments(object_template.c) 와obj_delete(object_accessor.c) 에서 발화한다.trigger_manager.h의#error Does not belong to server module가드가 이를 명시적 으로 못박는다. -
SM_CLASS::triggers는tr_schema_cache *다. 트리거 MOP 의 직접 리스트가 아니다.cubrid-class-object.md가 이 슬롯을 Trigger cache 로 나열하는데, 본 문서가 그 구조를 풀어 놓은 것이다. 캐시는 두 뷰를 유지한다.objects(평탄한 MOP 리스트, 영속화 형태) 와triggers[event](이벤트별 우선순위 정렬 리스트, 발화 형태). 둘은tr_validate_schema_cache가 동기화한다. -
_db_trigger는 일반 카탈로그 클래스다. 특수 테이블이 아니다.cubrid-catalog-manager.md가 이를 다른 시스템 클래스와 같은 방식으로 다룬다. 트리거 모듈은 평범한obj_get/obj_set으로 접근한다. viewdb_trigger가 사용자 가시 투영이다. -
서버 측 MVCC 에는 트리거가 보이지 않는다. 트리거 액션은 같은 파서/executor/locator 파이프라인을 거치는 평범한 SQL 로 실행되며, 트랜잭션의 스냅샷을 상속한다. BEFORE INSERT 트리거가 대상 클래스를 조회하면 자기 pending 행은 보지 못한다 (아직 insert 되지 않았다). AFTER UPDATE 트리거는 갱신된 행을 본다 (워크스페이스가 이미 할당을 적용했다). 이는 Oracle의 mutating-table 문제를 더 느슨한 의미로 회피한다. CUBRID은 액션이 절반쯤 갱신된 워크스페이스 상태를 읽도록 허용한다.
-
문장 단위 재귀는 조용히 건너뛴다. Postgres 와 Oracle 이 에러를 던지거나 허용하는 자리에서, CUBRID은
TR_DECISION_DO_NOT_CONTINUE를 반환하고 안쪽 문장이 재귀 트리거 발화 없이 진행되도록 둔다. 메커니즘은 OID 스택 (tr_Stack) 이다. 깊이 카운터는 행 단위 재귀를 32 로 묶는 별도의 가드다.
미해결 질문
섹션 제목: “미해결 질문”-
STATEMENT 재귀에서 silent-skip 의 합리. 구현 주석은 “we should not go further with the action, but we should allow the call to succeed.” 라고 한다. 왜 에러가 아닌가? SQL-99 가 강제하지는 않는다. Oracle 은 에러를 띄우고, Postgres 는 허용한다. 이 선택은 chained DML 을 단순화하지만 일부 사용자 오류를 가린다.
-
MYSQL_TRIGGER_CORRELATION_NAMES마이그레이션. 이 파라미 터가 두 상관 이름 모드 (obj/new/old와new/old만) 사이를 토글한다. 릴리스마다 어느 쪽이 기본인지는 소스에서 보이지 않는다. 한 설정에서 만든 트리거가 다른 설정에서는 파싱 되지 않을 수 있다. -
PRM_ID_RESET_TR_PARSER의 오버헤드.eval_condition과eval_action이exec_cnt가 임계를 넘으면 파서를 리셋한다. 주석은 의도적인 refresh 가 아니라 임시 우회 (“until we figure out how to reuse the same parser”) 임을 시사한다. 높은 발화율 에서의 성능 영향은 분명하지 않다. -
HA 복제에서의 트리거 DML 의미. 트리거 액션이 서버에서 평범한 로그 레코드를 만들기에, 트리거로 유발된 각 DML 은 독립적인 레코드로 복제된다.
tr_execute_activities안의CDC_TRIGGER_INVOLVED_BACKUP/_RESTORE가 트리거 유발 변경 을 CDC 용으로 표시하지만, 주(primary)의 트리거가 복제본에서 다시 발화되는지는 여기서는 보이지 않는다.cubrid-cdc.md와cubrid-ha-replication.md참조. -
Commit 트리거 / 지연 액티비티 순서. 소스 주석 — “Do we run the deferred activities before the commit triggers? If not, the commit trigger can schedule deferred activities as well.” 현 순서는 commit 트리거가 먼저 돌고, 그 다음 지연 큐가 비워진 다. 따라서 지연 액션은 commit 트리거의 효과를 볼 수 있지만 반대 방향은 아니다. 의도된 설계인지는 기록에 없다.
코드 인용은 2026-05-01 의 CUBRID 소스 트리에서 가져왔다.
프론트매터 references: 는 참고한 파일을, 위치 힌트 표는 특정
심볼을 못박는다.
이론:
- Silberschatz/Korth/Sudarshan, Database System Concepts, 7판, 5장 §Triggers.
- Widom and Ceri, Active Database Systems, Morgan Kaufmann, 1996 — ECA 정식화의 정전.
- SQL:1999 (ISO/IEC 9075-2:1999), §CREATE TRIGGER.
- Postgres 문서 Triggers 챕터; Oracle PL/SQL Language Reference 의 Trigger 챕터.
이 지식 베이스 안의 형제 문서:
cubrid-class-object.md—SM_CLASS::triggers.cubrid-catalog-manager.md— 일반 시스템 클래스로서의_db_trigger.cubrid-locator.md— 서버 측 locator 가 트리거 발화에서 명시적으로 역할이 없음.cubrid-parser.md—pt_compile_trigger_stmt.cubrid-cdc.md—CDC_TRIGGER_INVOLVED_*와 change-data-capture.