Skip to content

[CBRD-26877] DEFAULT EXPR (4/6) - VOLATILE evaluation - #7896

Open
childyouth wants to merge 15 commits into
CUBRID:feature/default_exprfrom
childyouth:CBRD-26877-defexpr
Open

[CBRD-26877] DEFAULT EXPR (4/6) - VOLATILE evaluation#7896
childyouth wants to merge 15 commits into
CUBRID:feature/default_exprfrom
childyouth:CBRD-26877-defexpr

Conversation

@childyouth

@childyouth childyouth commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

http://jira.cubrid.org/browse/CBRD-26877

Purpose


effective volatility가 VOLATILE인 잔여식 DEFAULT(예: DEFAULT (CONCAT(SYS_GUID(), '')), UUID(7)을 품은 식)를 서버 경로와 클라이언트(LOCAL) 경로 양쪽에서 행당 1회 평가로 동작시킨다.

  • STABLE(문장당 1회) vs VOLATILE(행당 1회) 분기는 레거시 enum이 아니라 volatility 분류를 기준으로 한다.
  • 임의의 잔여식은 레거시 whitelist와 달리 NULL로 평가될 수 있으므로, NOT NULL 컬럼에 대한 런타임 가드를 함께 넣는다.
  • 리뷰 반영: 클라이언트의 Compact DEFAULT Tree(CDT) 재수화를 파서당 속성 1회로 통합하고(CDT registry), LOCAL 경로는 이 구문이 실제로 소비하는 CDT만 평가한다(CDT_EVAL_SET).

용어
CDT : Compact DEFAULT Tree — 잔여식 DEFAULT의 저장형(EDL은 값으로 동결, 레거시 enum은 스트림 없음). 코드에서 CDT는 그 재수화 트리도 가리킨다.
Default Reference : 값 자리의 DEFAULT 키워드 / DEFAULT(col) (PT_DEFAULTF)

동작 변화

항목 변경 후
DDL CREATE TABLE t (c VARCHAR(64) DEFAULT (CONCAT(SYS_GUID(), ''))) 허용 (이전: STABLE 이하만)
INSERT (서버 경로, 다중 행) 행마다 서로 다른 값
INSERT (트리거 있는 경로 = LOCAL) 행마다 서로 다른 값 (서버 경로와 동일 결과)
LOCAL 경로, 컬럼을 명시한 INSERT 명시 컬럼의 잔여식 DEFAULT는 평가하지 않음. 이전: 사용되지 않을 VOLATILE DEFAULT의 런타임 오류(예: SMALLINT 오버플로)로 INSERT 실패, 서버 경로와 불일치
ALTER ADD (기존 행 있음) VOLATILE이면 eager 재기록(UPDATE ALL [t] SET [c] = DEFAULT)으로 행마다 distinct. STABLE 이하는 기존대로 instant(original_value 단일 스냅샷)
NOT NULL 컬럼의 DEFAULT가 NULL로 평가된 행 INSERT 거부 (SERVER/LOCAL 양 경로). 이전: 서버는 NULL 저장, 클라이언트는 "값 누락"으로 오인 보고
Default Reference 의미 불변 — VALUES (DEFAULT), SELECT DEFAULT(col), INSERT ... SELECT DEFAULT(col), prepared 반복 실행 모두 VOLATILE 행별·STABLE 문장당. 재수화만 참조 수 → 속성 수로 감소
RANDOM / DRANDOM VOLATILE 분류 → DEFAULT 허용, 행당 평가
UUID_FORMAT / NULLIF / LEAST / GREATEST IMMUTABLE 분류 → 상수 DEFAULT는 리터럴로 폴딩

Implementation


1. Volatility 분류 추가 (pt_get_expression_definition)

Volatility Operators 비고
VOLATILE UUID(전 overload), SYS_GUID, RANDOM, DRANDOM 평가할 때마다 값이 달라짐
IMMUTABLE UUID_FORMAT, NULLIF, LEAST, GREATEST 피연산자 선택/포맷팅만 하므로 상수 DEFAULT는 폴딩
UNSET 유지 RAND, DRAND 무인자(문장당)와 시드형(행별)의 volatility가 overload마다 다름 → CBRD-26879
  • 다른 volatility와 case label을 공유하던 연산자는 독립 블록으로 분리했다. pt_get_op_volatilityoverloads[0]만 읽는 현 구조에서 whole-op로 정확한 것만 분류한다.
  • 식 전체의 effective volatility는 slice 1의 MAX 전파(pt_volatility_max)로 판정하며, pt_check_data_defaultIMMUTABLE/잔여식(STABLE|VOLATILE)을 허용한다.

2. Volatility 전달 — 분류 테이블이 단일 진실원, 소비는 경로별로

경로 전달 방식
SERVER 클라이언트가 REGU 직렬화 시 effective volatility를 regu flags 2비트에 stamping. 서버는 stamped 값만 읽는다
LOCAL CDT registry가 재수화한 트리에 같은 분류 테이블로 1회 재계산(pt_get_expr_tree_volatility)
REGU_VARIABLE_DEFAULT_VOLATILITY_SHIFT = 13
REGU_VARIABLE_DEFAULT_VOLATILITY_MASK  = 0x3 << 13   /* 0x6000 */
  • 이미 직렬화되는 regu->flags에 얹으므로 스트림 포맷 변경이 없다. 이 2비트는 카탈로그에 영속되므로 런타임 플래그로 재사용하면 안 된다(주석으로 명시).
  • stamping 지점: pt_to_default_expr_stream(xasl_generation.c). pt_check_data_default가 검증한 트리이므로 UNSET은 assert로 고정한다.

3. SERVER 평가 — fill 루프 + per-row 패스 (query_executor.c)

함수 역할
qexec_prepare_default_expr_stream 저장된 REGU 스트림을 문장당 1회 역직렬화해 FUNC_PRED와 stamped volatility를 얻음
qexec_eval_default_expr_func_pred 역직렬화된 func_pred를 평가(fetch_copy_dbval + tp_value_cast), NOT NULL 가드
qexec_free_default_expr_caches 해제. qexec_clear_func_predfunc_regu의 결과·피연산자 DB_VALUE를 클리어 (없으면 CS 경로 resource_tracker 누수로 서버 abort)
  • fill 루프STABLE 잔여식만 평가하고 VOLATILE은 volatility만 캡처한다. per-row 패스(qexec_evaluate_row_default_exprs)가 유일한 평가자다.
  • 재사용 func_pred가 행마다 다시 계산되는 근거는 VOLATILE leaf의 NOT_CONST 전파이며 디버그 빌드가 "클라 VOLATILE ⟺ 서버 NOT_CONST" 를 assert한다.
  • UUIDv7 상태 공유: per-row 패스가 thread_p->uuidv7_last_ms/uuidv7_seq를 직접 가리킨다. 레거시 row-determined 경로와 fetch.c 경로가 하나의 단조 증가 소스를 공유해야 한다.

4. LOCAL 평가 — CDT registry(파서 범위) + CDT_EVAL_SET(문장 범위) (parser_support.c, name_resolution.c, execute_statement.c)

클라이언트에서 CDT 스트림을 읽는 곳은 셋이다: Default Reference 해석, INSERT의 SI(문장 시계) 판정 워커, LOCAL 행 루프.

객체 범위 역할
CDT registry 파서(컴파일실행, prepared면 preparedeallocate) attribute 당 1회 CDT->PT 변환, PT 공유. 트리는 파서 노드 → 파서와 함께 소멸, 해제 코드 없음
CDT_EVAL_SET INSERT 실행 1회(행 루프 소유자) 이 구문이 소비하는 잔여식만 모아 STABLE은 빌드 직후 1회, VOLATILE은 행마다 평가 → att->default_value.value 갱신, populate_defaults가 소비
  • 세 소비자는 registry 트리를 다르게 쓴다: SI 워커는 walk만, Default Reference는 복사(parser_copy_tree; pt_fold_const_expr의 DEFAULTF 치환과 노드 해제가 소유권을 가정), CDT_EVAL_SET은 포인터 참조(같은 트리 재평가는 pt_evaluate_tree에 노드 메모이제이션이 없어 안전).
  • 명시 할당 컬럼 제외 (is_template_assigned_attr): 오브젝트 템플릿이 이미 실제 값을 할당한 컬럼(컬럼 목록 명시, SELECT 공급, Default Reference)은 populate_defaults가 DEFAULT를 읽지 않으므로 eval set에서 뺀다. 한 구문은 모든 행에 같은 컬럼 집합을 할당하므로 첫 행 템플릿의 필터가 구문 전체에 유효하다. vclass 템플릿은 assignments가 base 클래스 순서로 색인되어 판정을 생략한다.
  • do_evaluate_default_expr_by_smclass의 평가 모드 분기(DEFAULT_EXPR_EVAL_BY_STATEMENT_ONLY vs BY_ROW_ONLY)는 유지. 속성 목록 순회는 레거시 pseudo-column enum 경로만 담당한다.
  • UUID(7)은 시간 정렬이라 LOCAL 평가가 동기화된 문장 시계를 읽어야 한다. SI 워커(pt_residual_needs_si_datetime_walk)에 PT_UUID 케이스를 추가했다(버전 인자 7 또는 미확정 → si_datetime).

5. ALTER ADD — VOLATILE만 eager 재기록 (execute_schema.c)

  • 게이트는 effective volatility다: PT_IS_VOLATILE_RESIDUAL_DEFAULTUPDATE ALL [t] SET [c] = DEFAULT, STABLE 이하 → 기존대로 instant. slice 3의 Default Reference + do_not_fold 경로를 타므로 각 행이 서로 다른 값을 갖는다.
  • get_att_default_from_def가 잔여식도 DDL 시점 1회 평가한 뒤 컬럼 타입으로 coerce한다. 비호환 결과 타입은 DDL에서 거부된다.
  • 매크로 정리: PT_IS_STABLE_RESIDUAL_DEFAULTPT_IS_RESIDUAL_DEFAULT(STABLE|VOLATILE) + PT_IS_VOLATILE_RESIDUAL_DEFAULT.

6. NOT NULL 런타임 가드

경로 강제 지점 에러
SERVER qexec_eval_default_expr_func_pred — 도메인 캐스트 직후 attr->is_notnull && DB_IS_NULL ER_NULL_CONSTRAINT_VIOLATION
LOCAL populate_defaults (object_template.c) — 소비 지점 ER_OBJ_ATTRIBUTE_CANT_BE_NULL
  • SERVER: 생략 컬럼의 DEFAULT는 cons_pred(명시 컬럼 한정)가 커버하지 못하므로 평가 지점에서 강제한다. fill 루프와 per-row 패스가 같은 헬퍼를 지나 가드 1곳으로 양쪽을 덮는다.
  • LOCAL: obt_check_missing_assignments는 "DEFAULT가 있는가"만 보므로 실제로 값이 쓰이는 populate_defaults에서 강제한다.

7. 부수 수정

  • continued_case 직렬화: CONCAT/CONCAT_WS/FIELDCASE/DECODE/COALESCE/LEAST/GREATEST 체인의 unparse 힌트가 CDT에 실리지 않아 재수화된 잔여식이 다른 형태로 출력되던 버그. OP/FUNC 노드 헤더에 continued_case를 추가했다.

Remarks


  • RAND/DRAND는 overload별 volatility가 달라 whole-op로 분류할 수 없다. matched-overload stamping이 들어가는 CBRD-26879에서 분류하며, 그때까지 DEFAULT에서 거부된다.
  • SI 판정 워커는 CBRD-26879의 영속 default_expr_si_requirement로 대체·삭제된다. 그때까지 워커가 명시 컬럼의 CDT도 registry에 올릴 수 있다. volatility도 같은 방식으로 영속화되면 registry의 pt_get_expr_tree_volatility 순회가 사라진다.
  • INSERT INTO <view> SELECT ...pt_resolve_vclass_args의 서브쿼리 분기가 pt_sm_attribute_default_value_to_node(저장값)를 써 생략 컬럼의 잔여식 DEFAULT가 DDL 시점 값으로 동결된다. CBRD-26878의 CREATE VIEW 동결 회귀와 함께 처리한다.

…d REGU form

pt_to_default_expr_stream records the STABLE/VOLATILE classification in two spare
regu flag bits so the server can pick the evaluation cadence without a parser.
The PT_VOLATILITY values become explicit now that they are persisted.
The stored REGU form is deserialized once per INSERT statement and the cached
func_pred is re-fetched per row for a VOLATILE residual, while a STABLE one keeps
its single per-statement value. The UUIDv7 sequence state is shared with fetch.c
so a residual embedding UUID(7) stays monotonic across rows.
do_evaluate_default_expr_by_smclass picks the evaluation cadence from the
rehydrated tree's volatility and reuses a per-statement cache of rehydrated trees
instead of rehydrating per row. UUID(7) requests the statement clock sync it needs
on the local path.
… ADD

UUID() and SYS_GUID() are classified VOLATILE (UUID_FORMAT IMMUTABLE),
pt_check_data_default admits a VOLATILE residual alongside STABLE ones, and DDL
compiles its stored forms. Adding a VOLATILE DEFAULT column to a populated table
rewrites the rows through SET col = DEFAULT so each row gets a distinct value.
A rehydrated CONCAT/CASE-family residual lost its unparse hint and printed in a
different form than the original expression; the stream now carries
continued_case.
…TABLE

RANDOM and DRANDOM draw a new value on every evaluation, so a DEFAULT embedding
them is evaluated once per row. NULLIF, LEAST and GREATEST only select among
their operands, so a constant DEFAULT built from them folds to a literal. RAND
and DRAND stay unclassified until their no-argument and seeded overloads can be
classified apart.
The server's NOT NULL predicate is built from the explicitly listed columns
only, so a residual DEFAULT yielding NULL for an omitted column was stored as
NULL. The client path reported such a row as a missing value, which hides that
the column does have a DEFAULT.
The per-statement pass no longer builds a throwaway cache of its own: the row-loop
owner's cache evaluates STABLE residuals once when it is built and VOLATILE ones per
row, walking the cache itself instead of the attribute list. The attribute walk now
serves only the legacy pseudo-column enum path.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

🧪 TC Test Environment Ready

CircleCI Testing:

  • CircleCI will automatically test using the branches below.

TC Repositories & Branches:

Next Steps:

  1. Wait for CircleCI tests to complete
  2. If CircleCI tests failed, please check the test results and fix the issues.
  3. When ready to merge this PR, please merge the TC PR first, then merge this PR.

@childyouth

Copy link
Copy Markdown
Contributor Author

/run all

@childyouth
childyouth marked this pull request as ready for review September 9, 2026 04:06
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@greptile-apps

greptile-apps Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Retrigger

이전 리뷰 이후 수정 사항에서 새로 발생한 merge 차단 결함은 확인되지 않아 현재 상태는 병합해도 안전해 보입니다.

Reviews (3) · Last reviewed commit: "Keep a parser-owned copy of the CDT stre..."

Comment thread src/parser/parser_support.c
Comment thread src/query/execute_statement.c Outdated
Comment thread src/base/pt_volatility.h Outdated
Comment thread src/query/execute_statement.c Outdated
… Tree

RESIDUAL_DEFAULT_CACHE was neither a cache in the eviction sense nor a list of
every residual: it is the set of Compact DEFAULT Trees one INSERT evaluates on
the Local Evaluation path, with their volatility. A DEFAULT has a CDT exactly
when it is residual (an Expression-Derived Literal is frozen to a value, a
legacy pseudo-column DEFAULT has no stream), so CDT_EVAL_SET names both what
the set holds and why literals and legacy defaults are absent. No behavior
change.
The Local Evaluation row loop evaluated every VOLATILE residual of the class,
including columns the INSERT assigns explicitly, whose DEFAULT populate_defaults
never reads. A runtime error in such an unused DEFAULT failed the whole INSERT
on the trigger path while the server path, which evaluates only the omitted
columns, succeeded. The set is now built from the object template: a column
that already holds a real assignment is skipped, so neither its STABLE nor its
VOLATILE tree is rehydrated or evaluated. One statement assigns the same
columns on every row, so the first row's template decides for the statement; a
virtual-class template indexes assignments by the base class and keeps the old
behavior.
Three client-side readers decoded the same stream on their own: every Default
Reference (a 200-row VALUES with one DEFAULT keyword per row decoded it 200
times), the statement-clock probe of an INSERT, and the Local Evaluation set.
A parser-wide CDT registry now decodes each attribute's stream once, keyed on
(class, attribute id) and validated against the stream bytes so a class
re-fetched after an ALTER gets a fresh tree. Entries and trees are parser
memory and go away with the parser. Default References copy the shared tree,
the probe only walks it, and the eval set evaluates it in place.
@childyouth

Copy link
Copy Markdown
Contributor Author

@greptileai 리뷰해줘

Comment thread src/parser/parser_support.c Outdated
The Compact DEFAULT Tree reader doubles as a probe and never sets an error, so
its callers decided what to report: the Local Evaluation set said "unknown
error" and a Default Reference raised an internal error. Both now report
ER_SM_INVALID_DEFAULT_EXPR_STREAM naming the attribute whose stored expression
this build cannot restore (a version mismatch or a corrupted stream).
A registry entry validated its tree against the attribute's own stream buffer,
which belongs to the workspace: once the class is released and re-fetched while
the parser lives on (a prepared statement), the pointer dangles and the compare
reads freed memory, or a reused address passes for the old content. The entry
now keeps a parser_alloc'd copy of the bytes and compares only against that;
the copy goes away with the parser like the tree.
@childyouth

Copy link
Copy Markdown
Contributor Author

@greptileai 코드 리뷰해줘

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants