(KO) CUBRID 의미 검사 — 이름 해석, 타입 검사, 상수 접기, 그리고 문 종별 검증
목차
학술적 배경
섹션 제목: “학술적 배경”관계형 데이터베이스의 컴파일러는 프로그래밍 언어 컴파일러 와 거의 같은 골격을 따른다. 어휘 분석(lexical analysis), 구문 분석 (syntactic analysis), 의미 분석(semantic analysis), 그리고 코드 생성 (여기서는 질의 계획 생성) 이다. Database System Concepts (Silberschatz et al., 16장)와 Database Internals(Petrov, 7장)는 모두 이 흐름을 같은 그림으로 그린다. 다만 RDBMS의 의미 분석은 일반 컴파일러보다 훨씬 무거운 짐을 진다는 점이 다르다. 카탈로그를 들여다봐야 하고, 사용자 권한을 검증해야 하며, 묵시적 형변환을 삽입해야 하고, 트리거·제약·뷰 정의를 본문에 합성해 넣어야 하기 때문이다.
이러한 의미 분석 단계가 질의 컴파일러의 형식 계약(formal contract) 을 정의한다. 곧, 어떤 SQL 문장이 의미적으로 받아들여질 조건이 무엇인지, 그리고 그 조건이 어떤 보강(annotation)을 거친 뒤 다음 단계인 옵티마이저(optimizer)에 전달되는지를 결정한다. 그 계약은 다음 네 약속으로 분해된다.
- 이름 해석(name resolution). SQL 텍스트에 등장하는 모든 이름 — 컬럼 이름, 테이블 별칭, 사용자 정의 함수, 호스트 변수 — 을 카탈로그(catalog) 또는 활성 스코프(scope)의 정확한 한 항목에 바인딩(bind)해야 한다는 점이다. 모호함이 남아 있다면 컴파일은 실패한다. 표준 SQL은 스코프 규칙을 명세한다 — 가장 안쪽 FROM 절부터 바깥으로 — 그리고 모호 컬럼은 오류이다.
- 타입 검사(type checking) 와 도메인 일관성(domain consistency). 모든 식(expression)에 단일한 자료형을 부여해야 하며, 그 자료형이 그 식이 쓰이는 문맥에 맞아야 한다는 점이다. 술어(predicate)는 부울로, GROUP BY 식은 비교 가능 도메인으로, 집합 연산의 양변은 동일한 컬럼 형으로 맞아야 한다. 묵시적 형변환(implicit cast)은 이 단계에서 명시적으로 트리에 박혀 들어간다 — 옵티마이저가 보는 표현은 이미 형이 굳어 있다.
- 권한과 제약 검증(authorization & constraint validation). 참조된 객체를 사용자가 어떤 권한을 갖는지, 새 객체가 기존 카탈로그와 충돌하지 않는지, NOT NULL·CHECK·외래 키 같은 제약이 깨질 가능성이 없는지를 확인하는 단계다.
- 묵시 의미 합성(implicit semantics synthesis). 뷰 본문 펼치기 (view expansion), 트리거 본문 첨부, 기본값 채우기, 자동 증가 컬럼의 next-value 구문 삽입 같은 — 사용자가 문장으로 적지 않았지만 표준이 하기로 정한 의미들을 트리에 합성해 넣는다 는 점이다.
이 네 약속은 이론적으로는 직교한다. 그러나 실제 엔진은 거의 항상 이 네 단계를 하나의 트리 변형 파이프라인으로 융합한다. 같은 파스 트리를 여러 번 훑되, 매 패스(pass)마다 한 가지 의미 정보만 보강하는 식으로 진행한다는 점이다. 그래야 패스마다 들고 다닐 보조 자료구조가 작아지고, 오류 메시지가 정확한 위치를 가리킬 수 있기 때문이다.
CUBRID 역시 같은 길을 따른다. 어휘·구문 분석은 csql_grammar.y
(yacc/bison) 와 손으로 쓴 어휘기로 끝내고, 그 뒤를 pt_* 패스가
이어받아 한 패스에 한 약속씩 붙여 나가는 구조이다. 각 패스가
다음 패스의 전제 조건(precondition) 을 만들어 준다는 점에서, 이
파이프라인은 형식 계약을 점진적으로 누적해 가는 형태로 읽는
것이 옳다.
DBMS 공통 설계 패턴 (Common DBMS Design)
섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”이론이 무엇 을 검증해야 하는지를 말한다면, 이 절은 거의 모든 관계형 엔진이 어떤 형태의 자료구조와 패스 분할로 그 검증을 구현하는지를 정리한다. PostgreSQL, Oracle, MySQL, SQL Server, 그리고 CUBRID 모두 이 공유 설계 공간(shared design space)에서 한 점을 선택한 결과로 봐야 한다.
파스 트리는 1급 자료구조 (Parse tree as a first-class IR)
섹션 제목: “파스 트리는 1급 자료구조 (Parse tree as a first-class IR)”의미 분석의 대상 은 SQL 텍스트가 아니라 파스 트리(parse tree) 다. 정확히 말하면 추상 구문 트리(AST) 와 해소된 참조(resolved references) 가 한 자료구조에 융합된 형태이며, 업계에서는 이를 흔히 질의 IR(Query IR) 또는 논리 트리(logical tree) 라고 부른다. 텍스트가 아니라 트리에 손대는 이유는 다음과 같다.
- 위치 정보(position info)가 보존된다. 오류 메시지는 항상 토큰의 라인/컬럼을 지적할 수 있어야 한다.
- 여러 패스가 같은 자료구조를 공유한다. 패스마다 트리를 다시 파싱하지 않는다. 보강 정보는 노드 슬롯(slot) 에 누적된다.
- 변형(rewrite)이 자연스럽다. 뷰 펼치기, 부분식 끌어올리기 (subquery pull-up), 묵시 형변환 삽입 같은 변형은 서브트리 교체 로 표현하기 가장 단순하다.
- 카탈로그 의존성을 한 곳에 격리한다. 카탈로그 조회는 패스의 하나 — 보통 이름 해석 패스 — 안에서만 일어나고, 나머지 패스는 트리에 박힌 OID/포인터를 신뢰한다.
PostgreSQL의 Query 노드, Oracle의 parse tree, SQL Server의
algebrizer tree, MySQL의 LEX/SELECT_LEX, CUBRID의 PT_NODE 가
모두 같은 역할을 맡는다는 점이다.
노드 종별과 공통 헤더 (Node kinds and shared header)
섹션 제목: “노드 종별과 공통 헤더 (Node kinds and shared header)”거의 모든 엔진의 파스 트리 노드는 공통 헤더 + 종별 페이로드 (union) 의 모양을 띤다. 공통 헤더에는 다음이 들어간다.
- 노드 종별(kind) —
SELECT,INSERT,EXPRESSION,NAME,VALUE, … 라는 enum. - 소스 위치(line, column) — 오류 메시지를 위해.
- 자료형 정보(data_type, expected_domain, type_enum) — 타입 검사 패스에서 채운다.
- 다음 형제 포인터(next) — 같은 절(clause) 안의 항목들을 단일 연결 리스트로 잇는다. 트리는 사실상 형제-아들 트리 (sibling-child tree) 로 구현된다.
- 플래그 비트(flag bits) — 이 식은 이미 형변환이 삽입됨, 이 부분식은 상수로 접힘, 이 노드는 내부적으로 합성됨 같은 패스간 전달용 메타데이터.
페이로드는 종별마다 다르며 union으로 좁힌다. SELECT 노드는
from, where, group_by, having, order_by, into_list,
hint 따위를 자식으로 가지며, EXPRESSION 노드는 arg1, arg2,
arg3 자식과 연산자 코드(opcode)를 가진다. 이 모양은 PostgreSQL
의 Node + 태그 union, MySQL의 Item 클래스 계층, CUBRID의
PT_NODE 와 그 안의 info union이 모두 공유한다.
패스 분할 (Pass partitioning)
섹션 제목: “패스 분할 (Pass partitioning)”의미 분석은 하나의 거대한 함수 가 아니라 여러 트리 워커 (walker) 패스의 합성으로 구현된다. 각 패스는 비슷한 구조를 가진다. (a) 트리를 깊이 우선으로 내려가며 pre-함수 를 호출하고, (b) 자식이 끝나면 post-함수 를 호출하며, (c) 패스 별로 좁혀진 컨텍스트(context)를 들고 다닌다는 점이다. 다음이 거의 표준에 가까운 분할이다.
| 패스 | 입력 전제 | 출력 보강 |
|---|---|---|
| 매크로/식별자 정규화 | 원시 토큰 트리 | 식별자 대소문자 정규화, 스칼라 ↔ 함수 구분 |
| 이름 해석 | 정규화된 트리 | NAME 노드에 카탈로그 OID/스코프 ID 박힘 |
| 타입 추론·형변환 삽입 | 이름이 해소된 트리 | 모든 식에 도메인 부착, CAST 노드 합성 |
| 의미 검사 / 무결성 검사 | 형이 굳은 트리 | 권한·제약·집계 규칙 위반 검출 |
| 뷰·트리거 펼치기 | 검증된 트리 | 뷰 본문, 트리거 본문이 트리에 합성됨 |
| 질의 재작성 (rewrite) | 펼쳐진 트리 | 부분식 평탄화, 상수 접기, 외부 조인 정리 |
엔진마다 패스의 수 와 경계 는 조금씩 다르다. 어떤 엔진은 타입
추론과 의미 검사를 한 패스로 합치고, 어떤 엔진은 뷰 펼치기를
이름 해석 안 에서 처리한다. CUBRID는 패스를 비교적 잘게 쪼개는
편이다 — pt_semantic_check가 다시 여러 sub-walker로 분기한다.
카탈로그 접근의 격리 (Catalog access isolation)
섹션 제목: “카탈로그 접근의 격리 (Catalog access isolation)”이름 해석 패스는 카탈로그(catalog) — 곧 시스템 테이블 또는 그 인메모리 캐시 — 에 수많은 조회를 발생시킨다. 한 SELECT 한 번에도 수십 번의 클래스/속성 조회가 일어날 수 있다. 그래서 모든 실용 엔진은 다음 두 가지 기법을 결합한다.
- 세션 스코프 카탈로그 캐시(per-session catalog cache). 같은 컴파일 안에서 같은 객체를 두 번 읽지 않는다.
- OID 기반 핸들(handle-by-OID). 트리에는 이름 문자열 이 아닌 OID 또는 디스크립터 포인터 가 박힌다. 다음 패스는 이름을 다시 해석하지 않는다.
CUBRID는 이를 db_object 핸들과 mop (Memory Object Pointer) 로
구현한다. 한 번 해석된 클래스는 트리의 NAME 노드에 db_object*
형태로 박혀 다음 패스에 그대로 전달된다.
오류 보고의 위치성 (Localized error reporting)
섹션 제목: “오류 보고의 위치성 (Localized error reporting)”의미 분석에서 가장 사용자 체감이 큰 요소는 오류 메시지의 정확한 위치 보고 다. 이를 위해 모든 엔진은 다음을 지킨다.
- 모든 노드가 (line, column) 을 보존한다.
- 패스가 오류를 만나도 그 자리에서 멈추지 않는다 — 가능한 한 뒤쪽까지 검사하여 한 컴파일에서 여러 오류를 수집한다 (배치 보고).
- 합성된(synthesized) 노드는 원본 노드의 위치를 상속한다 — 그래야 뷰를 거친 오류도 사용자 SQL의 적절한 토큰을 가리킬 수 있다.
트리거·제약·기본값의 합성 (Synthesized semantics)
섹션 제목: “트리거·제약·기본값의 합성 (Synthesized semantics)”표준 SQL은 사용자가 적지 않아도 엔진이 대신 적어 주는 의미를 다수 정의한다. 트리거 본문(BEFORE/AFTER), 제약식 평가, 기본값 대입, 시퀀스의 NEXT VALUE, ON UPDATE CURRENT_TIMESTAMP 같은 것들이다. 이들은 의미 분석의 끝자락에서 트리에 명시적으로 합성 된다는 점이다. 곧, 옵티마이저는 트리거를 알지 못한다 — 트리에 이미 트리거 본문이 INSERT 문의 자식 노드처럼 박혀 있을 뿐이다. 이 설계가 옵티마이저를 단순하게 유지하는 핵심 비결이다.
이론 ↔ CUBRID 명칭 매핑 (Theory ↔ CUBRID mapping)
섹션 제목: “이론 ↔ CUBRID 명칭 매핑 (Theory ↔ CUBRID mapping)”§학술적 배경의 텍스트북 개념과 CUBRID의 명명된 엔티티가 다음
표처럼 대응된다. ## CUBRID의 구현은 이 표의 각 행을 천천히
줌인하는 구성이다.
| 이론 (Theory) | CUBRID 명칭 |
|---|---|
| 파스 트리 노드 (parse tree node) | PT_NODE — 공통 헤더 + info union |
| 노드 종별 (node kind) | enum pt_node_type (PT_SELECT, PT_NAME, PT_EXPR, …) |
| 트리 워커 (tree walker) | parser_walk_tree (pre/post 함수와 사용자 컨텍스트) |
| 컴파일 컨텍스트 (compilation context) | PARSER_CONTEXT — 오류 리스트·할당자·플래그를 모음 |
| 이름 해석 패스 | pt_resolve_names 와 그 sub-walker들 |
| 카탈로그 핸들 (catalog handle) | DB_OBJECT * (a.k.a. MOP) — NAME 노드의 db_object 슬롯에 박힘 |
| 타입 추론 패스 | pt_semantic_type → pt_eval_expr_type 등 |
| 묵시 형변환 (implicit cast) | PT_EXPR 노드 종별 PT_CAST, 또는 expression info의 cast_type |
| 의미 검사 패스 | pt_semantic_check → 종별별 sub-walker |
| 뷰 본문 펼치기 | mq_translate (view rewriting; 본 문서는 헤드라인만 다룸) |
| 오류 큐 | PARSER_CONTEXT::error_msgs 와 pt_record_error |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”여기부터는 CUBRID 소스가 위 공통 설계 공간에서 어떤 점을 선택했는지
를 따라간다. 핵심 형(type)은 PT_NODE 하나이며, 모든 패스는 이
하나의 트리에 주석을 덧붙이는 형태로 협조한다.
컴파일 파이프라인 한눈에 보기
섹션 제목: “컴파일 파이프라인 한눈에 보기”flowchart LR SQL["SQL 텍스트"] --> LEX["lexer\n(parser_main.l)"] LEX --> YACC["bison\n(csql_grammar.y)"] YACC --> AST["원시 PT_NODE 트리"] AST --> RES["pt_resolve_names\n이름 → DB_OBJECT/스코프"] RES --> TYP["pt_semantic_type\n도메인 추론, CAST 합성"] TYP --> CHK["pt_semantic_check\n무결성·집계·권한"] CHK --> XPN["뷰/트리거 펼치기\n(mq_translate 등)"] XPN --> RWR["pt_rewrite\n부분식 평탄화·상수 접기"] RWR --> XAS["XASL 생성\n(parser_generate_xasl)"] XAS --> EXE["실행기"]
각 박스가 다음 박스의 전제 조건을 만든다는 것이 가장 중요한 관찰이다. 이름 해석 없이는 타입 추론을 할 수 없고, 타입 추론 없이는 의미 검사가 비교를 강제할 수 없으며, 뷰가 펼쳐지지 않으면 재작성이 옵티마이저에게 넘기기 전에 끌어올릴 부분식을 알 수 없기 때문이다.
PT_NODE — 모든 것을 담는 한 자료구조
섹션 제목: “PT_NODE — 모든 것을 담는 한 자료구조”CUBRID의 파스 트리는 단일 노드 형 PT_NODE 위에 세워져 있다.
공통 헤더는 모든 종별이 공유하며, 종별별 페이로드는 info
union 으로 좁혀진다. 트리는 형제 포인터 next 로 같은 절의
항목들을 연결하고, or_next 는 OR 분기 같은 보조 사슬에 쓰인다는
점이다.
// PT_NODE — src/parser/parse_tree.h (condensed)struct parser_node{ PT_NODE_TYPE node_type; /* what kind of node am I? */ short flag; /* misc per-node flags */ short line_number; /* source position */ short column_number; PT_NODE *next; /* next sibling in clause list */ PT_NODE *or_next; /* secondary chain (e.g. OR list) */ PT_NODE *etc; /* free-form back-pointer slot */
TP_DOMAIN *expected_domain; /* domain expected by parent context */ PT_TYPE_ENUM type_enum; /* SQL type after type-check pass */ PT_NODE *data_type; /* refined type info (precision, etc.) */
union pt_info info; /* per-kind payload (PT_SELECT_INFO, ...) */};이 한 형이 SELECT 절, 표현식, 테이블 참조, 호스트 변수, INSERT
문 모두 를 표현한다. 종별 분기는 패스 함수가 node_type 을
스위치하는 자리에서만 일어난다.
flowchart TB ROOT["PT_NODE\n node_type = PT_SELECT"] ROOT --> FROM["info.query.q.select.from\n PT_NODE list (PT_SPEC ...)"] ROOT --> WHERE["info.query.q.select.where\n PT_NODE expression"] ROOT --> GROUP["info.query.q.select.group_by"] ROOT --> HAVING["info.query.q.select.having"] ROOT --> ORDER["info.query.order_by"] ROOT --> LIST["info.query.q.select.list\n SELECT 출력 컬럼들"] LIST --> N1["PT_NODE\n node_type = PT_NAME\n info.name.original = 'a'"] N1 --> N2["PT_NODE\n node_type = PT_EXPR\n info.expr.op = PT_PLUS"] N2 --> N3["PT_NODE\n node_type = PT_VALUE\n info.value.data_value = 1"]
형제 포인터(next)가 list 모양을 트리 안에 끼워 넣는다는 점에
유의한다. SELECT 의 출력 컬럼 목록, FROM 의 테이블 목록, WHERE 식
의 OR 분기 모두 이 한 가지 패턴을 재사용한다.
PARSER_CONTEXT — 컴파일의 가방
섹션 제목: “PARSER_CONTEXT — 컴파일의 가방”각 컴파일은 PARSER_CONTEXT 하나를 들고 다닌다. 트리 자체는
이 컨텍스트가 소유한 풀(pool)에서 할당되고, 오류 큐도, 호스트
변수 테이블도, 컴파일 단위의 플래그도 모두 여기에 산다.
// PARSER_CONTEXT — src/parser/parser.h (condensed)struct parser_context{ PARSER_VARCHAR *original_buffer; /* the original SQL text */ PARSER_VARCHAR *error_buffer; /* accumulated error msg */
PT_NODE **node_table; /* growable table of all nodes */ int max_node_id; int next_node_id;
PT_HOST_VARS *host_var_table; /* :host variables */ DB_VALUE *host_variables; /* their bound values */ int host_var_count;
int statement_number; int line_offset; unsigned int custom_print; /* print-mode flags */ int flag; /* misc flags (DONT_PRT_LONG_STRING, ...) */};패스 함수의 시그니처는 거의 항상 (PARSER_CONTEXT *parser, PT_NODE *node, void *arg, int *continue_walk) 의 모양을 하며, 트리 워커는
이 컨텍스트로 오류를 누적하거나 새 노드를 할당한다. 모든
노드가 같은 풀에서 나오므로, 컴파일이 끝나면 컨텍스트 한 번
파괴로 일관되게 정리된다.
트리 워커 — parser_walk_tree
섹션 제목: “트리 워커 — parser_walk_tree”CUBRID의 모든 패스는 parser_walk_tree 한 함수의 변주다. 깊이
우선으로 내려가며 pre-함수 를, 자식이 끝나면 post-함수 를
호출한다는 점이다.
// parser_walk_tree — src/parser/parse_tree_cl.c (condensed)PT_NODE *parser_walk_tree (PARSER_CONTEXT * parser, PT_NODE * node, PT_NODE_WALK_FUNCTION pre_function, void *pre_arg, PT_NODE_WALK_FUNCTION post_function, void *post_arg);pre-함수가 자식으로 내려가지 않을 결정을 할 수도 있다 —
*continue_walk 를 PT_STOP_WALK 으로 세팅하면 그 서브트리는
스킵된다. 이 한 가지 노브(knob)가 부분 패스(partial pass) 와
상호작용 회피(short-circuit) 를 깔끔하게 표현한다. 예를 들어
이름 해석 패스가 이미 해소된 서브트리에 다시 들어가지 않는다
든가, 타입 추론이 묵시 CAST 안의 자식을 한 번만 본다든가 하는
규칙이 모두 이 노브로 표현된다.
sequenceDiagram
participant W as parser_walk_tree
participant PRE as pre_function
participant POST as post_function
participant N as PT_NODE
W->>N: enter(node)
W->>PRE: pre(parser, node, arg, &cont)
alt cont == PT_CONTINUE_WALK
loop 모든 자식 슬롯
W->>W: walk_tree(child, ...)
end
W->>POST: post(parser, node, arg, &cont)
else PT_STOP_WALK
note right of W: 자식 진입 스킵
end
W->>N: leave(node)
이름 해석 — pt_resolve_names
섹션 제목: “이름 해석 — pt_resolve_names”가장 먼저, 그리고 가장 카탈로그 의존도가 높은 패스다. 모든
PT_NAME 노드를 카탈로그의 정확한 한 항목 — 클래스, 속성, 별칭,
호스트 변수, 사용자 정의 함수 — 에 바인딩하는 단계다. 잘못된
바인딩은 곧 모호 컬럼 오류 또는 정의되지 않은 식별자 오류로
이어진다.
// pt_resolve_names — src/parser/name_resolution.c (condensed)PT_NODE *pt_resolve_names (PARSER_CONTEXT * parser, PT_NODE * statement, SEMANTIC_CHK_INFO * sc_info){ /* push outermost scope (the statement's FROM clauses) */ scopestack_push (sc_info, statement);
statement = parser_walk_tree (parser, statement, pt_bind_names_pre, sc_info, pt_bind_names_post, sc_info);
scopestack_pop (sc_info); return statement;}스코프 스택은 표준 SQL의 가장 안쪽 FROM 부터 바깥으로 규칙을
직접적으로 모델링한다. SELECT 안의 SELECT 가 들어오면 스택에
새 프레임을 푸시하고, 빠져 나오며 팝한다. PT_NAME 노드가 들어
오면 스택의 가장 위 프레임부터 차례대로 검색해 처음 매치되는
항목을 채택한다는 점이다. 한 프레임 안에서 둘 이상이 매치되면
모호 컬럼 오류이며, 어느 프레임에서도 안 잡히면 알 수 없는
이름 오류다.
flowchart LR
N["PT_NAME 'a.b'"] --> S0{"스코프 0\n현재 SELECT의 FROM"}
S0 -- "match" --> R0["바인딩\n db_object* 채움"]
S0 -- "no match" --> S1{"스코프 1\n바깥 SELECT의 FROM"}
S1 -- "match" --> R0
S1 -- "no match" --> S2{"...\n전역 카탈로그"}
S2 -- "match" --> R0
S2 -- "no match" --> ERR["err: undefined name"]
S0 -- "ambiguous" --> ERR2["err: ambiguous column"]
해석된 이름은 info.name.db_object (또는 info.name.spec_id) 슬롯에
박힌다. 다음 패스부터는 문자열을 다시 보지 않는다. 이 점이
이후 패스의 비용을 결정적으로 낮춘다.
flowchart TB PRE["pt_bind_names_pre\n - PT_SELECT 진입 시 scope push\n - PT_NAME 만나면 lookup"] POST["pt_bind_names_post\n - PT_SELECT 종료 시 scope pop\n - 바인딩 결과 검증"] CTX["SEMANTIC_CHK_INFO\n - scope_stack\n - top_node\n - donot_fold 등 플래그"] PRE --> CTX POST --> CTX
타입 추론 — pt_semantic_type
섹션 제목: “타입 추론 — pt_semantic_type”이름이 다 해소되고 나면 다음 패스가 모든 식에 도메인을 부여
한다. 이 패스의 출발 함수는 pt_semantic_type 이며, 본체는
pt_eval_expr_type, pt_eval_function_type, pt_eval_value_type 등
종별 별 분기로 갈린다.
// pt_semantic_type — src/parser/type_check.c (condensed)PT_NODE *pt_semantic_type (PARSER_CONTEXT * parser, PT_NODE * tree, SEMANTIC_CHK_INFO * sc_info){ if (sc_info) { sc_info->donot_fold = false; }
tree = parser_walk_tree (parser, tree, pt_eval_type_pre, sc_info, pt_eval_type_post, sc_info);
if (pt_has_error (parser)) return NULL; return tree;}post-함수에서 자식들이 모두 형이 굳은 뒤에야 부모의 형을 결정할
수 있다는 점이 핵심이다. 따라서 타입 추론은 상향식 (bottom-up)
이다. 식의 형이 결정되면 그 식이 부모 문맥에서 기대하는
expected_domain 과 비교되고, 다르면 묵시 CAST 노드가 그 사이에
끼워진다는 점이다.
flowchart TD ROOT["PT_EXPR (op = PT_PLUS)\n type = ?"] ROOT --> L["PT_NAME 'a'\n type = INT"] ROOT --> R["PT_VALUE 0.5\n type = DOUBLE"] L --> RC["CAST(a AS DOUBLE)\n type = DOUBLE\n자동 합성"] ROOT2["PT_EXPR (op = PT_PLUS)\n type = DOUBLE"] --> RC ROOT2 --> R
이 패스가 옵티마이저의 입력 계약을 결정한다는 점이 중요하다. 옵티마이저는 모든 식이 이미 구체적인 도메인으로 굳어 있다고 가정하고 동작한다. 그래서 묵시 형변환 비용은 컴파일 타임에 이미 트리에 고정 되며, 런타임에 가서 형이 흔들릴 일이 없다.
의미 검사 — pt_semantic_check
섹션 제목: “의미 검사 — pt_semantic_check”타입까지 굳고 나면 본격적인 의미적 규칙 검사가 시작된다.
이 패스의 진입점이 pt_semantic_check 이며, 종별마다 별도
sub-walker를 호출하는 구조다.
// pt_semantic_check — src/parser/semantic_check.c (condensed)PT_NODE *pt_semantic_check (PARSER_CONTEXT * parser, PT_NODE * node){ SEMANTIC_CHK_INFO sc_info = { node, NULL, 0, 0, 0, false, false };
node = pt_resolve_names (parser, node, &sc_info); if (!node || pt_has_error (parser)) return NULL;
node = pt_check_with_info (parser, node, &sc_info); if (!node || pt_has_error (parser)) return NULL;
return node;}pt_check_with_info는 node->node_type 별로 분기한다. SELECT/UPDATE
/DELETE 는 pt_check_query, pt_check_update, pt_check_delete 로,
DDL 은 pt_check_create_entity, pt_check_alter 로 — 종별마다
독립한 검사 트리가 따로 있다는 점이다.
각 sub-walker가 점검하는 항목은 대체로 다음과 같이 묶을 수 있다.
| 그룹 | 대표 검사 |
|---|---|
| 집계 / GROUP BY | GROUP BY 식이 select list 에 포함되어야 함, HAVING 에서 |
| 비집계 식이 GROUP BY 에 없을 때 거부 | |
| ORDER BY / DISTINCT | UNION 의 양변 컬럼 수 일치, ORDER BY 식이 select 에서 보여야 함 |
| 부속 질의 (subquery) | 스칼라 부속 질의가 단일 행/단일 컬럼인지, EXISTS 의 인자가 |
| 부울로 강제 가능한지 | |
| 도메인 호환성 | UPDATE 의 SET 좌변 도메인 ↔ 우변 식의 도메인 |
| 권한 | 참조 클래스에 대한 SELECT/INSERT/UPDATE/DELETE 권한 |
| 제약 | NOT NULL 컬럼에 NULL 대입, CHECK 식의 부울 가능성 |
오류는 pt_record_error 로 컨텍스트의 큐에 누적된다. 의도적으로
첫 오류에서 멈추지 않는 구현이며, 이는 한 컴파일에서 다수의
오류를 한꺼번에 보고하기 위함이다.
flowchart LR
IN["pt_check_with_info"] --> SW{"node_type?"}
SW -- "PT_SELECT" --> Q["pt_check_query"]
SW -- "PT_UPDATE" --> U["pt_check_update"]
SW -- "PT_DELETE" --> D["pt_check_delete"]
SW -- "PT_INSERT" --> I["pt_check_insert"]
SW -- "PT_CREATE_ENTITY" --> CE["pt_check_create_entity"]
SW -- "PT_ALTER" --> AL["pt_check_alter"]
SW -- "..." --> ETC["..."]
Q --> AGG["GROUP BY · HAVING · 집계"]
Q --> SUB["부속 질의 · UNION 컬럼 수"]
U --> DOM["좌·우 도메인 호환성"]
CE --> CAT["카탈로그 충돌·권한"]
묵시 의미 합성 — 뷰, 트리거, 기본값
섹션 제목: “묵시 의미 합성 — 뷰, 트리거, 기본값”의미 검사가 통과하면 사용자가 적지 않은 의미 가 트리에 합성 되어 들어간다. 본 문서는 헤드라인만 다루지만, 이 단계에서 무엇이 일어나는지를 알고 있어야 다음 패스의 입력 모양을 이해할 수 있다.
- 뷰 펼치기 (view expansion). FROM 에 등장한 뷰 이름은
mq_translate단계에서 그 정의 본문(SELECT)으로 치환된다. 즉, 옵티마이저가 보는 트리에는 뷰가 없다 — 정의 본문이 인라인 되어 있다. - 트리거 본문 부착 (trigger body attachment). INSERT/UPDATE /DELETE 에 트리거가 정의되어 있다면, 트리거 본문이 같은 트리의 자식으로 매달려 들어간다. 실행기는 이 자식 노드를 별도 단계로 실행한다.
- 기본값 채우기 (default value fill-in). 명시되지 않은 컬럼을 카탈로그의 기본 식이 INSERT 의 값 목록 에 합성된다.
- AUTO_INCREMENT / SERIAL. SERIAL 컬럼은 그 시퀀스의 NEXT VALUE 식이 자동 합성되어 들어간다.
이들은 모두 원본 사용자 SQL 의 토큰 위치를 상속 한다는 점이 중요하다. 합성된 노드도 적절한 라인/컬럼을 보유하므로, 뷰 본문 안에서 발생한 형 오류도 여전히 사용자 SQL의 어느 토큰 을 가리킬 수 있다.
질의 재작성 — pt_rewrite
섹션 제목: “질의 재작성 — pt_rewrite”마지막으로, 옵티마이저에 넘기기 전에 기계적으로 더 다루기 좋은 모양 으로 트리를 다듬는다. 이 단계는 의미를 바꾸지 않는다는 약속을 지킨다 — 즉, 같은 결과를 내되 옵티마이저가 더 쉽게 읽을 수 있는 형태로 모양만 바꾼다는 점이다.
대표적인 변형은 다음과 같다.
- 상수 접기 (constant folding).
1 + 2는3으로 평탄화된다. 단, NULL 의미와 묵시 형변환 규칙을 깨지 않는 범위에서만. - 부분식 평탄화 (subquery flattening).
EXISTS (SELECT 1 FROM T WHERE ...)같은 패턴은 가능하면 세미 조인(semi-join)으로 끌어올려진다. - 외부 조인 정리 (outer join cleanup). 외부 조인의 보존 측에 영향을 미치지 않는 술어는 ON 절에서 WHERE 로, 또는 그 반대로 옮겨진다.
- DNF/CNF 정규화. 술어 트리가 옵티마이저가 인덱스 매칭하기 좋은 형태(보통 CNF)로 정규화된다.
본 문서는 이 변형들의 목록 까지만 다룬다. 각 변형의 정확한 조건과 예외는 옵티마이저 분석서의 영역이다.
오류 보고 — pt_record_error 와 형제 노드
섹션 제목: “오류 보고 — pt_record_error 와 형제 노드”CUBRID 컴파일러의 오류 큐는 단순한 리스트다. pt_record_error 가
노드 위치 + 메시지 코드 + 인자 를 큐의 끝에 매달고, pt_has_error
가 큐가 비어 있지 않은지를 검사한다. 호출자는 보통 패스 사이에
이 검사를 끼워 넣어 오류가 누적된 채로 더 깊이 들어가지 않게
한다는 점이다.
// pt_record_error / pt_has_error — src/parser/parser.h, semantic_check.cvoid pt_record_error (PARSER_CONTEXT *, int stmt_no, int line, int col, const char *msg, const char *context);bool pt_has_error (const PARSER_CONTEXT *);오류 메시지는 카탈로그의 메시지 카탈로그(message catalog) 에서 포맷 문자열로 가져오고, 인자만 노드별로 채워 넣는다. 이 점이 국제화(i18n)와 메시지 일관성을 동시에 보장한다.
호스트 변수와 준비 문 (host variables & prepared statements)
섹션 제목: “호스트 변수와 준비 문 (host variables & prepared statements)”CUBRID는 :hv 형태의 호스트 변수를 컴파일 시점에 플레이스홀더
로만 두고, 실행 시에 바인딩된 값으로 채운다. 의미 검사 단계에서
호스트 변수는 다음 둘 중 하나로 처리된다.
- 타입이 사용처에서 강제된다.
WHERE a = :hv의:hv는a의 도메인으로 굳어진다. 이 강제는expected_domain슬롯에 남는다. - 타입을 강제할 문맥이 없으면 동적으로 둔다. 이 경우 실행 시점에 첫 바인딩된 값의 도메인이 채택되며, 그 뒤 컴파일된 계획은 그 도메인 가정 아래 캐시된다.
PARSER_CONTEXT::host_var_table 이 호스트 변수의 위치/형/이름을
모은다. 이 테이블이 곧 prepared statement 의 인자 명세 가 된다.
pt_check_with_info — 통합 드라이버
섹션 제목: “pt_check_with_info — 통합 드라이버”위에서 본 패스들은 모두 한 함수의 호출 시퀀스로 묶여 있다. 본
문서의 영문판이 the four-stage pipeline 이라 부르는 이 함수가
pt_check_with_info 다. 진입점은 pt_compile 이며, 그 안에서
pt_semantic_check → pt_check_with_info 순으로 호출된다.
// pt_compile — src/parser/compile.cPT_NODE *pt_compile (PARSER_CONTEXT * parser, PT_NODE * volatile statement){ PT_NODE *next;
PT_SET_JMP_ENV (parser);
if (statement) { next = statement->next; statement->next = NULL;
statement = pt_semantic_check (parser, statement);
/* restore link */ if (statement) { statement->next = next; } }
PT_CLEAR_JMP_ENV (parser);
return statement;}statement->next = NULL 한 줄이 핵심이다. 다중 문(multi-statement)
리스트가 들어오면 매 호출마다 현재 문 을 떼어 내고 그 한 문을
만 의미 검사를 수행한다는 점이다. setjmp/longjmp 봉투는
패스 깊숙한 곳에서 발생한 의미 오류가 호출 스택으로 코드를
일일이 끌고 올라오지 않고도 안전하게 빠져 나오도록 한다. 이는
파서 영역의 panic-mode 복구 관행과 같은 모양이다.
pt_check_with_info 본체는 SELECT/UPDATE/DELETE/INSERT/MERGE 같은
DML과 SELECT/UNION/INTERSECT/EXCEPT 같은 질의를 다음 시퀀스를
적용한다. 단계의 경계 가 그대로 모듈 분할을 따라간다는 점이
중요하다.
flowchart LR PARSE["parser_main\n(bison/flex)"] --> PT["PT_NODE 트리\n(미해소)"] PT --> COMP["pt_compile"] COMP --> SC["pt_semantic_check"] SC --> CWI["pt_check_with_info\n(문 종별 switch)"] CWI --> RN["1) pt_resolve_names\n이름 해석"] RN --> CW["2) pt_check_where\nWHERE 안 집계 검출"] CW --> RH["3) pt_check_and_replace_hostvar\n호스트 변수 치환"] RH --> SCL["4) parser_walk_tree\n(post=pt_semantic_check_local)\n문 종별 재작성·검사\n· pt_semantic_type"] SCL --> EI["pt_expand_isnull_preds\nIS NULL 펼치기"] EI --> CNF["pt_do_cnf / pt_cnf\n술어 CNF 정규화"] CNF --> XASL["xasl_generation"] SCL -. "재진입" .-> PST["pt_semantic_type"] PST --> EVTP["pt_eval_type_pre\n(top-down)"] PST --> EVTPOST["pt_eval_type\n(bottom-up)\n → pt_eval_expr_type\n → pt_eval_function_type\n → pt_where_type"] PST --> FOLD["pt_fold_constants_pre\n· pt_fold_constants_post"]
이 그림에서 한 가지 숨겨진 디테일이 있다. 4번 박스 안의
pt_semantic_check_local 은 LIMIT 같은 재작성을 트리에 합성한 직후
pt_semantic_type 을 재귀적으로 호출한다. 새로 합성된 식의 형이
아직 굳지 않았기 때문이다. 따라서 타입 추론·상수 접기 패스는
문 단위로 한 번만 도는 것이 아니라 재작성이 한 번 일어날 때마다
다시 돌게 된다. 마지막의 CNF 단계는 모든 재작성과 접기가 완전히
가라앉은 뒤 에 단 한 번 실행된다는 점이다.
단계 1 — 이름 해석
섹션 제목: “단계 1 — 이름 해석”pt_resolve_names 의 다섯 sub-walker는 위 §이름 해석 절에서
다룬 그대로다. 한 가지 더 짚는다면, pt_flat_spec_pre 가
*카탈로그를 읽는 유일한 패스다. 이 패스가 끝나면
모든 PT_SPEC 에 flat_entity_list 와 db_object 가 박혀 있고,
이후 패스는 다시 클래스 이름 문자열을 들고 카탈로그를 두드리지
않는다.
// pt_resolve_names — src/parser/name_resolution.c (condensed)PT_NODE *pt_resolve_names (PARSER_CONTEXT * parser, PT_NODE * statement, SEMANTIC_CHK_INFO * sc_info){ PT_BIND_NAMES_ARG bind_arg; PT_FLAT_SPEC_INFO info;
bind_arg.scopes = NULL; bind_arg.spec_frames = NULL; bind_arg.sc_info = sc_info;
/* Replace each Entity Spec with an Equivalent flat list */ info.spec_parent = NULL; info.for_update = false; statement = parser_walk_tree (parser, statement, pt_flat_spec_pre, &info, pt_continue_walk, NULL);
/* Mark PT_NAME nodes that are inside GROUP BY / HAVING. */ statement = parser_walk_tree (parser, statement, pt_mark_group_having_pt_name, NULL, NULL, NULL);
/* The main name-binding walk. */ statement = parser_walk_tree (parser, statement, pt_bind_names, &bind_arg, pt_bind_names_post, &bind_arg);
/* Resolve alias references in GROUP BY / HAVING. */ statement = parser_walk_tree (parser, statement, pt_resolve_group_having_alias, NULL, NULL, NULL);
/* Convert NATURAL JOIN into INNER/OUTER JOIN by synthesizing the equi-predicates. */ statement = parser_walk_tree (parser, statement, NULL, NULL, pt_resolve_natural_join, NULL);
/* FOR UPDATE: flag the affected specs. */ if (statement && statement->node_type == PT_SELECT && PT_SELECT_INFO_IS_FLAGED (statement, PT_SELECT_INFO_FOR_UPDATE)) { // ... condensed: flag each spec with PT_SPEC_FLAG_FOR_UPDATE_CLAUSE ... }
return statement;}다섯 sub-walker는 다음과 같이 분담한다.
-
Spec 펼치기 (
pt_flat_spec_pre) —entity_name을 상속(inheritance) 을 따라가며 평탄화한다. CUBRID는 ORDBMS 이므로SELECT FROM Parent한 줄이 모든 자식 클래스를 건드릴 수 있다.ALL ... EXCEPT문법은 특정 자식 클래스를 빼낼 수 있고, 결과 리스트는 정확히 어떤 물리 클래스를 건드릴지를 박아 둔다. -
GROUP BY / HAVING 마킹 (
pt_mark_group_having_pt_name) — GROUP BY / HAVING 안에 등장한PT_NAME에CPTR_PT_NAME_IN_GROUP_HAVING태그를 붙인다. 그 다음 패스가 여기서는 SELECT-list 별칭을 FROM-list 컬럼보다 우선해 봐야 하나 를 결정할 수 있게 하기 위함이다. -
이름 바인딩 (
pt_bind_names+pt_bind_names_post) — 위 §이름 해석 절의 본 패스. 스코프 푸시/팝과PT_NAME해소를pt_bind_name_or_path_in_scope로 위임한다. -
별칭 해석 (
pt_resolve_group_having_alias) — GROUP BY / HAVING 의PT_NAME중 SELECT-list 별칭과 일치하는 것을 SELECT -list 노드의 복사본 으로 치환한다. 후 처리인 이유는 분명 하다. 바인더가 먼저 FROM-list 에서 찾기를 시도해 실패한 뒤 에 별칭으로 폴백하는 의미를 보존해야 하기 때문이다. -
NATURAL JOIN 재작성 (
pt_resolve_natural_join) — 트리에서info.spec.natural == true인PT_SPEC을 찾아, 좌·우 spec 의 동명 컬럼마다lhs.col = rhs.col등가 술어를 합성하여 우측 spec 의on_cond에 매단다. 그 결과 옵티마이저는 자연 조인을 알지 못한다 — 명시적 ON 을 가진 일반 INNER JOIN 으로 보일 뿐이다.
Spec 펼치기 — pt_flat_spec_pre
섹션 제목: “Spec 펼치기 — pt_flat_spec_pre”CUBRID 가 spec 을 리스트로 펼쳐 두는 이유는 ORDBMS 의 클래스 계층(class hierarchy)을 의미 검사 시점에 물리적 관계의 집합 으로 이미 굳히기 위함이다. 옵티마이저가 보는 트리에는 부모 클래스 한 이름이 아니라 그 부모를 포함한 모든 자식 클래스가 나란히 들어 있어야 인덱스 선택 같은 결정이 가능하기 때문이다.
각 PT_SPEC 에는 펼치기 이후 다음 세 슬롯이 채워진다.
id—PT_SPEC노드 자신의 주소(UINTPTR캐스트). 이후 모든PT_NAME::info.name.spec_id가 이 키를 들고 다닌다는 점이다.db_object— 워크스페이스(workspace) 포인터. 이미pt_class_pre_fetch에서 락이 걸리고 캐시에 올라가 있는 상태다.flat_entity_list— 한 물리 클래스마다 하나씩 매달린PT_NAME의 단일 연결 리스트. 각 노드가 자기db_object를 들고 있어 속성 조회는 그 한 노드만 보면 된다.
이름 바인딩과 스코프 스택
섹션 제목: “이름 바인딩과 스코프 스택”스코프 스택은 선언 역순 — 가장 안쪽이 머리 — 으로 연결된 단일 연결 리스트다. 각 프레임은 그 스코프가 가시화하는 spec 들의 리스트 하나만 들고 있다.
// SCOPES — src/parser/name_resolution.c:78typedef struct scopes SCOPES;struct scopes{ SCOPES *next; /* next outermost scope */ PT_NODE *specs; /* list of PT_SPEC nodes */ unsigned short correlation_level; /* how far up the stack was a name found? */ short location; /* for outer join */};
typedef struct pt_bind_names_arg PT_BIND_NAMES_ARG;struct pt_bind_names_arg{ SCOPES *scopes; PT_EXTRA_SPECS_FRAME *spec_frames; SEMANTIC_CHK_INFO *sc_info;};correlation_level 의 값이 0 이 아니면 상관 부속질의 (correlated
subquery) 라는 뜻이다. 즉, 안쪽 SELECT 의 평가가 바깥 행에 의존
하므로 옵티마이저가 부속질의 평탄화(flattening)를 거부해야 한다.
location 은 외부 조인의 ON 술어에 등장한 모든 PT_NAME 에 동일
값을 매겨서 옵티마이저가 그 술어를 조인 밖으로 밀어 낼 수 없도록
한다는 점이다.
flowchart TD
subgraph OUTER["바깥 SELECT 스코프"]
OS["specs: history h, olympic o"]
end
subgraph INNER["부속질의 스코프 (상관)"]
IS["specs: prize p"]
IS --> ISN["p.year:\n IS 안에서 매치 — level 0"]
IS --> ISO["o.host_year:\n IS 안 매치 없음\n 바깥으로 — level 1\n → 부속질의 = 상관"]
end
INNER -- "next" --> OUTER
ISO -. "spec_id ← &outer_PT_SPEC\nresolved ← 'o'" .-> OS
스코프 푸시는 질의 노드로 들어갈 때 pt_bind_scope 가 처리한다.
이 함수가 새 스코프가 가시화할 spec 들을 모아 SCOPES *new 를
구성하고, new->next = bind_arg->scopes 로 머리에 끼워 넣은 뒤
bind_arg->scopes = new 로 갱신한다는 점이다. 팝은 post-walk
콜백에서 일어난다. parser_walk_tree 의 두 콜백 시그니처가 이
푸시·팝 짝맞춤을 기계적으로 보장 한다는 게 핵심이다.
* 의 펼치기
섹션 제목: “* 의 펼치기”SELECT * FROM code 의 * 은 파서가 별도의 star 종 PT_VALUE
로 만들어 넣는다. SELECT-list 단계에서 바인더가 만나는 순간
pt_resolve_star 가 다음을 한다. (a) 그 별이 가리키는 spec 또는
모든 spec 을 찾고, (b) 각 spec 의 flat_entity_list 가 가진
DB_ATTRIBUTE 집합을 읽어, (c) 속성마다 새 PT_NAME 을 한 개씩
합성해 next 로 잇는다는 점이다. 합성된 노드들은 다시 일반
바인더에 들어가 spec_id, data_type 을 채운다.
flowchart LR
STAR["PT_VALUE ('*')\nSELECT list 안"] --> EXP["pt_resolve_star"]
EXP --> NL["새 PT_NAME 리스트:\n s_name → f_name → ..."]
NL --> BIND["pt_bind_name_or_path_in_scope\n바인더 재진입"]
BIND --> DONE["각 PT_NAME 에\n spec_id, resolved,\n data_type 채움"]
Derived table, 부속질의, CTE
섹션 제목: “Derived table, 부속질의, CTE”부속질의 (PT_SELECT 안의 PT_SELECT) 와 derived table (FROM-list
의 PT_SPEC 바로 아래에 매달린 PT_SELECT) 은 모두 새 스코프 를
연다. 다만 derived table 에는 한 가지 추가 규칙이 있다. 바깥
스코프에 노출되기 전에 안쪽이 먼저 형까지 굳어 있어야 한다 는
점이다. 그래야 바깥 스코프가 derived table 의 as_attr_list 컬럼을
형에 따라 묶어 줄 수 있다. pt_bind_scope 는 이를 위해
derived_table 이 비어 있지 않은 spec 을 만나면 안쪽으로 재귀
하여 pt_semantic_type 까지 부른 뒤, 안쪽 select-list 의 형 정보를
바깥 spec 의 as_attr_list 에 되짚어 넣는다.
CTE 는 pt_bind_names_in_with_clause / pt_bind_names_in_cte 가
같은 모양으로 처리한다. CTE 본문은 derived table 처럼 먼저 바인딩
되고, CTE 의 이름과 컬럼 리스트가 WITH 절 본문 SELECT 의 스코프에
하나의 PT_SPEC 로 들어간다는 점이다.
Oracle 식 외부 조인 — (+) 문법
섹션 제목: “Oracle 식 외부 조인 — (+) 문법”Oracle 의 WHERE a.col(+) = b.col 문법은 외부성을 조인이 아니라
술어에 매달아 두는 모양이다. 파서는 (+) 를 만나면 해당 PT_EXPR
에 PT_EXPR_INFO_RIGHT_OUTER / PT_EXPR_INFO_LEFT_OUTER 플래그만
붙여 놓는다. 그러면 이름 해석 시점에 pt_check_Oracle_outerjoin
이 그 술어를 찾아 좌·우 spec 을 식별하고, FROM 순서로 본 조인의
방향 을 추론한 뒤 술어를 WHERE 에서 우측 spec 의 on_cond 로
이주시킨다. 그 spec 의 join_type 에는 추론된 LEFT/RIGHT OUTER 가
박힌다. 이후 파이프라인은 일반적인 ANSI 외부 조인 만 본다는
점이다.
좌·우 spec 의 FROM 순서가 술어와 거꾸로 잡혀 있다면 — 예를 들어
FROM x, y WHERE y.i = x.i(+) — 재작성기는 join_type 을 한 번
뒤집는다.
// pt_check_Oracle_outerjoin (excerpt) — src/parser/name_resolution.cif (lhs_spec && rhs_spec && lhs_spec->info.spec.id != rhs_spec->info.spec.id) { /* found edge: set join type and spec */ if (lhs_location < rhs_location) /* left-to-right in FROM order */ { p_spec = lhs_spec; spec = rhs_spec; } else /* SELECT ... FROM x, y WHERE y.i = x.i(+) */ { join_type = (join_type == PT_JOIN_LEFT_OUTER) ? PT_JOIN_RIGHT_OUTER : PT_JOIN_LEFT_OUTER; p_spec = rhs_spec; spec = lhs_spec; } }코너 케이스 한 가지: SELECT ... FROM x, y ON y.i(+) = 1 같은
무의미한 외부 조인 술어는 PT_JOIN_NONE 으로 무너지고 술어는
다시 WHERE 로 돌아간다. 재작성기가 문맥상 (+) 가 실제 조인
요청이 아닌 구문적 흔적 이라는 것을 인지한다는 의미다.
NATURAL JOIN 재작성
섹션 제목: “NATURAL JOIN 재작성”NATURAL JOIN 은 동명 컬럼들에 대한 등가 술어를 동반한 INNER JOIN
의 사탕(syntactic sugar) 이다. 파서는 이를 info.spec.natural == true 인 PT_SPEC 으로 가져 둔다. (참고로 PT_JOIN_NATURAL 이라는
join-type enum 값도 존재하지만 실제로는 사용되지 않는다. 사용
되는 신호는 spec 의 natural 플래그 한 비트뿐이다.)
pt_resolve_natural_join 은 트리를 훑다가 자연 조인 spec 을
만나면, 좌·우 양측의 속성 리스트를 모은 뒤 동명 컬럼마다
lhs.col = rhs.col 의 PT_EXPR 노드를 합성하여 우측 spec 의
on_cond 끝에 잇는다. 이때 좌·우의 속성 리스트는 spec 의 종류에
따라 다른 출처에서 가져온다는 점이다. 일반 spec 이면
flat_entity_list 의 DB_ATTRIBUTE 집합, derived table 이면 안쪽
SELECT 의 select-list, CTE 면 CTE 의 attribute 리스트.
-- 변환 전SELECT DISTINCT h.host_year, o.host_nationFROM history h NATURAL JOIN olympic oWHERE o.host_year > 1950;
-- pt_resolve_natural_join 이후SELECT DISTINCT h.host_year, o.host_nationFROM history h INNER JOIN olympic o ON h.host_year = o.host_yearWHERE o.host_year > 1950;재작성이 끝나면 info.spec.natural 도 false 로 클리어된다. 이후
패스는 그 spec 을 그냥 INNER JOIN 으로 본다는 점이다.
단계 2 — WHERE 절 안 집계 검사 (pt_check_where)
섹션 제목: “단계 2 — WHERE 절 안 집계 검사 (pt_check_where)”작지만 빠질 수 없는 검사다. WHERE 절 안에 집계(SUM, AVG, …) 나
분석(ROW_NUMBER, RANK, …) 함수가 등장하면 의미상 잘못된 위치
이므로 거부한다. 집계는 HAVING 으로 옮겨야 하고, 분석 함수는
SELECT-list 또는 ORDER BY 에서만 정의된다는 표준 규칙을 코드로
강제하는 자리다.
// pt_check_where — src/parser/semantic_check.c:17503PT_NODE *pt_check_where (PARSER_CONTEXT * parser, PT_NODE * node){ PT_NODE *function = NULL;
/* check if exists aggregate/analytic functions in where clause */ function = pt_find_aggregate_analytic_in_where (parser, node); if (function != NULL) { if (pt_is_aggregate_function (parser, function)) { PT_ERRORm (parser, function, MSGCAT_SET_PARSER_SEMANTIC, MSGCAT_SEMANTIC_INVALID_AGGREGATE); } else { PT_ERRORm (parser, function, MSGCAT_SET_PARSER_SEMANTIC, MSGCAT_SEMANTIC_NESTED_ANALYTIC_FUNCTIONS); } }
return node;}분석 함수가 WHERE 에 들어왔을 때 재사용되는
MSGCAT_SEMANTIC_NESTED_ANALYTIC_FUNCTIONS 는 이름은 NESTED 지만
의미적으로 다른 자리에 있어야 할 함수 노드 라는 분류를 공유하기
때문에 그대로 쓰인다.
단계 3 — 호스트 변수 치환 (pt_check_and_replace_hostvar)
섹션 제목: “단계 3 — 호스트 변수 치환 (pt_check_and_replace_hostvar)”호스트 변수는 클라이언트가 실행 시점에 바인딩하는 ? 또는 :name
파라미터다. 파싱 직후에는 모두 PT_TYPE_MAYBE 의 PT_HOST_VAR 로
존재한다. 이 패스는 값이 이미 컴파일 시점에 알려진 변수만
PT_VALUE 로 치환하고, 실제로 미정인 것은 그대로 둔다. 그리고
호스트 변수가 OBJECT 형 파라미터를 참조 하는 경우에는 statement
의 cannot_prepare 플래그를 켠다 — 이런 문장은 매 실행마다 의미
검사를 다시 거쳐야 하므로 prepared statement 캐시를 만들 수 없다는
뜻이다.
단계 4 — 문 종별 재작성 + 타입 검사
섹션 제목: “단계 4 — 문 종별 재작성 + 타입 검사”pt_semantic_check_local 은 semantic_check.c 에서 가장 큰 함수다.
pt_check_with_info 가 띄우는 parser_walk_tree 의 post-order
콜백 으로 등록되어, 부속질의가 그 부모 질의보다 먼저 처리되도록
한다는 점이다. 그래야 형 정보가 자식에서 부모로 위로 전파될 수
있다.
// pt_check_with_info (excerpt) — src/parser/semantic_check.cnode = parser_walk_tree (parser, node, NULL, NULL, pt_semantic_check_local, sc_info_ptr);이 함수는 node->node_type 으로 종별 분기한다. PT_SELECT 한
종별만 보더라도 일이 많다.
-
SELECT-list 검사:
pt_check_into_clause(INTO :var의 인자 수가 select-list 길이와 일치하는지, 부속질의 안에서 INTO 를 쓰지 않았는지).MSGCAT_SEMANTIC_NOT_SINGLE_COL는 다중 컬럼 부속질의 검사를 의미 단계에서 다시 한 번 잡아 준다.WITH INCREMENT FOR col은 숨겨진incr(col)선택으로 재작성된다. -
GROUP BY 검사:
MSGCAT_SEMANTIC_SORT_SPEC_RANGE_ERR는 select -list 길이를 넘는 위치 GROUP BY,MSGCAT_SEMANTIC_NO_GROUPBY_ALLOWED는 호스트 변수를 GROUP BY 에 쓰는 경우(실행 시점에 위치가 변할 수 있어 거부),MSGCAT_SEMANTIC_CANNOT_USE_GROUPBYNUM_WITH_ROLLUP는 드물지만WITH ROLLUP HAVING GROUPBY_NUM()결합을 거부한다. -
집계 / 분석 함수 인자 검사:
COUNT(*),ROW_NUMBER(),RANK(),DENSE_RANK(),CUME_DIST(),PERCENT_RANK()같은 소수 화이트리스트를 제외하면 모든 집계·분석 함수는arg_list가 비어 있을 수 없다.MEDIAN은OVER (ORDER BY ...)와 함께 쓸 수 없다.PARTITION BY NULL은 의미 없으므로 제거되고ORDER BY <const>로 대체되어 분석 함수의 형식이 무너지지 않게 한다. -
ORDER BY 검사: 단일 행 집계처럼 ORDER BY 가 의미 없는 경우 잘라 내고, 위치 ORDER BY 의 범위를 검사한다.
-
계층 질의(CONNECT BY) 검사:
CONNECT_BY_ISLEAF와CONNECT_BY_ISCYCLE은 prior expr 문맥 밖에서 거부된다.LEVEL분석으로 사이클 검사가 필요한지 결정한다. -
SHOW 문 재작성: SHOW 종별마다 정해진 시스템 카탈로그 SELECT 로 플레이스홀더 select-list 가 바뀐다. 즉,
SHOW TABLES는 의미 검사 단계에서 이미 시스템 테이블에 대한 일반 SELECT 로 치환 된다는 점이다. -
Derived 질의 검사:
as_attr_list의 이름이 유일한지 (MSGCAT_SEMANTIC_AMBIGUOUS_REF_TO), 숨겨진 컬럼에 대해서는ha_<n>같은 합성 이름을 채워 컬럼 수가 맞도록 한다. -
CAST 적격성:
pt_check_cast_op이 source → target 강제 변환이 강제 변환표(coercion table) 안에서 허용되는지 검사한다. 표에 있지만 NO 인 경우MSGCAT_SEMANTIC_CANT_COERCE_TO, 표에 아예 없는 경우MSGCAT_SEMANTIC_COERCE_UNSUPPORTED이다. -
LIMIT 재작성 — 아래에서 따로 다룬다.
-
pt_semantic_type호출: 위 재작성이 모두 끝난 뒤에 마지막 으로pt_semantic_type을 부른다. 이 호출이 타입 검사 + 상수 접기 패스의 출발점이다.
LIMIT → 번호식 재작성
섹션 제목: “LIMIT → 번호식 재작성”LIMIT 은 N행에서 멈춰라 라는 명령형 의미를 가진다. 그러나 옵티마이저는 멈춤 을 모르고 술어 만 안다. 그래서 의미 검사 단계에서 LIMIT 은 주변 절의 모양에 따라 적절한 번호식 술어 로 재작성된다는 점이다.
| 문맥 | 재작성 |
|---|---|
| LIMIT, WHERE 만 | WHERE 끝에 inst_num() <= ? 추가 |
| LIMIT, ORDER BY 동반 | orderby_num() <= ? 추가 (자르기 전에 정렬은 보존) |
| LIMIT, GROUP BY 동반 | HAVING 끝에 groupby_num() BETWEEN 1 AND ? 추가 |
| LIMIT, DISTINCT 만 | ORDER BY 케이스로 처리 (DISTINCT 는 자르기 목적상 정렬과 동등) |
| LIMIT, 집계 안 | 집계 SELECT 를 derived table 로 감싸고 바깥에서 LIMIT 적용 |
-- example 4SELECT * FROM code LIMIT 3;=> SELECT code.s_name, code.f_name FROM code code WHERE (inst_num() <= ?:0);
-- example 1 — LIMIT 이 분석 함수의 ORDERBY_NUM 안으로 들어감SELECT PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY math) AS pdisc FROM scores LIMIT 5;=> ... WITHIN GROUP (ORDER BY math FOR ORDERBY_NUM() BETWEEN 1 AND 5) ...이 재작성은 pt_semantic_type 이 호출되기 전에 일어나기 때문에
타입 검사 패스가 보는 트리는 이미 일반 WHERE/HAVING + 번호식의
모양이다. LIMIT 이 등장한 절을 알아야 어느 번호식을 박을지 결정할
수 있으므로, top-down 인 pt_eval_type_pre 안에서 처리되는 것이
자연스럽다.
pt_semantic_type — 형 검사 + 상수 접기 엔진
섹션 제목: “pt_semantic_type — 형 검사 + 상수 접기 엔진”// pt_semantic_type — src/parser/type_checking.cPT_NODE *pt_semantic_type (PARSER_CONTEXT * parser, PT_NODE * tree, SEMANTIC_CHK_INFO * sc_info_ptr){ /* ... condensed: derive sc_info_ptr->has_dblink from tree ... */
/* do type checking */ tree = parser_walk_tree (parser, tree, pt_eval_type_pre, sc_info_ptr, pt_eval_type, sc_info_ptr); if (pt_has_error (parser)) { return NULL; }
/* Parsing static sql is only for semantic check. Any kind of execution should be avoided */ if (!parser->flag.is_parsing_static_sql) { PT_NODE *spec_list = NULL; /* do constant folding */ tree = parser_walk_tree (parser, tree, pt_fold_constants_pre, &spec_list, pt_fold_constants_post, sc_info_ptr); if (pt_has_error (parser)) { tree = NULL; } }
return tree;}parser_walk_tree 호출 두 번이 본체의 전부다.
-
타입 검사 — pre 콜백
pt_eval_type_pre가 top-down 으로 LIMIT 재작성·재귀 식 처리·derived 외부 조인 플래그 같은 자식이 필요로 하는 셋업 을 마치고, post 콜백pt_eval_type이 bottom-up 으로 각 노드의 형을 확정한다는 점이다.pt_eval_type은 종별로 분기한다 —PT_EXPR→pt_eval_expr_type,PT_FUNCTION→pt_eval_function_type,PT_CREATE_INDEX / PT_DELETE / PT_UPDATE→ 각 search-condition 에 대한pt_where_type. -
상수 접기 — pre 콜백
pt_fold_constants_pre는 진입을 결정한다 (BENCHMARK 의 인자는 접지 않도록 진입을 막는다). post 콜백pt_fold_constants_post가 각PT_EXPR/PT_FUNCTION의 모든 인자가PT_VALUE인지 확인하고, 그렇다면 컴파일 시점에 평가하여 결과 값으로 노드 자체를 치환한다.
is_parsing_static_sql 가드가 두 번째 패스를 완전히 건너뛴다
는 점에 주의한다. 정적 SQL 파싱은 의미만 보고 실행하지 않는
모드로, 런타임 상태에 의존하는 접기를 발생시키지 않기 위함이다.
PT_EXPR 의 타입 검사 — pt_eval_expr_type
섹션 제목: “PT_EXPR 의 타입 검사 — pt_eval_expr_type”이 함수는 자식의 형이 모두 굳은 뒤 부모 PT_EXPR 를 호출
된다. 진입부에서 최대 세 인자 슬롯과 그 호스트 변수 형제를 모은
뒤, 본체의 거대한 switch 가 본격적인 분류를 시작한다는 점이다.
// pt_eval_expr_type — src/parser/type_checking.c (head, condensed)static PT_NODE *pt_eval_expr_type (PARSER_CONTEXT * parser, PT_NODE * node){ PT_OP_TYPE op = node->info.expr.op; PT_NODE *arg1 = NULL, *arg2 = NULL, *arg3 = NULL; PT_NODE *arg1_hv = NULL, *arg2_hv = NULL, *arg3_hv = NULL; PT_TYPE_ENUM arg1_type = PT_TYPE_NONE, arg2_type = PT_TYPE_NONE; PT_TYPE_ENUM arg3_type = PT_TYPE_NONE, common_type = PT_TYPE_NONE; /* ... condensed: enumeration-comparison fast path ... */
arg1 = node->info.expr.arg1; if (arg1) { arg1_type = arg1->type_enum; if (arg1->node_type == PT_HOST_VAR && arg1->type_enum == PT_TYPE_MAYBE) arg1_hv = arg1; /* ... condensed: unwrap unary-minus / PRIOR / CONNECT_BY_ROOT around a host var ... */ } /* ... arg2, arg3 similarly ... */}이후 본체가 하는 일을 여섯 묶음으로 정리할 수 있다.
-
연산자 별 손-쓴 규칙. 산술 (
PT_PLUS,PT_MINUS),PT_BETWEEN_*패밀리,PT_LIKE,PT_TO_CHAR,PT_FROM_TZ,PT_NEW_TIME,PT_IS_IN/PT_IS_NOT_IN같은 좁은 집합은 switch 안에서 직접 형을 결정한다. 예를 들어 산술 규칙은 날짜·시각 × 정수 행렬을 코드로 표현한다 —TIMESTAMP - TIMESTAMP는 초 단위BIGINT,INT + DATE는DATE, 등. -
시그니처 표 매칭 (
pt_apply_expressions_definition). 그 외 모든 연산자는pt_get_expression_definition (op, &def)가 돌려주는 미리 만들어 둔EXPRESSION_DEFINITION테이블에서 오버로드를 골라낸다. 매처는 인자 슬롯마다pt_are_equivalent_types점수를 매겨 합산한다. 가장 높은 점수의 오버로드가 채택되며, 어느 오버로드도 unmatchable-free 하지 못하면 0번 오버로드로 폴백 — 이후pt_coerce_expression_argument가 묵시 CAST 를 끼워 넣지 못해 결국 다운스트림에서 오류가 난다는 의미다.// pt_apply_expressions_definition (scoring loop, condensed) — src/parser/type_checking.c:5778best_match = 0;matches = -1;for (i = 0; i < def.overloads_count; i++){int match_cnt = 0;if (pt_are_unmatchable_types (def.overloads[i].arg1_type, arg1_type)) { match_cnt = -1; continue; }if (pt_are_equivalent_types (def.overloads[i].arg1_type, arg1_type)) match_cnt++;if (pt_are_unmatchable_types (def.overloads[i].arg2_type, arg2_type)) { match_cnt = -1; continue; }if (pt_are_equivalent_types (def.overloads[i].arg2_type, arg2_type)) match_cnt++;if (pt_are_unmatchable_types (def.overloads[i].arg3_type, arg3_type)) { match_cnt = -1; continue; }if (pt_are_equivalent_types (def.overloads[i].arg3_type, arg3_type)) match_cnt++;if (match_cnt == 3) { best_match = i; break; } /* perfect — short-circuit *//* ... condensed: track running best_match by match_cnt > matches ... */} -
콜레이션 호환성 검사 (
pt_check_expr_collation). 문자열 인자는 양쪽이 같은 콜레이션을 갖거나 한쪽이 강제 변환 가능 해야 한다. CUBRID 의 콜레이션 시스템은 explicit (사용자 지정), implicit (컬럼 기본), coercible (리터럴) 의 세 단계를 둔다. 더 explicit 한 쪽이 우선되며, 두 인자가 모두 explicit 인 채로 충돌하면 오류다. -
호스트 변수 후기 바인딩 (
pt_is_op_hv_late_bind). 일부 연산자 — 범위 비교,?인자에 대한 산술 — 는 호스트 변수의 정확한 형 결정을 실행 시점까지 늦출 수 있다. 이 경우expected_domain을DB_TYPE_VARIABLE도메인으로 두고 다시 클리어 한다는 점이 재미있다. XASL 생성기에 “이건 후기 바인딩 CAST 니까 조기에 굳히 지 말 것” 이라는 신호를 보내는 셈이다. -
자기-증명 형.
PT_RAND→ numeric,PT_YEAR→ integer,PT_EXTRACT→ integer,PT_COALESCE→ 공통 형 — 처럼 인자와 무관하게 반환 형이 연산자 자체로 정해지는 경우들이다. 시그 니처 매칭 결과를 덮어쓴다. -
형이 굳은 호스트 변수의 역전파. 매처가 반환 형을 결정하면, 그 결정에 의해 호스트 변수가 도메인에 묶여 들어간다는 점이다.
pt_eval_expr_type은 추론된 도메인을 호스트 변수의expected_domain에 박아 두어, 이후 패스와 실행기가 바인딩 시점 에 그 도메인으로 강제 변환하도록 한다.
함수가 반환할 때면 node->type_enum 이 굳어 있고, 필요한 경우
node->data_type 이 정밀도/콜레이션을 들고 할당되어 있으며,
필요한 자리에 PT_CAST 가 자식 위로 끼워져 있다.
재귀식 — GREATEST, CASE, …
섹션 제목: “재귀식 — GREATEST, CASE, …”GREATEST, LEAST, COALESCE (좌측 재귀) 와 CASE, DECODE
(우측 재귀) 는 같은 모양의 PT_EXPR 가 사슬로 이어지는 형태로
파싱된다. 사슬은 부분별로 본다면 두 인자식밖에 없지만, 전체를
한 식으로 봐야 마지막 공통 형이 모든 링크에 동일하게 전파된다는
점이다.
pt_eval_type_pre 의 재귀식 처리는 사슬의 끝까지 한 번 내려간
뒤, 거기서 eval_recursive_expr_type 을 반복 호출하며 위로 올라
오면서 공통 형을 한 번에 결정한다. 결정된 recursive_type 은
각 링크의 expr.recursive_type 슬롯에 박혀, 링크별
pt_apply_expressions_definition 이 각자 추론하지 않고 그 슬롯을
읽도록 한다.
flowchart TD CASE1["PT_EXPR (CASE)\narg1: cond\narg2: val\narg3: → CASE2"] CASE2["PT_EXPR (CASE)\narg1: cond\narg2: val\narg3: → CASE3"] CASE3["PT_EXPR (CASE)\narg1: cond\narg2: val\narg3: → CASE4"] CASE4["PT_EXPR (CASE)\narg1: cond\narg2: val\narg3: const"] CASE1 --> CASE2 --> CASE3 --> CASE4 PRE["eval_type_pre 가 끝까지\n내려가 leaf vals 로\n공통 형을 만든 뒤,\n모든 recursive_type 에\n박아 둠"] POST["eval_type 은 bottom-up.\n각 링크는 자기 recursive_type\n슬롯을 읽고 결정"]
PT_FUNCTION 의 타입 검사
섹션 제목: “PT_FUNCTION 의 타입 검사”pt_eval_function_type 이 PT_FUNCTION 을 다룬다. 함수는 임의 개수의
인자를 가질 수 있다는 점이 식과 다르다. CUBRID 는 두 구현이
공존한다.
pt_eval_function_type_old— 오랜 C 구현. 빌트인 집계, 분석, 대부분의 문자열·수치 함수.pt_eval_function_type_new—func_type::Node를 중심으로 한 C++17 구현. 현재 JSON 함수,BENCHMARK패밀리, 새REGEXP_*패밀리가 이쪽으로 가 있다. 신규 함수는 C++ 경로를 우선한다는 방향성이다.
두 경로 모두 함수별 시그니처 리스트를 읽고 인자 형으로 최선의
오버로드를 고르는, pt_apply_expressions_definition 과 같은 점수
규칙을 쓴다.
pt_where_type — 술어 컷오프
섹션 제목: “pt_where_type — 술어 컷오프”자식 형이 모두 굳은 뒤 WHERE 자체에 한 번 더 증명적으로 참 / 거짓
인 conjunct 를 잘라 내는 패스가 있다. 파서는 WHERE 를 이미 CNF
모양 — next = AND, or_next = OR — 의 리스트로 넘기므로, 컷오프
로직은 리스트를 한 번 훑으면서 다음 두 정리만 적용한다.
// pt_where_type (excerpt) — src/parser/type_checking.c:6495PT_NODE *pt_where_type (PARSER_CONTEXT * parser, PT_NODE * where){ PT_NODE *cnf_node, *dnf_node, *cnf_prev, *dnf_prev; bool cut_off; short location;
/* traverse CNF list and keep track the pointer to previous node */ cnf_prev = NULL; while ((cnf_node = ((cnf_prev) ? cnf_prev->next : where))) { // ... condensed: extract location, validate logical type ...
if (cnf_node->or_next == NULL) { if (cnf_node->node_type == PT_VALUE && cnf_node->type_enum == PT_TYPE_LOGICAL && cnf_node->info.value.data_value.i == 1) { cut_off = true; /* ... AND TRUE AND ... → drop the conjunct */ } else if (cnf_node->node_type == PT_VALUE && cnf_node->type_enum == PT_TYPE_LOGICAL && cnf_node->info.value.data_value.i == 0) { if (cnf_node == where && cnf_node->next == NULL) return where; goto always_false; /* ... AND FALSE AND ... → whole WHERE is false */ } } else { /* DNF inner pass: drop FALSEs from OR-list, short-circuit on first TRUE */ // ... condensed ... }
// ... condensed: advance cnf_prev or splice cnf_node out ... } return where;}두 정리는 다음과 같다.
A ∧ TRUE ∧ B→A ∧ B(conjunct 를 빼낸다).A ∧ FALSE ∧ B→FALSE(전체 WHERE 를 단일 falsePT_VALUE로 교체).
DNF 안쪽 루프는 그 이중 (dual) 을 적용한다.
D ∨ TRUE ∨ E→TRUE(disjunct 전체를 단일 true 로 단축).D ∨ FALSE ∨ E→D ∨ E(FALSE 를 빼낸다).
같은 컷오프 로직이 ON, HAVING, START WITH / CONNECT BY, ORDERBY-FOR 절에도 동일하게 적용된다.
상수 접기
섹션 제목: “상수 접기”pt_fold_constants_post 가 노드별 접기 함수다. PT_EXPR 이면
pt_fold_const_expr 에 위임한다. 모든 인자가 PT_VALUE 인지를
확인한 뒤, 실행기가 런타임에 쓰는 평가기 (pt_evaluate_db_value_expr)
와 같은 코드로 컴파일 시점에 평가하고 그 결과 PT_VALUE 로
노드를 통째로 교체한다는 점이다. PT_FUNCTION 도 같은 패턴으로
pt_fold_const_function 이 처리한다.
pre 콜백 pt_fold_constants_pre 는 단 한 가지 역할이다 —
BENCHMARK 의 인자식에 내려가지 못하도록 PT_LIST_WALK 를 반환
한다. 벤치마크의 대상 식은 측정 의미상 런타임에 평가되어야 하기
때문이다.
SEMANTIC_CHK_INFO::donot_fold 는 또 한 단계의 게이트다. 호출자가
이 플래그를 켜면 pt_semantic_type 의 두 번째 패스가 통째로 건너
뛰어진다. 뷰 변환 (view transform) 직후의 재검사 같은 자리에서
변환이 만든 구조를 접기로 지워 버리지 않도록 막는 용도다.
형 인프라 — PT_DATA_TYPE 와 TP_DOMAIN
섹션 제목: “형 인프라 — PT_DATA_TYPE 와 TP_DOMAIN”CUBRID 의 파스 트리는 자체 형 표현 (PT_TYPE_ENUM + PT_DATA_TYPE)
을 갖는다. 엔진의 DB_TYPE + TP_DOMAIN 와는 별개 다. 두
표현이 공존하는 이유는 분명하다. 파스 트리는 정밀도·스케일·콜레이션
·코드셋·열거값 같은 파라미터화된 메타데이터를 파서 노드 모양에
맞춰 들고 다녀야 하고, 엔진의 도메인 캐시는 빠른 비교를 위해
표준화된 단일 인스턴스 가 되어야 한다는 점이다.
| 계층 | 헤더 | 역할 |
|---|---|---|
| 파스 트리 형 | parser/parse_tree.h | PT_TYPE_ENUM, PT_DATA_TYPE_INFO — 파서 측 형 메타 |
| 엔진 도메인 | object/object_domain.h | TP_DOMAIN — 빌트인 + 사용자 정의 형 디스크립터, 캐시됨 |
| 프리미티브 형 | object/object_primitive.h | PR_TYPE — 프리미티브별 직렬화·비교·크기 |
| 디스크 레이아웃 | object/object_representation.h | OR_* — 저장용 물리 바이트 레이아웃 |
파스 트리에 등장한 모든 PT_DATA_TYPE 은 pt_data_type_to_db_domain
이 TP_DOMAIN 으로 사상한다. 같은 파라미터화된 형이 두 번 나오면
tp_Domains[] 캐시로 같은 포인터 로 dedup 된다는 점이
중요하다. 이 덕분에 형 동일성 비교가 깊은 비교가 아니라 ==
한 번으로 끝난다.
단계 5 — CNF 정규화 (pt_do_cnf / pt_cnf)
섹션 제목: “단계 5 — CNF 정규화 (pt_do_cnf / pt_cnf)”모든 재작성과 접기가 가라앉으면 pt_do_cnf 가 트리를 한 번 더
훑으며 SELECT 마다 WHERE 와 HAVING 를 pt_cnf 를 호출한다.
// pt_do_cnf — src/parser/cnf.c:1190PT_NODE *pt_do_cnf (PARSER_CONTEXT * parser, PT_NODE * node, void *arg, int *continue_walk){ PT_NODE *list, *spec;
if (node->node_type != PT_SELECT) return node;
list = node->info.query.q.select.where; if (list) { for (; list; list = list->next) PT_EXPR_INFO_CLEAR_FLAG (list, PT_EXPR_INFO_CNF_DONE);
node->info.query.q.select.where = pt_cnf (parser, node->info.query.q.select.where);
/* annotate each conjunct with the spec(s) it touches — used by predicate pushdown */ for (spec = node->info.query.q.select.from; spec; spec = spec->next) pt_tag_terms_with_specs (parser, node->info.query.q.select.where, spec, spec->info.spec.id); }
list = node->info.query.q.select.having; if (list) { for (; list; list = list->next) PT_EXPR_INFO_CLEAR_FLAG (list, PT_EXPR_INFO_CNF_DONE); node->info.query.q.select.having = pt_cnf (parser, node->info.query.q.select.having); }
return node;}PT_EXPR_INFO_CNF_DONE 의 clear-then-set 프로토콜은 의도적이다.
뷰 변환은 이미 CNF 태그가 붙어 있는 술어를 다시 흘려 넣을 수 있는데,
같은 컴파일 안에서 두 번째 진입은 그 술어를 새 입력으로 봐야
하기 때문이다.
pt_cnf 본체는 교과서 그대로의 세 단계다.
// pt_cnf — src/parser/cnf.c:941PT_NODE *pt_cnf (PARSER_CONTEXT * parser, PT_NODE * node){ PT_NODE *list = NULL, *cnf, *next, *last = NULL; CNF_MODE mode;
do { next = node->next; node->next = NULL;
if (node->node_type == PT_VALUE || PT_EXPR_INFO_IS_FLAGED (node, PT_EXPR_INFO_CNF_DONE)) { /* already in CNF (or a folded constant) — splice in unchanged */ cnf = node; // ... condensed: append to (list, last) ... } else { /* AND/OR form: push NOT inward via De Morgan */ node = pt_and_or_form (parser, node);
/* if too many disjuncts, do CNF in OR-tree-pruning mode */ mode = (count_and_or (parser, node) > 100) ? TRANSFORM_CNF_OR_COMPACT : TRANSFORM_CNF_AND_OR;
/* distribute disjunction over conjunction */ cnf = parser_walk_tree (parser, node, pt_transform_cnf_pre, &mode, pt_transform_cnf_post, &mode); // ... condensed: append cnf to (list, last) ...
/* tag every conjunct as done */ for (last = cnf; last->next; last = last->next) PT_EXPR_INFO_SET_FLAG (last, PT_EXPR_INFO_CNF_DONE); PT_EXPR_INFO_SET_FLAG (last, PT_EXPR_INFO_CNF_DONE); }
node = next; } while (next);
list = parser_walk_tree (parser, list, NULL, NULL, pt_tag_start_of_cnf_post, NULL); return list;}세 단계는 다음과 같다.
- AND/OR form (
pt_and_or_form) — 함의·동치를 AND/OR 로 풀고, De Morgan 으로 NOT 을 leaf 술어 바로 앞까지 밀어 넣는다. - 분배 (
pt_transform_cnf_*) —A ∨ (B ∧ C) ≡ (A ∨ B) ∧ (A ∨ C)를 반복 적용해 식을 평탄한 conjunction-of-disjunction 모양으로 만든다. 100 개를 넘는 OR 자식이 있으면 모드를TRANSFORM_CNF_OR_COMPACT로 전환한다 — 분배가 쉽게 지수적으로 conjunct 수를 부풀릴 수 있기 때문에, 매우 넓은 식에서는 OR 의 고수준 모양을 보존하는 보수적 모드로 폴백한다는 점이다. - 태깅 — 각 conjunct 에
PT_EXPR_INFO_CNF_DONE을 박아, 이후pt_cnf가 다시 분배에 들어가는 것을 막는다.
마지막의 pt_tag_start_of_cnf_post 는 CNF 리스트의 첫 conjunct
에 별도 플래그를 추가로 박아, 다운스트림 코드가 헤드와 멤버를
구분할 수 있게 한다.
pt_tag_terms_with_specs 의 spec-id 비트셋 태깅은 CNF 후 에
일어난다. 각 conjunct 가 어느 spec 들의 컬럼을 건드리는지를
비트셋으로 박아 두어, 옵티마이저가 conjunct 단위로 술어 push-down
대상을 결정할 수 있도록 한다는 점이다.
flowchart LR RAW["WHERE (a OR b) AND NOT (c AND d)"] RAW --> AOF["pt_and_or_form\n→ (a OR b) AND (NOT c OR NOT d)"] AOF --> DIST["pt_transform_cnf_∗\n→ (a OR b) AND (NOT c OR NOT d)\n (이미 CNF — 평탄)"] DIST --> TAG["각 conjunct 에\nPT_EXPR_INFO_CNF_DONE +\nspec-id 비트셋 태그"] TAG --> OPT["xasl_generation\n· 옵티마이저가\nconjunct 별 push-down 결정"]
문 종별 커버리지
섹션 제목: “문 종별 커버리지”pt_check_with_info 의 switch 는 파서가 만들 수 있는 모든 종별을
다룬다. 같은 파이프라인을 쓰지만 적용되는 패스 집합 은 종별마다
다르다는 점이다.
| 문 종별 | pt_check_with_info 가 하는 일 |
|---|---|
PT_SELECT, PT_UNION, PT_INTERSECTION, PT_DIFFERENCE | 풀 파이프라인: 이름 해석, WHERE 검사, 호스트 변수 치환, semantic_check_local (semantic_type 호출), IS NULL 펼치기, CNF |
PT_INSERT, PT_UPDATE, PT_DELETE, PT_MERGE | 풀 파이프라인 + DML 별 대입 / 대상 검증 |
PT_CREATE_INDEX, PT_ALTER_INDEX, PT_DROP_INDEX | pt_resolve_names + pt_check_create_index / pt_check_function_index_expr / pt_check_filter_index_expr; 호스트 변수 단계 없음 |
PT_CREATE_ENTITY, PT_ALTER, PT_DROP, … | DDL 별 검사기 (속성 이름 유일성, 파티션 범위 검증, FK 유효성, …) |
PT_EVALUATE, PT_DO, PT_SET_* | 풀 파이프라인 — SQL 수준의 식·명령이라 형이 필요 |
PT_HOST_VAR, PT_VALUE, PT_NAME, PT_EXPR, PT_FUNCTION | 단독 식 — 풀 파이프라인 |
DDL 경로가 호스트 변수 단계를 건너뛰는 이유는 명확하다.
MSGCAT_SEMANTIC_HOSTVAR_IN_DDL 가 DDL 안의 호스트 변수를 금지
하므로, 파서가 parser->host_var_count 를 본 즉시 의미 검사 전
에 거부한다는 점이다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼명을 anchor로 삼는다 — 라인 번호가 아니다. CUBRID 소스는 시간이 지나면 변한다. 그에 비해 함수명·struct 태그·enum 태그 같은 심볼은 잘 변하지 않는다. 현재 위치는
git grep -n '<symbol>' src/parser/로 찾으면 된다. 이 섹션 끝의 위치 힌트 표는 문서가 마지막으로updated:된 시점의 관찰값이며 시간이 지나면 어긋날 수 있다.
최상위 드라이버 (src/parser/)
섹션 제목: “최상위 드라이버 (src/parser/)”pt_compile(compile.c) — 단일 문 진입점. longjmp 봉투를 깔고pt_semantic_check를 호출한다.pt_class_pre_fetch(compile.c) — 의미 검사 전 에 호출자가 부르는 사전 락 단계. 의미 검사 자체는 아니지만,pt_flat_spec_pre가 워크스페이스에서 모든 관계를 찾을 수 있게 하는 전제 조건이다.pt_semantic_check(semantic_check.c) —pt_check_with_info의 얇은 래퍼.pt_check_with_info(semantic_check.c) — 문 종별 switch. 네 sub-pass 가 그 위에서 돈다.
이름 해석 (name_resolution.c)
섹션 제목: “이름 해석 (name_resolution.c)”SCOPES,PT_BIND_NAMES_ARG(name_resolution.c:78,87) — 스코프 스택 프레임과 워크 상태 인자.pt_resolve_names— 다섯 단계 드라이버.pt_flat_spec_pre— 각PT_SPEC을flat_entity_list로 펼침 (상속과ALL ... EXCEPT처리).pt_mark_group_having_pt_name— GROUP BY / HAVING 의PT_NAME태깅.pt_bind_names,pt_bind_names_post— 이름 바인딩 본 패스 (스코프 푸시/팝,PT_DOT_처리).pt_bind_scope— 새SCOPES프레임 푸시. derived table 또는 CTE 면 노출 전에 안쪽으로 재귀.pt_bind_name_or_path_in_scope— 단일PT_NAME또는PT_DOT_경로의 스코프 검색.pt_get_resolution,pt_find_entity_in_scopes,pt_find_outer_entity_in_scopes— 바인더의 워커 프리미티브.pt_resolve_star,pt_resolve_star_reserved_names— SELECT-list 의*펼치기.pt_resolve_correlation— 이름이 바깥으로 walk 한 만큼correlation_level을 증가. 옵티마이저가 읽음.pt_resolve_group_having_alias,_pt_name,_pt_expr,_pt_sort_spec,_internal— GROUP BY / HAVING 별칭 해석.pt_resolve_natural_join,pt_resolve_natural_join_internal— NATURAL JOIN → INNER JOIN ON 재작성.pt_check_Oracle_outerjoin,pt_clear_Oracle_outerjoin_spec_id— Oracle(+)→ ANSI 재작성.pt_bind_names_in_with_clause,pt_bind_names_in_cte— CTE 바인딩.pt_bind_names_merge_insert,pt_bind_names_merge_update— MERGE 전용 바인딩.pt_make_flat_name_list,pt_make_subclass_list— flat entity 리스트 빌더.
WHERE 안 집계 검사 (semantic_check.c)
섹션 제목: “WHERE 안 집계 검사 (semantic_check.c)”pt_check_where— 술어 트리에서 집계·분석 함수를 찾아MSGCAT_SEMANTIC_INVALID_AGGREGATE또는MSGCAT_SEMANTIC_NESTED_ANALYTIC_FUNCTIONS발생.pt_find_aggregate_analytic_in_where,pt_is_aggregate_function— 술어 헬퍼.
호스트 변수 치환 (semantic_check.c)
섹션 제목: “호스트 변수 치환 (semantic_check.c)”pt_check_and_replace_hostvar—parser_walk_tree콜백.SET_HOST_VARIABLES_IF_INTERNAL_STATEMENT(매크로) — 부모 parser 컨텍스트에서 호스트 변수를 상속한다. 뷰 변환·트리거 등 내부에서 합성된 문에 사용.
문 종별 검사 (semantic_check.c)
섹션 제목: “문 종별 검사 (semantic_check.c)”pt_semantic_check_local— 문 종별 검사·재작성을 구동하는 post-order 콜백.pt_semantic_type의 호출지이기도 하다.pt_check_into_clause,pt_check_into_clause_for_static_sql— INTO 인자 수와 부속질의 안 INTO 검사.pt_check_create_index,pt_check_function_index_expr,pt_check_filter_index_expr— DDL 인덱스 식 검사.pt_check_cast_op— CAST 의 형/콜레이션 적격성.pt_check_unique_attr— 컬럼 중복 검사.pt_check_range_partition_strict_increasing,pt_coerce_partition_value_with_data_type— 파티션 범위 검증.pt_expand_isnull_preds,pt_expand_isnull_preds_helper—col IS NULL을 상속에 따른 클래스 판별 술어들의 분해로 펼침.pt_semantic_check_local후, CNF 전 에 동작 (CNF 가 펼쳐진 형태를 봐야 하기 때문).pt_mark_union_leaf_nodes— UNION leaf 플래그를 위로 전파.
타입 검사·상수 접기 (type_checking.c)
섹션 제목: “타입 검사·상수 접기 (type_checking.c)”pt_semantic_type— 드라이버. 타입 워크와 접기 워크를 호출.pt_eval_type_pre— top-down 콜백 (LIMIT 재작성, 재귀식, derived 외부 조인 플래그, sort-spec 부속질의 플래그, search 조건 정리).pt_eval_type— bottom-up 콜백 (노드 종별로pt_eval_expr_type,pt_eval_function_type,pt_where_type분기).pt_eval_expr_type—PT_EXPR의 연산자-규칙 + 시그니처 매칭.pt_eval_function_type,pt_eval_function_type_old,pt_eval_function_type_new—PT_FUNCTION의 동등물._new경로는 신규 함수(JSON, REGEXP, BENCHMARK)를func_type::Node(func_type.cpp) 에 위임.pt_apply_expressions_definition— 시그니처 표 매처.pt_get_expression_definition— 연산자별EXPRESSION_DEFINITION반환.pt_coerce_expression_argument— 인자를 targetTP_DOMAIN으로PT_CAST감쌈.pt_check_expr_collation— 콜레이션 호환성 검사.pt_is_op_hv_late_bind— 연산자 술어. 호스트 변수 인자가 후기 바인딩 가능하면 true.pt_upd_domain_info— 연산자와 인자에 따라PT_EXPR의data_type정밀도/스케일 갱신.pt_where_type,pt_where_type_keep_true— 결정된 부울값에 따른 술어 컷오프.pt_fold_constants_pre— 진입 게이트 (BENCHMARK).pt_fold_constants_post— 노드별 접기.pt_fold_const_expr— 모든 인자가PT_VALUE인PT_EXPR접기.pt_fold_const_function— 같은 의미의PT_FUNCTION접기.pt_evaluate_db_value_expr— 컴파일 시점 평가 프리미티브 (parse_evaluate.c). 접기 함수가 사용.
형 인프라 (parse_dbi.c, object_domain.c)
섹션 제목: “형 인프라 (parse_dbi.c, object_domain.c)”pt_data_type_to_db_domain(parse_dbi.c) —PT_DATA_TYPE→TP_DOMAIN변환.tp_domain_new,tp_domain_construct(object_domain.c) — 새 도메인 할당. 파서 측 할당기가 이를 감쌈.tp_Domains[]—DB_TYPE별 전역 도메인 캐시.tp_is_domain_cached— 파라미터화된 도메인의 캐시 조회 프리미티브.
CNF 정규화 (cnf.c)
섹션 제목: “CNF 정규화 (cnf.c)”pt_do_cnf—PT_SELECT의 WHERE/HAVING 을 찾아 CNF 를 도는 최상위 워커.pt_cnf— 세 단계 변환 (AND/OR form → 분배 → 태깅).pt_and_or_form— ⇒ / ⇔ 제거, NOT 안쪽으로 밀기.pt_aof_to_cnf— AOF → CNF 재귀 헬퍼.pt_distributes_disjunction— OR 의 AND 위 분배.pt_transform_cnf_pre,pt_transform_cnf_post,pt_tag_start_of_cnf_post—parser_walk_tree콜백.pt_tag_terms_with_specs— 각 CNF conjunct 에 spec-id 비트셋 주석.count_and_or—TRANSFORM_CNF_AND_OR↔TRANSFORM_CNF_OR_COMPACT모드를 가르는 disjunct 수 게이트.PT_EXPR_INFO_CNF_DONE— CNF 후 conjunct 에 박히는 플래그.
이 개정 시점의 위치 힌트 (2026-04-30)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30)”이 라인 번호는 문서가 마지막으로 updated: 된 시점에 관찰한
값이다. 다른 정의에 도착한다면, 위의 심볼명이 정본이다 — 지나가는
길에 표를 갱신해 주면 된다.
| 심볼 | 파일 | 라인 |
|---|---|---|
pt_compile | parser/compile.c | 381 |
pt_class_pre_fetch | parser/compile.c | 432 |
pt_semantic_check | parser/semantic_check.c | 12523 |
pt_check_with_info | parser/semantic_check.c | 12060 |
pt_check_where | parser/semantic_check.c | 17503 |
pt_check_and_replace_hostvar | parser/semantic_check.c | 11934 |
pt_semantic_check_local | parser/semantic_check.c | 10865 |
pt_check_into_clause | parser/semantic_check.c | 10684 |
pt_check_into_clause_for_static_sql | parser/semantic_check.c | 10657 |
pt_check_create_index | parser/semantic_check.c | 8888 |
pt_expand_isnull_preds | parser/semantic_check.c | 222 |
pt_mark_union_leaf_nodes | parser/semantic_check.c | 269 |
SCOPES struct | parser/name_resolution.c | 78 |
PT_BIND_NAMES_ARG struct | parser/name_resolution.c | 87 |
pt_bind_name_or_path_in_scope | parser/name_resolution.c | 841 |
pt_bind_scope | parser/name_resolution.c | 1206 |
pt_bind_names_post | parser/name_resolution.c | 1467 |
pt_bind_names | parser/name_resolution.c | 1974 |
pt_flat_spec_pre | parser/name_resolution.c | 4735 |
pt_resolve_star_reserved_names | parser/name_resolution.c | 7391 |
pt_resolve_star | parser/name_resolution.c | 7460 |
pt_resolve_natural_join_internal | parser/name_resolution.c | 8914 |
pt_resolve_natural_join | parser/name_resolution.c | 9026 |
pt_resolve_group_having_alias_pt_sort_spec | parser/name_resolution.c | 9142 |
pt_resolve_group_having_alias_pt_name | parser/name_resolution.c | 9158 |
pt_resolve_group_having_alias_pt_expr | parser/name_resolution.c | 9211 |
pt_resolve_group_having_alias_internal | parser/name_resolution.c | 9269 |
pt_resolve_group_having_alias | parser/name_resolution.c | 9302 |
pt_resolve_names | parser/name_resolution.c | 9350 |
pt_bind_names_merge_insert | parser/name_resolution.c | 10837 |
pt_bind_names_merge_update | parser/name_resolution.c | 10933 |
pt_bind_names_in_with_clause | parser/name_resolution.c | 11147 |
pt_bind_names_in_cte | parser/name_resolution.c | 11202 |
pt_coerce_expression_argument | parser/type_checking.c | 4473 |
pt_apply_expressions_definition | parser/type_checking.c | 5778 |
pt_where_type | parser/type_checking.c | 6495 |
pt_where_type_keep_true | parser/type_checking.c | 6689 |
pt_eval_type_pre | parser/type_checking.c | 7082 |
pt_fold_constants_pre | parser/type_checking.c | 7487 |
pt_fold_constants_post | parser/type_checking.c | 7661 |
pt_eval_type | parser/type_checking.c | 7714 |
pt_eval_expr_type | parser/type_checking.c | 8706 |
pt_upd_domain_info | parser/type_checking.c | 11245 |
pt_eval_function_type | parser/type_checking.c | 12285 |
pt_fold_const_expr | parser/type_checking.c | 17579 |
pt_fold_const_function | parser/type_checking.c | 18799 |
pt_semantic_type | parser/type_checking.c | 19073 |
pt_is_op_hv_late_bind | parser/type_checking.c | 20260 |
pt_check_expr_collation | parser/type_checking.c | 22076 |
pt_aof_to_cnf | parser/cnf.c | 255 |
pt_cnf | parser/cnf.c | 941 |
pt_do_cnf | parser/cnf.c | 1191 |
소스 검증 노트
섹션 제목: “소스 검증 노트”각 항목은 현재 소스에 대한 사실이며, 원본 분석 자료를 함께 보지 않아도 그 자체로 읽힌다. 끝의 부연은 어떻게 검증되었는지와, 관련이 있을 때 역사적 드리프트 또는 검증의 한계를 적는다. “미해결 질문”은 큐레이터가 해결을 미루고 기록해 둔 갭이다 — 차후 독자는 이를 알려진 버그가 아니라 시작점으로 다룬다.
원본 분석 자료(버전 0.5 ~ 1.0, 사내 RND-2팀 리뷰)는 일부 지점에서 현재 소스보다 뒤처져 있다.
-
pt_semantic_check_local이 더 이상pt_semantic_type의 유일한 호출처가 아니다. 원본 자료는pt_semantic_check_local을 semantic_type 호출 + 상수 접기 단계로 묘사한다. 현재 소스는pt_check_with_info가 워크들을 스케줄링하며,pt_semantic_type호출은pt_semantic_check_local안 (문 단위) 과pt_check_with_info(DML 이 아닌 종별, 예를 들어PT_CREATE_INDEX는semantic_check.c:12257에서 직접 호출) 모두 에서 일어난다. -
pt_eval_function_type이 두 갈래로 갈라졌다. 원본 자료는pt_eval_function_type한 함수만 거론한다. 현재 소스에는pt_eval_function_type_old와pt_eval_function_type_new가 공존하며, 새 C++17 경로 (func_type.cpp의func_type::Node) 는 JSON, BENCHMARK, REGEXP 패밀리를 담당한다. 다른 함수는 대부분 여전히 옛 경로를 쓴다. -
PT_JOIN_NATURAL은 죽은 enum 값이다. 원본 자료가 “사용되지 않음”으로 표시한 그대로다. enum 정의는parse_tree.h에 남아 있지만, NATURAL JOIN 의 실제 신호는PT_SPEC의bool natural플래그다.PT_JOIN_FULL_OUTER도 죽어 있다 — CUBRID 는 FULL OUTER JOIN 을 지원하지 않는다. -
Oracle 외부 조인의 무의미한 술어 처리. 원본 자료가
SELECT ... FROM x, y ON y.i(+) = 1이PT_JOIN_NONE으로 무너 지고 술어가 WHERE 로 돌아간다고 적은 부분은 그대로 유효하다.pt_check_Oracle_outerjoin의 post-walk 에서 검증된다. -
CNF 의
mode스위치 임계값.pt_cnf는count_and_or > 100이면TRANSFORM_CNF_OR_COMPACT로 전환한다. CNF 자료에는 이 지수적 폭발 가드가 언급되지 않는다. 비교적 최근의 hardening 이며, 소스 쪽이 정본이다. -
pt_check_into_clause에 정적 SQL 변형이 추가됐다. 종별별 자료의 INTO 절은 일반 인자 수 검사만 다룬다. 현재 소스는pt_check_into_clause_for_static_sql도 갖고 있고, 이는 정적 SQL 파싱 시점에 같은 검사를 수행한다 (is_parsing_static_sql플래그가 접기 게이트와도 같은 신호다). -
호스트 변수의
expected_domain처리가 5단계로 성숙했다. 자료는 단일 후기 바인딩 단계로 묘사한다. 현재 소스는 호스트 변수를 다섯 가지 settle-then-revisit 동작을 한다. 형 unwrap (-?,prior ?처리), 후기 바인딩 연산자 분류 (pt_is_op_hv_late_bind), expected-domain 계산, 후기 바인딩 시 expected-domain 클리어 (XASL 생성기에DB_TYPE_VARIABLE신호), 그리고 추론된 형의 expected-domain 으로의 역전파. 자료에서는 암시적이지만 코드에서는 명시적인 다섯 단계다. -
NATURAL JOIN 의 derived-table / CTE 속성 조회. 자료는 두 케이스 (entity 기반
DB_ATTRIBUTE, derived table 또는 CTE 의 select-list / WITH-list) 를 적는다.pt_resolve_natural_join_internal에서 검증되며, 현재 소스는 자료와 일치한다.
미해결 질문
섹션 제목: “미해결 질문”-
pt_eval_function_type_new마이그레이션 로드맵. 세 함수 패밀리가 C++17 경로에 들어가 있고 나머지는 여전히pt_eval_function_type_old를 쓴다. 더 많은 함수를 옮기는 문서 화된 계획이 있는가? 기존 집계 함수를 이동시키려면 무엇이 필요한가? 추적 경로:func_type.cpp의git log와pt_eval_function_type_new호출지에서 새 함수 패밀리를 도입한 머지 커밋들을 본다. -
donot_fold의미론.SEMANTIC_CHK_INFO::donot_fold는pt_check_with_info의 일부 호출자(뷰 변환 포함) 가 켠다. 원본 자료가 모든 호출지를 나열하지는 않는다. 접기 억제 문맥의 완전한 목록이 있는가? 그중 어느 것이 plan 캐시 정합성에 영향을 미치는가? 추적 경로: 파서 전반에서donot_fold = true를 grep. -
PT_EXPR_INFO_CNF_DONE와 재진입.pt_do_cnf는pt_cnf적용 전에 플래그를 클리어한다. 만약 술어가 부분적으로 CNF 태그된 서브트리를 (뷰 변환에서 흘러 들어와) 이미 들고 있다면? 현재 코드는 최상위 conjunct 의 플래그만 무조건 클리어하므로, 서브 conjunct 의 플래그는 살아 있을 수 있다.pt_cnf가 재-CNF 해야 할 서브트리를 건너뛸 수 있는가? 추적 경로: CNF 태그된 술어를 가진 뷰를 만들고, 그 뷰를 다른 질의와 UNION 하여 CNF 가 재진입하는 회귀 케이스를 작성. -
pt_tag_terms_with_specs와 NATURAL JOIN 후조건. 자연 조인 재작성은 새PT_EXPR술어를 합성하여 우측 spec 의on_cond에 매단다. 그 합성 술어의 spec-id 비트셋은 좌·우의 합집합이어야 옳다. 실제로 그렇게 만들어지는가? 추적 경로:pt_tag_terms_with_specs를 계측하고 자연 조인 회귀 스위트를 돌린다. -
정적 SQL 파싱과 접기.
pt_semantic_type의is_parsing_static_sql가드는 접기 패스를 통째로 건너뛴다. 결과적으로 정적 SQL 안의 상수 술어는 실행 시점까지 파스 트리에 살아 있다. 실행기는 이 상태를 견디는가 — 즉 lazy 접기가 있는가, 아니면 늘 평가만 하는가? 추적 경로:query/query_executor.c에서pt_evaluate_db_value_expr호출지를 추적. -
재귀식의 깊이 제한.
eval_recursive_expr_type은 사슬의 끝까지 내려간다. 매우 긴 사슬 (수백 개의CASE WHEN ... CASE WHEN ...) 은 스택을 터뜨릴 수 있다. 깊이 가드가 있는가? 추적 경로:type_checking.c에서MAX_RECURSIVE_DEPTH또는 동등한 상수를 grep. -
MVCC 의 5비트
OR_MVCC_FLAG_MASK같은 자리가 있는가? MVCC 레코드 헤더에는 예약 비트가 있다. 파스 트리 노드 구조 에도 자체 플래그 바이트(PT_NODE::flag,PT_EXPR_INFO_*,PT_NAME_INFO_*) 가 있다. 그 비트 중 일부가 향후 의미 검사 기능을 위해 예약되어 있는가, 아니면 빈자리가 거의 없는가? 추적 경로:PT_*_INFO_와PT_NODE_FLAG_*마스크를 모두 열거하여 겹침이 없는지 확인.
원본 분석 (raw/code-analysis/cubrid/query-processing/)
섹션 제목: “원본 분석 (raw/code-analysis/cubrid/query-processing/)”code_analysis_Semantic_check_Overview_v_1_0.pdf— 4단계 파이프 라인 개관.code_analysis_Semantic_check-Name_Resolution_v_0_9.pdf— 가장 긴 자료(변환 후 마크다운 26 KB). spec 펼치기, 스코프 스택,*해소, derived table, 부속질의, 조인(Oracle(+)포함), GROUP BY / HAVING 별칭, NATURAL JOIN 재작성, FOR UPDATE 플래깅 망라.code_analysis_Semantic_check-Type_Checking_and_Constant_Folding_v_1_0.pdf—pt_semantic_type과 형 추론 규칙.code_analysis_Semantic_check-Checking_Semantic_of_Particular_Statement_v_0_8.pdf— 문 종별 재작성과 오류 메시지.type_checking_v1.0.pptx— 형 검사 패스의 깊이 있는 동반 자료 (시그니처 표, 형 강제 변환표, 파라미터화된 형,PT_DATA_TYPE↔TP_DOMAIN관계).code_analysis_Common_module_CNF_(parsercnf).pdf— 의미 검사와 뷰 변환이 공유하는 유틸리티 모듈로서의 CNF.
형제 문서
섹션 제목: “형제 문서”knowledge/code-analysis/cubrid/cubrid-mvcc.md— 같은 질의 처리 파이프라인의 또 다른 단면. 여기서 만든 CNF 술어가 옵티마이저로 넘어가고, 옵티마이저가 짠 계획이 MVCC 인식 스캔으로 페이지를 뽑아낸다.knowledge/code-analysis/cubrid/cubrid-heartbeat.md— 본 문서의 포맷 참고용.
교재 챕터·참고 문헌
섹션 제목: “교재 챕터·참고 문헌”- Database Internals (Petrov), 5장 §Query Processing — 파서 → 옵티마이저 파이프라인의 무대.
- Database System Concepts (Silberschatz, Korth, Sudarshan), 4장 §Built-in Data Types, 5장 §Functions and Procedures — §“학술적 배경” 의 형 시스템 프레이밍의 출처.
- Compilers: Principles, Techniques, and Tools (Aho, Lam, Sethi, Ullman; “Dragon Book), 6장 §Symbol Tables, 9장 §Loop-Invariant Computations” — 심볼 테이블과 상수 접기 프레이밍의 출처.
- Robinson, A Machine-Oriented Logic Based on the Resolution Principle, JACM 12, 1965 — 형식 CNF 변환.
- Nilsson, Principles of Artificial Intelligence, “Resolution Refutations” 장 — CUBRID CNF 자료가 직접 인용.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/parser/compile.c—pt_compile,pt_class_pre_fetch.src/parser/semantic_check.c— 문 종별 드라이버와 모든 문 종별 검사.src/parser/semantic_check.h—SEMANTIC_CHK_INFO와 모듈의 공개 API.src/parser/name_resolution.c— 모든 이름 해석 sub-pass.src/parser/type_checking.c—pt_semantic_type과 형 추론 규칙.src/parser/cnf.c— CNF 정규화.src/parser/parse_tree.h—PT_NODE정의. 모든 노드 종별의info.<kind>페이로드 union.src/parser/parse_dbi.c—pt_data_type_to_db_domain와PT_DATA_TYPE↔TP_DOMAIN다리.src/parser/parse_evaluate.c—pt_evaluate_db_value_expr. 상수 접기 함수가 사용하는 컴파일 시점 평가기.src/parser/func_type.cpp,func_type.hpp—pt_eval_function_type_new가 사용하는 C++17 함수 형 평가 기계.src/object/object_domain.{c,h}—TP_DOMAIN과 도메인 캐시.