콘텐츠로 이동

(KO) CUBRID DBI와 CCI — 클라이언트 API 표면, 구문 수명 주기, 와이어 드라이버 파사드

목차

관계형 엔진에 이르는 경로는 세 겹의 동심원 API로 이루어진다. DBMS 클라이언트 드라이버 코드에서 혼란이 생기는 가장 흔한 원인은 이 세 겹을 하나로 뭉뚱그려 보는 데 있다. 가장 바깥 층은 언어 바인딩이다. JDBC, Python DB-API, ODBC, REST 프론트엔드가 여기 속한다. 중간 층은 호출 수준 인터페이스(CLI)다. X/Open이 정의한 의미에서, 모든 연산이 함수 호출(SQLPrepare, SQLBindParameter, SQLExecute, SQLFetch, SQLFreeStmt)로 표현되고 불투명 핸들을 소유하며 상태 코드를 반환하는 절차형 C 표면이다. 가장 안쪽 층은 내장 SQL 층이다. Silberschatz 외 Database System Concepts (5장 §Embedded SQL)가 설명하듯이, 호스트 언어 전처리기가 EXEC SQL SELECT ...를 일련의 CLI 호출로 변환하는 방식이다. 오라클 OCI, PostgreSQL libpq, MySQL libmysqlclient, SQLite 등 모든 현대 엔진이 CLI 층을 구현하고 그 위에 언어 바인딩을 얹는 구조를 택한다.

그 위에 세 가지 독립적인 설계 결정이 결과 C API의 모양을 거의 결정한다.

  1. 세션을 핸들로 삼을 것인가, 구문을 핸들로 삼을 것인가. ODBC는 HENV, HDBC, HSTMT를 별도로 둔다. PostgreSQL의 libpq는 연결과 구문을 합쳐서, PQexec 호출 하나가 PGconn에 대한 일회성 텍스트 실행이 된다. Oracle OCI는 OCIStmt를 독립적인 구문 객체로 주고 나중에 서비스 컨텍스트와 연결한다. CUBRID는 ODBC에 가장 가깝다. DB_SESSION * 이 구문 핸들이고, 연결은 db_restart가 채우는 모듈 스코프 전역 변수(db_Connect_status, db_Database_name)에 암묵적으로 산다. 한 프로세스에 DB_SESSION 객체가 여러 개 있을 수 있지만, 한 번에 하나의 데이터베이스에만 연결된다는 점이다.

  2. 컴파일 후 실행할 것인가, 준비와 실행을 합칠 것인가. CLI는 일회성 텍스트 경로(PQexec, mysql_query), 준비/실행 분리 형태 (SQLPrepare/SQLExecute), 또는 둘 다를 제공할 수 있다. CUBRID는 내부적으로 분리 형태를 택했다. db_compile_statement_localdb_execute_statement_local이 별개의 호출이며, 합쳐진 형태는 래퍼 (db_open_buffer_and_compile_first_statement + 실행 루프 in db_compile_and_execute_queries_internal)로 노출된다. 이 분리 덕분에 JDBC 클라이언트가 PreparedStatement를 사용할 때 브로커의 CAS 층이 컴파일된 DB_SESSION 하나를 여러 ux_execute 호출에 걸쳐 재사용할 수 있다.

  3. 커서를 반복자로 볼 것인가, 임의 접근 뷰로 볼 것인가. 결과 집합 API는 앞 방향 전용(cursor_next_tuple, 스트림 끝) 이거나, seek_tuple, prev_tuple, last_tuple을 갖춘 임의 접근 방식이다. CUBRID는 임의 접근을 선택했다. 서버 측 QFILE 목록 파일이 이를 뒷받침한다. T_SELECT 타입의 DB_QUERY_RESULT마다 CURSOR_IDQFILE_LIST_ID를 갖고 ODBC 스타일의 스크롤 가능한 커서 표면 전체를 지원한다.

이 세 가지를 명시하고 나면, CUBRID DBI의 나머지는 (세션=구문 핸들, 컴파일/실행 분리, 스크롤 가능 커서) 모서리를 선택했을 때 자연스럽게 따라 나오는 결과로 읽힌다. 브로커의 CCI 드라이버는 첫 번째 결정을 뒤집는다. CAS별 핸들(T_SRV_HANDLE)을 외부로 노출해 JDBC 클라이언트 가 연결 하나에 여러 구문 핸들을 쥘 수 있게 한다. 하지만 나머지 두 결정은 그대로 유지한다. 아래에 놓인 실행기가 동일한 db_* 코어이기 때문이다.

교과서적 CLI 층 아래로 내려오면, 모든 주요 클라이언트/서버 DBMS가 비슷한 한 줌의 패턴을 들고 있다. 이 패턴들은 X/Open 명세에 나오는 것이 아니라, 추상 API와 실제 소스 사이의 빈자리를 메우는 엔지니어링 어휘다.

파서 컨텍스트를 겸하는 구문 객체

섹션 제목: “파서 컨텍스트를 겸하는 구문 객체”

모든 CLI는 준비와 실행 사이에 파싱된 AST, 호스트 변수 배열, 컬럼 타입 목록, 서버 측 계획 핸들을 어딘가에 보관해야 한다. CUBRID의 DB_SESSIONparser(PARSER_CONTEXT), statements(PT_NODE ** 파스 트리 배열), type_list(구문별 컬럼 설명), stage(구문별 1바이트 상태 기계 배열)를 함께 담는다. ;으로 구분된 여러 구문을 DB_SESSION 하나가 처리할 수 있다. parser_parse_string_with_escapes가 배열을 반환하고 dimension이 개수를 기록하기 때문이다. Oracle의 OCIStmt가 가장 가까운 유사물이다. PostgreSQL의 PGresult는 순수하게 결과만 담으며 준비된 구문은 서버에 산다.

구문당 1바이트를 쓰는 네 단계 상태 기계

섹션 제목: “구문당 1바이트를 쓰는 네 단계 상태 기계”

CLI는 이중 실행, 이중 컴파일, 컴파일 전 실행을 막아야 한다. db_vdb.c는 이를 구문당 1바이트로 강제한다. 그 바이트는 StatementInitialStage(0), StatementCompiledStage, StatementPreparedStage, StatementExecutedStage 값을 갖는다. db_compile_statement_local은 초기 → 컴파일됨 → 준비됨으로 전진시킨다. 준비됨 단계는 do_prepare_statement가 XASL 캐시에서 statement->xasl_id 를 채웠다는 뜻이다. db_execute_and_keep_statement_local은 준비됨 → 실행됨으로 전진시킨다. 이 바이트는 모든 진입점에서 확인된다. 예를 들어, session->stage[stmt_ndx] < StatementPreparedStage이면 실행기가 컴파일러 를 다시 돌린다. CUBRID가 준비됨 단계를 별도 API 호출로 노출하지는 않지만 상태 기계는 내부에서 작동한다.

타입 태그를 가진 별도의 결과 집합 객체

섹션 제목: “타입 태그를 가진 별도의 결과 집합 객체”

구문이 실행되고 나면, 컬럼 목록과 커서가 구문 핸들(DB_SESSION)을 닫기 전에 결과를 다 소비하지 않을 수 있기 때문에 구문 밖 어딘가에 살아야 한다. CUBRID의 결과 객체인 DB_QUERY_RESULT는 컬럼 타입 목록과 커서를 모두 담는다. 타입 태그(T_SELECT / T_CALL / T_OBJFETCH / T_GET / T_CACHE_HIT)가 모든 커서 함수에서 어느 분기가 일할지를 결정한다. PostgreSQL의 PGresult와 MySQL의 MYSQL_RES도 같은 패턴을 따른다.

비내장 클라이언트를 위한 와이어 드라이버 파사드

섹션 제목: “비내장 클라이언트를 위한 와이어 드라이버 파사드”

위의 DBI 표면은 내장형이다. CUBRID 클라이언트 라이브러리를 직접 실행하는 프로세스에 링크된다. JDBC, ODBC, Python, PHP는 모두 다른 프로세스에서 살며, 브로커 데몬에 말을 건다. 브로커의 CAS 워커는 CUBRID 클라이언트 라이브러리를 내장하고 와이어 호출을 다시 db_* 호출로 변환한다. 브로커는 이 층을 CCI(CUBRID Call Interface)라고 부르며, 서버 측은 src/broker/cas_execute.c에 일련의 ux_* 함수 (ux_database_connect, ux_prepare, ux_execute, ux_fetch, ux_end_tran)로 산다. 모양은 db_*를 닮았지만 CCI 고유의 관심사가 추가된다. 서버 핸들 ID(T_SRV_HANDLE), CCI 플래그 (CCI_PREPARE_UPDATABLE, CCI_PREPARE_HOLDABLE, CCI_EXEC_QUERY_INFO), 브로커 로그 스트림, 그리고 같은 호출 안에서 db_commit_transaction을 유발하는 자동 커밋 힌트가 그것이다.

스키마 조작과 값 기본 연산은 절차형으로 유지

섹션 제목: “스키마 조작과 값 기본 연산은 절차형으로 유지”

스키마 정의(db_create_class, db_add_attribute, db_add_method)와 값 구성(db_make_int, db_make_string, db_make_object)은 X/Open CLI 명세에 없지만, OO 성향 RDBMS의 모든 C 클라이언트 API에 존재한다. CUBRID는 OODB 뿌리에서 무거운 스키마 표면을 물려받았다. MOP(메모리 객체 포인터)가 보편적인 클래스 또는 인스턴스 핸들이며, db_create / db_put / db_get은 SQL을 거치지 않고 인스턴스에 직접 작용한다.

CUBRID의 클라이언트 API는 계층화된 시스템이며 규칙은 하나다. 위 계층은 항상 아래 계층에게 말하며, 건너뛰는 경로는 없다. 위에서부터 보면: (1) 언어 바인딩 — JDBC(cubrid-jdbc/), CCI 네이티브 C 드라이버 (cubrid-cci/, 별도 저장소), Python, PHP, Perl DBD, CSQL (src/executables/csql.c). (2) CCI 와이어 — cas_protocol.hCAS_FC_* 플랫 오피코드 공간이 broker/cas.cserver_fn_table으로 디스패치된다. (3) CAS 측 ux_*broker/cas_execute.c의 브로커 측 파사드. 각 함수는 db_*를 호출하고 T_NET_BUF로 직렬화 해 돌려보내는 얇은 래퍼다. (4) DBI db_*src/compat/의 공개 C 클라이언트 API. 모든 함수는 순수하게 클라이언트 측에서 실행되거나 network_cl으로 NRP 요청을 발행한다. (5) network_cl — 대칭 와이어 마샬링 층(cubrid-network-protocol.md 참조). (6) boot_cl — 연결 수명 주기 층. db_restartboot_restart_client에 위임한다.

flowchart TD
    JDBC[JDBC / Python / ODBC 클라이언트]
    CCI_drv[CCI 네이티브 드라이버<br/>cubrid-cci/]
    CSQL[CSQL 명령행<br/>src/executables/csql.c]
    Broker[브로커 데몬<br/>src/broker/broker.c]
    CAS[CAS 워커 프로세스<br/>src/broker/cas.c]
    UX[ux_prepare / ux_execute / ux_fetch<br/>src/broker/cas_execute.c]
    DBI[db_compile_statement / db_execute_statement<br/>src/compat/db_vdb.c]
    Boot[boot_restart_client<br/>src/transaction/boot_cl.c]
    NET[network_cl<br/>src/communication/network_cl.c]
    Server[cub_server<br/>NET_SERVER_QM_QUERY_PREPARE]

    JDBC -->|CCI 와이어| Broker
    CCI_drv -->|CCI 와이어| Broker
    Broker --> CAS
    CAS --> UX
    UX --> DBI
    CSQL --> DBI
    DBI --> Boot
    DBI --> NET
    Boot --> NET
    NET --> Server

이 그림이 문서 전체에서 핵심이다. 모든 클라이언트 경로는 결국 src/compat/db_*에 닿는다. 브로커가 존재하는 이유는 CUBRID 서버가 연결당 단일 스레드이고, 무거운 연결 작업 — boot_restart_client가 수행하는 로케일 초기화, 시간대 로드, 파라미터 동기화, 호스트 장애 조치, 스키마 버전 협상 — 을 CAS 프로세스에서 풀링하는 것이 수천 개의 단기 JDBC 연결을 허용 가능한 지연으로 처리하는 유일한 방법이기 때문이다.

DBI의 연결 수명 주기는 의도적으로 단순하다. 연결 핸들이 없다. 연결은 db_Connect_statusdb_Database_name에 기록되는 프로세스 전역 상태 기계다. 모든 db_* 함수는 CHECK_CONNECT_* 매크로로 이를 확인하고 프로세스가 연결되지 않은 상태라면 일찍 반환한다.

db_loginau_login에 전적으로 위임하고 자격 증명을 인증 모듈에 기록하는 한 줄짜리 함수다. 네트워크에는 손대지 않는다. 두 단계가 분리되어 있는 이유는, CUBRID 유틸리티가 독립 실행 가능하고 db_login을 먼저 호출한 뒤 db_init(생성) 또는 db_restart(열기)를 호출하기 때문이다.

db_restart는 모듈 스코프 변수(db_Client_type, db_Preferred_hosts, db_Connect_order, db_Client_ip_addr)로부터 BOOT_CLIENT_CREDENTIAL을 채우고 boot_restart_client를 호출한다. 성공하면 db_Connect_status = DB_CONNECTION_STATUS_CONNECTED로 설정하고, volumedb_Database_name에 복사하며, install_static_methods를 실행하고, SIGFPE 핸들러를 설치한다. 자격 증명은 객체가 아닌 평탄한 구조체다. 호출자는 그 핸들을 받지 못한다. 무거운 작업은 boot_restart_client에서 이루어진다(cubrid-boot.md 참조).

db_restart_exau_login, db_set_client_type, db_restart를 하나로 합쳐서 대부분의 현대 코드가 쓰는 형태다.

// db_restart_ex — db_admin.c
db_restart_ex (const char *program, const char *db_name,
const char *db_user, const char *db_password,
const char *preferred_hosts, int client_type)
{
retval = au_login (db_user, db_password, false);
if (retval != NO_ERROR) return retval;
db_set_client_type (client_type);
if (preferred_hosts != NULL) db_set_preferred_hosts (preferred_hosts);
return db_restart (program, false, db_name);
}

이 함수는 JDBC 클라이언트가 처음 요청할 때 브로커의 CAS 워커가 ux_database_connect 안에서 호출하는 함수다. client_type은 중요하다. 이 연결이 일반 브로커인지, 읽기 전용 브로커인지, 슬레이브 전용 브로커인지, 독립 실행 CSQL인지, 관리자 유틸리티인지 서버에 알려서, 연결이 어떤 HA 역할, 잠금 모드, 보조에서 읽기 기능을 상속할지 결정하기 때문이다.

db_shutdown은 세션을 끝내고 연결을 끊는다. db_end_session을 호출해 서버 측 SESSION_STATE를 비우고(준비된 구문과 SET 변수 바인딩이 서버에 누출되지 않도록 — cubrid-server-session.md 참조), 그 다음 boot_shutdown_client (true)를 호출하며, 연결 상태 전역 변수를 지우고, SIGFPE 핸들러를 복원하고, 캐시된 실행 계획을 해제한다.

구문 컴파일: db_open_bufferdb_compile_statement

섹션 제목: “구문 컴파일: db_open_buffer → db_compile_statement”

컴파일은 두 API 호출로 나뉜다. db_open_buffer는 파서 컨텍스트를 설정하고 파싱하며, db_compile_statement는 의미 검사, 뷰 변환, XASL 준비를 진행한다. 둘을 이어주는 세션 구조체는 작다.

// db_session struct — db_session.h
struct db_session
{
char *stage; /* per-statement FSM state */
char include_oid; /* NO_OIDS, ROW_OIDS */
int dimension; /* number of statements */
int stmt_ndx; /* next statement to compile */
int line_offset;
int column_offset;
PARSER_CONTEXT *parser; /* parser context */
DB_QUERY_TYPE **type_list; /* nice column headings, per stmt */
PT_NODE **statements; /* parse trees, per stmt */
bool is_subsession_for_prepared;
DB_SESSION *next; /* sub-sessions for prepared stmts */
};

db_open_buffer는 파싱만 한다. 클래스를 잠그지 않고, 서버를 호출하지 않으며, stmt_id도 생성하지 않는다. db_open_local을 호출해 세션과 새 PARSER_CONTEXT를 할당한 뒤, parser_parse_string_with_escapes로 버퍼 위에서 Bison/Flex를 돌린다. 성공하면 initialize_sessiondimension = count(statements)를 기록하고 반환한다. db_open_filedb_open_file_name 변형은 FILE *을 받는다.

db_compile_statement에서 실제 작업이 이루어진다. cubrid-query-rewrite.mdcubrid-semantic-check.md에서 참조되는 함수 이름이 바로 이것이며, 모든 CUBRID 클라이언트 SQL 요청은 서버로 가기 전에 이 함수를 통과한다.

// db_compile_statement — db_vdb.c
int
db_compile_statement (DB_SESSION * session)
{
int statement_id;
er_clear ();
CHECK_CONNECT_MINUSONE ();
statement_id = db_compile_statement_local (session);
return statement_id;
}

db_compile_statement_localstmt_ndx 인덱스의 구문 하나를 클라이언트 측 SQL 파이프라인 전체를 실행한다. 그 단계들은 각각의 분석 파일에서 다뤄진 모듈과 일치한다.

sequenceDiagram
    participant Caller
    participant DB as db_compile_statement_local
    participant Parser as PARSER (PT_*)
    participant DBlink as pt_rewrite_for_dblink
    participant Cls as pt_class_pre_fetch
    participant Sem as pt_compile (semantic)
    participant View as mq_translate
    participant Plan as do_prepare_statement
    participant Server as cub_server

    Caller->>DB: db_compile_statement(session)
    DB->>Parser: pt_reset_error
    DB->>DBlink: pt_rewrite_for_dblink(parser, statement)
    DB->>Parser: pt_get_titles (SELECT용 qtype)
    DB->>Cls: pt_class_pre_fetch (클래스 잠금)
    Cls->>Server: locator_fetch_class (NRP)
    DB->>Sem: pt_compile(parser, statement)
    DB->>View: mq_translate(parser, statement)
    DB->>Cls: pt_class_pre_fetch 재실행 (mq 이후)
    DB->>Plan: do_prepare_statement(parser, statement)
    Plan->>Server: NET_SERVER_QM_QUERY_PREPARE
    Server-->>Plan: XASL_ID
    Plan-->>DB: NO_ERROR
    DB->>DB: stage[ndx] = StatementPreparedStage
    DB-->>Caller: stmt_id (1부터 시작)

핵심 규칙은 컴파일이 서버를 두 번 건드린다는 점이다. 한 번은 pt_class_pre_fetch에서(실행 시점의 교착 상태를 피하기 위해 참조된 클래스를 잠금), 한 번은 do_prepare_statement에서(XASL 스트림을 서버의 XASL 캐시로 보내고 XASL_ID를 돌려받음). 나머지 단계는 모두 순수하게 클라이언트 측이다. 이 때문에 db_compile_statement가 적지 않은 비용을 치르며, JDBC PreparedStatement를 사용할 때 CUBRID 브로커가 결과 DB_SESSION을 여러 ux_execute 호출에 걸쳐 살려 두는 이유이기도 하다.

함수는 성공 시 ODBC 관례대로 1부터 시작하는 구문 번호를 반환하고, 세션에 컴파일할 구문이 더 없으면 0을 반환한다. 음수는 오류 코드다. 여러 구문을 담은 입력(SELECT ... ; UPDATE ... ; INSERT ... ;)은 호출마다 구문 하나씩 컴파일하므로, 호출자가 컴파일과 실행을 교차할 수 있다. stage[] 바이트는 호출 끝에서 컴파일됨 → 준비됨으로 전진한다.

db_open_buffer_and_compile_first_statement는 일회성 SQL을 위한 편의 래퍼다. CSQL이 유틸리티 명령에 쓰는 경로가 여기다.

// db_open_buffer_and_compile_first_statement — db_vdb.c, condensed
int
db_open_buffer_and_compile_first_statement (const char *CSQL_query,
DB_QUERY_ERROR *query_error,
int include_oid,
DB_SESSION **session,
int *stmt_no)
{
CHECK_CONNECT_ERROR ();
*session = db_open_buffer_local (CSQL_query);
if (*session == NULL) return er_errid ();
db_include_oid (*session, include_oid);
*stmt_no = db_compile_statement_local (*session);
errs = db_get_errors (*session);
if (errs != NULL) {
int line, col;
(void) db_get_next_error (errs, &line, &col);
error = er_errid ();
if (query_error) {
query_error->err_lineno = line;
query_error->err_posno = col;
}
}
if (*stmt_no < 0 || error < 0) {
db_close_session_local (*session);
*session = NULL;
return er_errid ();
}
return error;
}

바인드와 실행: db_push_values, db_execute_statement

섹션 제목: “바인드와 실행: db_push_values, db_execute_statement”

호스트 변수는 실행 전에 파서의 host_variables 배열로 밀어 넣는다. 파서는 파싱 중에 ? 마커를 세어 필요한 슬롯 수(parser->host_var_count) 를 이미 알고 있으므로, API 계약은 정확히 이 크기의 배열을 줘다. db_push_valuespt_set_host_variables를 호출해 DB_VALUE 배열을 복사하고 파서에 기존 의미 오류가 있으면 pt_report_to_ersys로 보고한다. pt_set_host_variables는 타입 강제 변환을 하지 않는다는 점이다. 강제 변환은 의미 검사에서 파서가 기대 타입을 추론한 후에 db_execute_and_keep_statement_local 안의 do_cast_host_variables_to_expected_domain이 수행한다.

db_execute_statement는 공개 진입점이며, 호출 후 구문의 파스 트리를 해제한다는 점에서 db_execute_and_keep_statement와 다르다. 따라서 이 형태로는 구문을 재실행할 수 없다. and-keep 형태는 반복 사용을 위해 파스 트리를 보존한다. 두 래퍼 모두 세 가지를 한다. 연결 상태 확인, db_invalidate_mvcc_snapshot_before_statement 호출, 그리고 대응하는 _local 워커 호출이다. 반환할 때는 db_set_read_fetch_instance_version (LC_FETCH_MVCC_VERSION)을 설정한다.

둘 다 결국 db_execute_and_keep_statement_local을 실행한다. 이 함수가 실행의 핵심이다. 호스트 변수 배열이 채워졌는지(host_var_count > 0일 때 set_host_var == 1), 구문이 최소 준비됨 단계인지(아니면 자동으로 db_compile_statement_local을 다시 실행)를 검증한 뒤 다음 순서를 밟는다.

  1. 구문이 SYSTIMESTAMP나 로컬 트랜잭션 ID를 참조하면 qp_get_server_info로 서버 시각 / 트랜잭션 ID 값을 가져온다.
  2. do_execute_subquery_pre로 캐시된 CTE 하위 쿼리를 미리 실행한다.
  3. SQL PREPARE / EXECUTE / DEALLOCATE PREPARE 계열을 위해 do_process_prepare_statement, do_recompile_and_execute_prepared_statement, do_process_deallocate_prepare로 분기한다.
  4. do_execute_statement(XASL 캐시 빠른 경로)를 호출하거나, 캐시가 비활성화되었거나 구문이 캐시 불가라면 파스 트리를 복사하고 pt_bind_values_to_hostvars / pt_resolve_names / pt_semantic_type을 실행한 뒤 do_statement를 호출한다.
  5. stage[stmt_ndx] = StatementExecutedStage로 설정한다.
  6. DB_QUERY_RESULT 기술자를 구성한다. CUBRID_STMT_SELECTCUBRID_STMT_EXECUTE_PREPAREpt_new_query_result_descriptor를 호출해 서버 측 목록 파일을 커서로 감싼다. CUBRID_STMT_INSERT / CUBRID_STMT_CALL은 AST의 pt_node_etc 페이로드에서 행 수 또는 생성된 키를 꺼낸다.
  7. update_execution_values로 파서의 row_count 캐시를 갱신한다.

이 루틴에서 놓치기 쉽지만 중요한 세 가지 점이 있다.

MVCC 스냅샷 무효화는 구문 경계가 아닌 API 경계에서 일어난다. db_execute_statement_local에 위임하기 전에 db_invalidate_mvcc_snapshot_before_statement를 호출하고, 돌아올 때 LC_FETCH_MVCC_VERSION을 재설정한다. 이것이 READ COMMITTED 격리 수준을 동작하게 만드는 장치다. 최상위 API 실행 호출마다 새 스냅샷을 얻는다. 그러나 래퍼 없이 브로커 내부에서 db_execute_statement_local을 직접 호출하면, 낡은 스냅샷을 조용히 관찰하게 된다는 점이다.

XASL 캐시는 실행 중에 밑에서 실패할 수 있다. 서버가 캐시된 계획을 퇴출하거나 실행 중 스키마 변경을 감지하면 실행기는 ER_QPROC_INVALID_XASLNODE 또는 ER_QPROC_XASLNODE_RECOMPILE_REQUESTED를 반환한다. DBI는 구문의 xasl_id를 지우고 준비-실행 쌍을 다시 조용히 실행한다. 성공 시에는 호출자에게 보이지 않지만, 스키마 변경이 자동 복구되지 않으면 (DB_CLASS_MODIFIED) 오류가 호출자에게 전달된다.

INSERT 행 수는 REPLACE를 특별 처리가 있다. REPLACE 구문은 개념적으로 DELETE + INSERT다. 실행기가 insert 수를 반환하면, DBI는 그것을 pt_node_etc(statement)의 행 수(실행기가 AST 노드에 보관하는 값)와 비교하고 더 큰 값을 유지할 수 있다.

커서: db_query_first_tuple, db_query_next_tuple, db_query_get_tuple_value

섹션 제목: “커서: db_query_first_tuple, db_query_next_tuple, db_query_get_tuple_value”

DB_QUERY_RESULT는 태그가 달린 공용체다. result->typeT_SELECT, T_CALL, T_OBJFETCH, T_GET, T_CACHE_HIT 중 하나다. result->res 공용체는 태그마다 한 팔을 갖는다. T_SELECT(실제 목록 파일 커서를 가진 유일한 타입)는 res.s{QUERY_ID query_id; CURSOR_ID cursor_id; CACHE_TIME cache_time; int stmt_id;}를 담는다. T_CALLres.c{DB_VALUE *val_ptr; CURSOR_POSITION crs_pos;}를 담는다. T_OBJFETCHres.o{DB_VALUE **valptr_list; CURSOR_POSITION crs_pos;}를 담는다.

커서 함수들은 외부에서 보면 균일하지만 내부에서는 타입 태그에 따라 분기한다. db_query_first_tuple, db_query_next_tuple, db_query_prev_tuple, db_query_last_tuple, db_query_seek_tuple은 모두 같은 모양을 공유한다. 검증하고, result->type으로 분기해, T_SELECTcursor_*(목록 파일 기반 커서)로, T_CALL / T_OBJFETCHcrs_pos ∈ { C_BEFORE, C_ON, C_AFTER }의 작은 상태 기계를 이동한다. db_query_seek_tuple이 흥미롭다. 처음, 현재, 끝으로부터 각각 세 가지 상대 오프셋을 계산하고 절댓값이 가장 작은 것을 골라 cursor_first_tuple / cursor_last_tuple을 호출하고 스텝한다.

stateDiagram-v2
    [*] --> Initial: db_open_buffer
    Initial --> Compiled: db_compile_statement_local\n(pt_compile, mq_translate)
    Compiled --> Prepared: do_prepare_statement\n(서버 XASL 캐시 미스 => 채움)
    Prepared --> Executed: db_execute_and_keep_statement_local\n(do_execute_statement)
    Executed --> Prepared: 재실행\n(구문 유지)
    Executed --> Initial: db_drop_statement
    Prepared --> Initial: db_drop_statement
    Compiled --> Initial: db_drop_statement
    Initial --> [*]: db_close_session
    Executed --> [*]: db_close_session

위치가 결정되면 컬럼 인덱스(db_query_get_tuple_value)나 컬럼 이름 (db_query_get_tuple_value_by_name)으로 값을 추출한다. T_SELECTcursor_get_tuple_value를 호출해 목록 파일 버퍼에서 가져온다. T_OBJFETCHvalptr_list[index]를 읽는다. T_CALL은 단일 val_ptr을 읽는다. 모든 경우에서 값은 pr_clone_value로 호출자의 DB_VALUE에 복사된다.

pr_clone_value는 값을 호출자의 DB_VALUE에 깊은 복사(deep copy)로 담는다. 호출자는 결과를 소유하므로 문자열/객체/집합 저장소를 해제하기 위해 db_value_clear(또는 힙 할당 값에는 db_value_free)를 호출해야 한다. API 계약의 이 측면은 새 사용자가 자주 놓치는 부분이다. 문자열 값을 지우지 않으면 메모리 누수가 발생하며, API는 경고하지 않지만 valgrind는 잡아낸다.

db_query_get_tuple_value_by_nameresult->query_type을 선형으로 탐색해 컬럼 이름을 찾은 뒤(대소문자 구분 없음) db_query_get_tuple_value 에 위임하는 편의 래퍼다. 컬럼 별칭(name)과 원래 이름(original_name) 을 모두 시도하므로, SELECT ... AS x 형태의 쿼리는 별칭과 원래 표현식 이름 양쪽으로 접근 가능하다.

db_query_enddb_execute_statement가 생성한 결과의 짝 파괴자다. tran_was_latest_query_ended를 확인해 서버에 알림을 보낼지 결정한 뒤 db_query_end_internal에 위임한다. 이 함수는 (a) status == T_CLOSED를 멱등성을 갖고, (b) T_SELECT 결과를 qmgr_end_query (result->res.s.query_id)를 호출해 서버가 목록 파일을 해제할 수 있게 하는 NRP를 보내며(트랜잭션이 이미 끝난 경우 생략), (c) cursor_close를 호출해 클라이언트 측 커서 핸들을 해제하고, (d) db_free_query_resultDB_QUERY_RESULT 자체를 해제한다.

스키마 조작: db_create_class, db_add_attribute

섹션 제목: “스키마 조작: db_create_class, db_add_attribute”

db_class.c의 스키마 표면은 작고 절차형이다. db_create_class (name)smt_def_classSM_TEMPLATE을 구성하고, locator_reserve_class_name 으로 이름을 예약하며, sm_update_class로 커밋한다. 반환된 MOP가 새 클래스 객체다. SQL DDL이 동작하는 방향과 반대다. SQL 경로는 파서 → 의미 → do_alter/do_create_entity → 스키마 템플릿 관리자로 흐른다. 직접 API는 SQL 파서를 건너뛰고 같은 SM_TEMPLATE을 직접 구성한다. 두 경로 모두 sm_update_class를 공유하므로 스키마 의미에서 어긋날 수 없다.

db_add_attribute (obj, name, domain, default) 속성 추가는 db_add_attribute_internal에 위임한다. 이 함수는 smt_edit_class_mop로 템플릿을 열고, smt_add_attribute_w_dflt로 기본값과 함께 새 컬럼을 기록하며, sm_update_class로 커밋한다. "INTEGERVARCHAR(64)" 같은 도메인 문자열은 SQL 파서가 아닌 스키마 템플릿 모듈이 파싱한다. 따라서 프로그래밍 방식으로 컬럼을 추가할 때 전체 Bison 비용을 피할 수 있다. db_add_shared_attributedb_add_class_attribute 변형은 CUBRID OODB 뿌리에서 내려온 공유 속성과 클래스 수준 속성을 위한 것이다.

트랜잭션: db_commit_transaction, db_abort_transaction

섹션 제목: “트랜잭션: db_commit_transaction, db_abort_transaction”

트랜잭션 API는 표면에서 가장 작은 부분이다. db_commit_transactiontran_commit (false)를 호출한다. false는 API가 노출하지 않는 “잠금 유지” 플래그다. 그 뒤 cubmethod::get_callback_handler ()->free_query_handle_all (true)를 호출해 SP 메서드 콜백 층에 묶인 준비된 구문 핸들을 모두 강제 닫는다. 이는 연결이 보류 가능하지 않을 때 커밋 시 모든 PreparedStatement 객체를 닫는 JDBC 동작의 SP 동치물이다. db_abort_transactiontran_abort를 쓰는 것 외에 같은 구조다. 둘 다 db_admin.c에 있다.

CUBRID에는 두 개의 오류 채널이 있고 DBI는 둘 모두에 말을 건다. 세션 스코프 채널 db_get_errors / db_get_next_error는 파서의 오류 목록 (오류당 줄과 컬럼)을 반복하면서 er_set으로 전역 오류 스택에도 메시지를 밀어 넣는다. 그래서 er_errid()만 확인하는 호출자도 문법 오류를 볼 수 있다. 스레드 로컬 전역 오류 채널이 나머지 모든 것의 주된 메커니즘이다. 모든 db_*NO_ERROR / -1 / er_errid()를 반환하고, 호출자는 db_error_string / db_error_code를 불러 가장 최근 오류를 꺼낼 것으로 기대된다. db_vdb.c 곳곳에 박힌 assert(er_errid() != NO_ERROR) 단언이 -1을 반환할 때 항상 er_errid()가 설정되도록 강제한다.

CCI 드라이버: 브로커 측 ux_* 파사드

섹션 제목: “CCI 드라이버: 브로커 측 ux_* 파사드”

CCI 와이어 프로토콜은 src/broker/cas_protocol.h에 있고 서버 측은 src/broker/cas_function.csrc/broker/cas_execute.c에 있다. broker/cas.c의 디스패치 테이블 server_fn_table[](func_code - 1) 로 인덱싱되는 평탄한 배열이다. 항목들은 JDBC 표면을 끝에서 끝까지 덮는다. 트랜잭션 제어(fn_end_tran), 준비/실행(fn_prepare, fn_execute, fn_prepare_and_execute, fn_execute_batch, fn_execute_array), 결과 탐색(fn_cursor, fn_fetch, fn_cursor_update, fn_cursor_close, fn_next_result, fn_close_req_handle, fn_get_generated_keys, fn_make_out_rs), 스키마 조회(fn_schema_info, fn_get_db_version, fn_get_class_num_objs, fn_get_attr_type_str, fn_parameter_info, fn_get_query_info), 객체 접근(fn_oid_get, fn_oid_put, fn_oid, fn_collection), 세션 상태(fn_get_db_parameter, fn_set_db_parameter, fn_savepoint, fn_get_row_count, fn_get_last_insert_id, fn_end_session, fn_set_cas_change_mode, fn_check_cas), XA 2-phase commit(fn_xa_prepare, fn_xa_recover, fn_xa_end_tran), LOB 입출력(fn_lob_new, fn_lob_write, fn_lob_read). 사용되지 않는 인덱스에는 폐기된 GLO 슬롯 (CAS_FC_DEPRECATED1..4)과 CAS_FC_GET_SHARD_INFO 자리가 fn_deprecated / fn_not_supported로 자리를 지킨다.

CAS 프로세스는 소켓에서 함수 코드를 읽어 해당 fn_*로 디스패치한다. fn_*argv[]에서 인자를 꺼내 DBI 형태의 C 타입으로 변환한 뒤 하나 이상의 ux_* 함수를 호출한다. 실제 작업은 ux_* 층에서 이루어지며, fn_* 층은 순수하게 와이어 연결 고리다.

연결 경로에서, ux_database_connect는 브로커의 접근 모드 (shm_appl->access_mode ∈ {READ_ONLY_ACCESS_MODE, SLAVE_ONLY_ACCESS_MODE, 기본값})를 클라이언트 타입 (DB_CLIENT_TYPE_BROKER, DB_CLIENT_TYPE_READ_ONLY_BROKER, DB_CLIENT_TYPE_SLAVE_ONLY_BROKER, 그리고 *_REPLICA_ONLY 변형)으로 매핑하고, db_set_preferred_hosts / db_set_connect_order / db_set_max_num_delayed_hosts_lookup으로 브로커의 선호 호스트/연결 순서 설정을 DBI 전역 변수로 복사한 뒤, db_restart_ex를 호출해 데이터베이스 연결을 연다. 성공하면 데이터베이스 이름과 호스트를 as_info에 복사하고, last_connect_time을 설정하며, ux_get_default_setting으로 브로커의 잠금 타임아웃, 격리 수준, 시스템 파라미터를 읽는다. 트리거 토글 (db_enable_trigger / db_disable_trigger)은 shm_appl->trigger_action_flag 를 따른다.

CCI 고유의 특성이 두 가지 두드러진다. 첫째, 브로커는 모든 CCI 연결을 상태를 가진 데이터베이스 클라이언트 연결로 다루지만, 그것을 많은 CCI 연결(JDBC 클라이언트로부터)에 걸쳐 풀링한다. JDBC 클라이언트가 연결하면 브로커는 유휴 CAS 프로세스를 넘겨주며, 그 CAS 프로세스는 이미 데이터베이스에 붙어 있다는 점이다. 둘째, 데이터베이스에 대한 CAS 연결은 프로세스 단위이며 많은 JDBC 연결에 걸쳐 유지된다. ux_database_connect는 브로커 프로세스가 올바른 사용자로 이미 올바른 데이터베이스에 붙어 있으면 일찍 반환한다.

준비 경로에서, ux_preparehm_new_srv_handleT_SRV_HANDLE을 할당하고, db_open_bufferDB_SESSION을 열며, db_compile_statement 를 호출해 클라이언트 측 컴파일 파이프라인 전체를 진행한다. 그 뒤 T_NET_BUF로 새 srv_h_id, 결과 캐시 수명, 구문 타입 바이트, 바인드 마커 수 네 가지를 직렬화해 돌려보낸다. 컬럼 목록(컬럼 이름, 속성 이름, 명세 이름, 타입, 크기, 도메인)은 prepare_column_list_info_set 으로 이어진다. DB_SESSION *은 나중에 ux_execute가 다시 쓸 수 있도록 srv_handle->session 안에 보관된다. CCI_PREPARE_INCLUDE_OID, CCI_PREPARE_XASL_CACHE_PINNED, CCI_PREPARE_HOLDABLE, CCI_PREPARE_UPDATABLE 플래그는 컴파일 전에 db_include_oiddb_session_set_* 호출로 일대일 변환된다. HOLDABLEUPDATABLE을 함께 쓰면 CAS_ER_HOLDABLE_NOT_ALLOWED로 일찍 거부된다.

CCI 서버 핸들 T_SRV_HANDLEhm_new_srv_handle(cas_handle.c)이 할당하고 정수 인덱스로 JDBC 클라이언트에 돌아간다. 이후 ux_execute / ux_fetch 호출은 hm_find_srv_handle로 핸들을 찾는다. 이 층 덕분에 JDBC 연결 하나가 여러 열린 PreparedStatement 객체를 동시에 들 수 있다. 비록 아래의 CAS 프로세스가 db_* 연결을 하나만 갖더라도 말이다.

실행 경로에서, 브로커가 DBI 가장 깊은 곳까지 들어간다. ux_execute가 하는 일은 다음과 같다.

  1. hm_qresult_end를 호출해 같은 핸들에 있는 이전 결과를 해제한다.
  2. 구문이 미리 준비되었다면 srv_handle->session에서 DB_SESSION *을 복구한다. 그렇지 않으면 db_open_buffer (srv_handle->sql_stmt)로 다시 파싱하고 재컴파일한다.
  3. make_bind_valueargv[]DB_VALUE * 배열로 변환한 뒤 set_host_variables(db_push_values를 감싼)로 파서에 밀어 넣는다.
  4. CCI_EXEC_RETURN_GENERATED_KEYS, CCI_EXEC_QUERY_INFO, srv_handle->is_holdable, srv_handle->auto_commit_mode를 해당 db_session_set_* 세터로 처리한다.
  5. db_execute_and_keep_statement (session, stmt_id, &result)를 실행한다. JDBC가 재실행할 수 있어야 하므로 db_execute_statement를 쓰지 않는다.
  6. db_query_set_copy_tplvalue (result, 0)으로 결과를 들여다보기 모드로 전환해, 행 값 포인터가 목록 파일 버퍼를 별칭으로 참조하게 한다. 컬럼당 malloc을 피하기 위함이다.
  7. JDBC가 max_row > 0을 주면, db_query_seek_tuple (result, max_row, 1) 을 호출하고 행 수를 잘라낸다.
  8. 결과 행 수를 직렬화하고, execute_info_set으로 컬럼 정보를, 그리고 프로토콜 버전에 따라 추가 필드를(PROTOCOL_V2에서 컬럼 수명 정보, PROTOCOL_V5에서 샤드 id) 직렬화한다.
  9. do_commit_after_execute가 참이면(비 SELECT 구문에서의 구문별 자동 커밋), req_info->need_auto_commit = TRAN_AUTOCOMMIT을 설정해 와이어 응답이 전송된 뒤 디스패처가 커밋하도록 한다.

db_execute_statement가 아닌 db_execute_and_keep_statement를 쓴다는 점에 주목하라. 브로커는 동일한 PreparedStatement의 재실행에 걸쳐 파스 트리를 살려 둔다. CCI_EXEC_QUERY_INFO 플래그는 JDBC의 CUBRIDPreparedStatement.setQueryInfo 토글이다. 설정되면 CAS가 구문을 재컴파일하고 계획을 임시 파일에 써서 JDBC 클라이언트가 가져갈 수 있게 한다. max_row 상한은 커서를 탐색해 브로커 측(서버 관점에서는 클라이언트 측)에서 적용된다. 서버는 JDBC의 Statement.setMaxRows를 알지 못한다.

가져오기 경로에서, ux_fetch는 핸들을 검증하고, CCI_PREPARE_CALL이 설정되면 저장 프로시저 호출을 fetch_call로 디스패치하며, 그렇지 않으면 srv_handle->schema_type(-1이면 일반 쿼리 → fetch_result, CCI_SCH_* 상수면 fetch_class, fetch_attribute 같은 스키마 정보 가져오기)으로 소규모 디스패치 테이블 fetch_func[]를 인덱싱한다. 잎 함수 fetch_resultdb_query_seek_tuplecursor_pos로 이동한 뒤, fetch_count번 루프를 돌면서 컬럼마다 db_query_get_tuple_value를 호출하고 dbval_to_net_buf로 각 DB_VALUE를 와이어 버퍼에 직렬화하는 cur_tuple을 호출한다.

sequenceDiagram
    participant JDBC as JDBC PreparedStatement.executeQuery
    participant CCI as CCI 드라이버 (TCP)
    participant Broker as 브로커 데몬
    participant CAS as CAS 워커
    participant DBI as db_compile_statement / db_execute_and_keep_statement
    participant NetCl as network_cl
    participant Server as cub_server

    JDBC->>CCI: SQL + 파라미터
    CCI->>Broker: CAS_FC_PREPARE + sql_stmt
    Broker->>CAS: 디스패치
    CAS->>DBI: db_open_buffer + db_compile_statement
    DBI->>NetCl: pt_class_pre_fetch (NRP 잠금)
    NetCl->>Server: NET_SERVER_LC_FETCH_LOCKSET
    DBI->>NetCl: do_prepare_statement
    NetCl->>Server: NET_SERVER_QM_QUERY_PREPARE
    Server-->>NetCl: XASL_ID
    DBI-->>CAS: stmt_id
    CAS-->>CCI: srv_h_id, num_markers, 컬럼 타입
    JDBC->>CCI: setInt(1, 42)
    JDBC->>CCI: executeQuery
    CCI->>Broker: CAS_FC_EXECUTE + 바인드 값
    Broker->>CAS: 디스패치
    CAS->>DBI: db_push_values + db_execute_and_keep_statement
    DBI->>NetCl: do_execute_statement
    NetCl->>Server: NET_SERVER_QM_QUERY_EXECUTE
    Server-->>NetCl: 행 수, list_id
    DBI-->>CAS: DB_QUERY_RESULT
    CAS-->>CCI: 행 수, 컬럼 정보, 첫 번째 배치
    JDBC->>CCI: rs.next()
    CCI->>Broker: CAS_FC_FETCH
    Broker->>CAS: 디스패치
    CAS->>DBI: db_query_seek_tuple + db_query_get_tuple_value
    DBI-->>CAS: 컬럼당 DB_VALUE
    CAS-->>CCI: 직렬화된 행

핵심은 CCI 실행기가 따로 없다는 점이다. 브로커의 CCI 층은 db_* 위의 얇은 파사드이며 클라이언트 측 컴파일, 실행, 가져오기 논리 전체를 재사용 한다. 중요한 분리는 브로커 위의 네트워크 경계다. CSQL csql -u dba demodb 세션은 인프로세스에서 db_*를 직접 호출한다. JDBC 세션은 세 개의 TCP 소켓을 거친다(JDBC ↔ 브로커, 브로커 ↔ CAS, CAS ↔ 서버). 하지만 가장 깊은 두 소켓은 같은 머신 안에 있고, 가장 깊은 하나 — CAS ↔ 서버 — 는 cubrid-network-protocol.md에 문서화된 동일한 NRP 와이어 형식을 쓴다.

DBI 표면은 src/compat/의 아홉 파일에 걸쳐 있다. 책임별 핵심 심볼:

연결 수명 주기 (db_admin.c) — db_init, db_login, db_restart, db_restart_ex, db_shutdown, db_shutdown_without_request_to_server, db_ping_server, db_disable_modification, db_enable_modification, db_end_session, db_set_client_type, db_set_preferred_hosts, db_get_row_count, db_get_last_insert_id.

트랜잭션 제어 (db_admin.c) — db_commit_transaction, db_abort_transaction, db_set_isolation, db_set_lock_timeout, db_set_system_parameters, db_get_system_parameters.

구문 컴파일과 실행 (db_vdb.c) — db_open_buffer, db_open_buffer_local, db_open_file, db_compile_statement, db_compile_statement_local, db_rewind_statement, db_statement_count, db_open_buffer_and_compile_first_statement, db_compile_and_execute_queries_internal, db_execute_statement, db_execute_statement_local, db_execute_and_keep_statement, db_execute_and_keep_statement_local, db_drop_statement, db_drop_all_statements, db_close_session, db_close_session_local, db_push_values, db_get_hostvars, db_get_lock_classes, db_get_errors, db_get_next_error, db_get_warnings, db_session_set_holdable, db_session_set_xasl_cache_pinned, db_session_set_return_generated_keys, db_set_statement_auto_commit, db_get_query_type_list, db_invalidate_mvcc_snapshot_before_statement, do_process_prepare_statement, do_get_prepared_statement_info, do_cast_host_variables_to_expected_domain, do_recompile_and_execute_prepared_statement, do_process_deallocate_prepare.

쿼리 결과와 커서 (db_query.c) — db_query_first_tuple, db_query_next_tuple, db_query_prev_tuple, db_query_last_tuple, db_query_seek_tuple, db_query_get_tplpos, db_query_set_tplpos, db_query_free_tplpos, db_query_get_tuple_value, db_query_get_tuple_value_by_name, db_query_get_tuple_valuelist, db_query_tuple_count, db_query_column_count, db_query_end, db_query_end_internal, db_query_set_copy_tplvalue, db_is_client_cache_reusable, db_query_prefetch_columns, db_free_query_format, db_cp_query_type, or_pack_query_format, or_unpack_query_format.

스키마 정의 (db_class.c) — db_create_class, db_drop_class, db_drop_class_ex, db_rename_class, db_add_attribute, db_add_shared_attribute, db_add_class_attribute, db_drop_attribute, db_add_method, db_add_class_method, db_add_super, db_drop_super, db_change_default, db_constrain_non_null, db_constrain_unique.

객체 API (db_obj.c) — db_create, db_create_by_name, db_get, db_get_shared, db_get_expression, db_put, db_get_attribute_descriptor, db_get_method_descriptor, db_create_trigger, db_get_serial_current_value, db_get_serial_next_value.

값 기본 연산 (db_macro.c, db_set.c) — db_make_int, db_make_bigint, db_make_string, db_make_varchar, db_make_date, db_make_datetime, db_make_timestamp, db_make_set, db_make_multiset, db_make_sequence, db_make_object, db_make_oid, db_make_null, db_value_clear, db_value_clone, db_value_free, db_value_create, db_value_domain_init.

CCI 브로커 서버 측 (broker/cas_execute.c) — ux_database_connect, ux_database_shutdown, ux_set_session_id, ux_prepare, ux_execute, ux_execute_all, ux_execute_call, ux_execute_batch, ux_execute_array, ux_fetch, ux_oid_get, ux_cursor, ux_cursor_update, ux_cursor_close, ux_end_tran, ux_end_session, ux_get_row_count, ux_get_last_insert_id, fetch_result, fetch_call, prepare_column_list_info_set, set_host_variables, make_bind_value, netval_to_dbval, dbval_to_net_buf, do_commit_after_execute.

CCI 브로커 디스패치 (broker/cas.c, broker/cas_function.c) — server_fn_table, server_func_name, process_request, fn_end_tran, fn_prepare, fn_execute, fn_get_db_parameter, fn_close_req_handle, fn_cursor, fn_fetch, fn_schema_info, fn_oid_get, fn_oid_put, fn_oid, fn_collection, fn_next_result, fn_execute_batch, fn_execute_array, fn_cursor_update, fn_xa_prepare, fn_xa_recover, fn_xa_end_tran, fn_con_close, fn_check_cas, fn_get_generated_keys, fn_end_session, fn_get_row_count, fn_get_last_insert_id, fn_prepare_and_execute, fn_cursor_close.

네트워크 클라이언트 래퍼 (communication/network_cl.c) — net_client_init, net_client_final, net_client_request, net_client_request_with_callback, net_client_request_2recv_copyarea.

부트 (transaction/boot_cl.c, cubrid-boot.md 상호 참조) — boot_initialize_client, boot_restart_client, boot_shutdown_client, boot_client_initialize_css.

심볼파일
db_open_buffer_localsrc/compat/db_vdb.c214
db_open_buffersrc/compat/db_vdb.c246
db_compile_statement_localsrc/compat/db_vdb.c531
db_compile_statementsrc/compat/db_vdb.c851
db_get_errorssrc/compat/db_vdb.c1011
db_push_valuessrc/compat/db_vdb.c1612
db_execute_and_keep_statement_localsrc/compat/db_vdb.c1708
do_process_prepare_statementsrc/compat/db_vdb.c2492
do_cast_host_variables_to_expected_domainsrc/compat/db_vdb.c2797
do_recompile_and_execute_prepared_statementsrc/compat/db_vdb.c3064
db_execute_and_keep_statementsrc/compat/db_vdb.c3243
db_execute_statement_localsrc/compat/db_vdb.c3276
db_execute_statementsrc/compat/db_vdb.c3315
db_open_buffer_and_compile_first_statementsrc/compat/db_vdb.c3344
db_compile_and_execute_queries_internalsrc/compat/db_vdb.c3433
db_close_session_localsrc/compat/db_vdb.c3581
db_close_sessionsrc/compat/db_vdb.c3659
db_initsrc/compat/db_admin.c170
db_loginsrc/compat/db_admin.c854
db_restartsrc/compat/db_admin.c918
db_restart_exsrc/compat/db_admin.c982
db_shutdownsrc/compat/db_admin.c1012
db_end_sessionsrc/compat/db_admin.c1086
db_commit_transactionsrc/compat/db_admin.c1169
db_abort_transactionsrc/compat/db_admin.c1194
db_query_next_tuplesrc/compat/db_query.c2188
db_query_first_tuplesrc/compat/db_query.c2409
db_query_seek_tuplesrc/compat/db_query.c2555
db_query_get_tuple_valuesrc/compat/db_query.c2978
db_query_get_tuple_value_by_namesrc/compat/db_query.c3064
db_query_tuple_countsrc/compat/db_query.c3194
db_query_endsrc/compat/db_query.c3477
db_query_end_internalsrc/compat/db_query.c3588
db_create_classsrc/compat/db_class.c70
db_add_attribute_internalsrc/compat/db_class.c184
db_add_attributesrc/compat/db_class.c248
db_createsrc/compat/db_obj.c70
db_getsrc/compat/db_obj.c234
db_putsrc/compat/db_obj.c319
db_create_triggersrc/compat/db_obj.c1302
boot_initialize_clientsrc/transaction/boot_cl.c275
boot_restart_clientsrc/transaction/boot_cl.c690
boot_shutdown_clientsrc/transaction/boot_cl.c1352
net_client_initsrc/communication/network_cl.c3657
net_client_request_with_callbacksrc/communication/network_cl.c1153
ux_database_connectsrc/broker/cas_execute.c376
ux_database_shutdownsrc/broker/cas_execute.c589
ux_preparesrc/broker/cas_execute.c618
ux_end_transrc/broker/cas_execute.c874
ux_executesrc/broker/cas_execute.c992
ux_execute_allsrc/broker/cas_execute.c1307
ux_execute_arraysrc/broker/cas_execute.c2086
ux_fetchsrc/broker/cas_execute.c2442
ux_cursorsrc/broker/cas_execute.c2583
ux_cursor_closesrc/broker/cas_execute.c2732
fetch_resultsrc/broker/cas_execute.c5006
fetch_callsrc/broker/cas_execute.c9018
server_fn_tablesrc/broker/cas.c75
fn_preparesrc/broker/cas_function.c231
fn_executesrc/broker/cas_function.c345
fn_prepare_and_executesrc/broker/cas_function.c661
fn_fetchsrc/broker/cas_function.c959
struct db_sessionsrc/compat/db_session.h26
  • 세션이라는 단어가 세 군데서 쓰인다. CUBRID에서 세션이라는 이름을 공유하는 세 가지가 있다. 여기서 다룬 파서 세션 DB_SESSION (하나 또는 여러 SQL 구문 범위), db_login / db_restart가 만드는 클라이언트 세션(프로세스 전역 데이터베이스 연결), 그리고 src/session/session.c서버 세션 SESSION_STATE(준비된 구문 바인딩과 SET 변수 바인딩, 마지막 삽입 ID를 담는 클라이언트별 서버 측 컨테이너)가 그것이다. DB_SESSION은 완전히 클라이언트에 살며 db_close_session으로 죽는다. SESSION_STATE는 완전히 서버에 살며 클라이언트가 모든 NRP 요청에 담아 보내는 정수 SESSION_ID로 키잉된다. cubrid-server-session.md 참조.

  • db_compile_statement는 1부터 시작하는 구문 번호를 반환한다. 반환값은 stmt_ndx + 1이므로, 호출자는 성공한 첫 번째 구문(1 반환)과 구문 없음(0 반환)을 구분할 수 있다. db_execute_statement 계열은 내부에서 인덱스를 감소시킨다. db_execute_and_keep_statement_local 윗 부분의 stmt_ndx--; 줄이 정확히 이 변환이다.

  • db_execute_statement는 파스 트리를 해제하고, db_execute_and_keep_statement는 해제하지 않는다. 두 형태의 유일한 동작 차이지만, 구문을 재실행할 수 있는지를 결정한다. 브로커는 JDBC PreparedStatement 의미가 재실행을 요구하기 때문에 and-keep 형태를 쓴다.

  • CCI 클라이언트 라이브러리는 별도 저장소에 있다. 여기서 다룬 서버 측 CCI 로직은 src/broker/cas_*.c에 있으며 CUBRID 서버 소스와 함께 배포된다. JDBC/Python/ODBC가 링크하는 클라이언트 측 CCI 라이브러리(libcascci.so)는 별도의 cubrid-cci git 서브모듈에 산다. 분석 시점에 작업 트리에 없었다. 이 문서의 와이어 세부 내용은 서버 측 cas_*.c 파일과 cas_protocol.h에서 파생했다.

  • 스키마 조작은 세션 API 수준에서 MVCC를 우회한다. db_create_classdb_add_attribute는 즉시 스키마 변경을 일으킨다. API 수준에 템플릿 안 단계가 노출되지 않는다. C 코드에서 여러 단계의 변경을 하려면 smt_def_class / smt_edit_class_mop / smt_add_attribute로 템플릿을 직접 조합하고 sm_update_class로 한 번 커밋해야 한다.

  • do_prepare_statement 서버 왕복은 조건부다. db_compile_statement_local 에서 준비 단계는 PRM_ID_XASL_CACHE_MAX_ENTRIES > 0이고 statement->flag.cannot_prepare == 0일 때만 실행된다. 그렇지 않으면 컴파일은 컴파일됨 단계에서 멈추고 실행기는 매번 XASL 스트림을 구성하는 오래된 do_statement 경로를 쓴다. 준비를 건너뛰었더라도 stage[stmt_ndx]StatementPreparedStage로 설정된다는 점에 주의해야 한다. 이 이름은 실행 준비됨을 의미하지 XASL 캐시에 등록됨을 의미하지 않는다.

  • T_CALLT_OBJFETCH에서의 커서 임의 접근은 퇴화형이다. 이 타입들은 정확히 하나의 행을 담으며 커서 함수는 crs_posC_BEFORE / C_ON / C_AFTER 사이에서만 이동시킨다. 완전한 임의 접근 의미론은 실제 목록 파일 커서를 아래에 가진 T_SELECT에만 적용된다.

  • 브로커의 T_SRV_HANDLE과 DBI의 DB_SESSION이 항상 1:1은 아니다. is_prepared == TRUE로 준비된 핸들은 ux_cursor_close / ux_close_req_handle까지 자신의 DB_SESSION을 소유한다. is_prepared == FALSE로 준비된 핸들(준비 시점에 의미 검사가 실패했지만 브로커가 실행 시점에 재시도하려는 경우)은 ux_execute 안에서 새 DB_SESSION을 연다.

  • db_query_set_copy_tplvalue (result, 0)이 들여다보기 모드를 활성화한다. 기본적으로 커서는 목록 파일에서 행 값을 복사한다. 브로커는 ux_execute 이후 0 인자로 이 함수를 호출해 들여다보기 모드로 전환하고 컬럼당 malloc을 피한다. 브로커가 즉시 와이어 버퍼로 직렬화하기 때문에 안전하다.

  • XASL 캐시 무효화 재시도는 성공 경로에서 조용하다. do_execute_statementER_QPROC_INVALID_XASLNODE 또는 ER_QPROC_XASLNODE_RECOMPILE_REQUESTED를 반환하면, DBI는 구문의 xasl_id를 지우고 do_prepare_statement를 다시 호출하며 재시도한다. 호출자는 복구가 실패할 때만 오류를 본다.

  • db_invalidate_mvcc_snapshot_before_statement는 모든 최상위 실행 호출에서 실행된다. 이것이 CUBRID를 구문 단위 READ COMMITTED 엔진으로 만드는 장치다. 래퍼를 건너뛰고(바로 _local로 가면) 이전 스냅샷이 유지된다. 브로커는 래퍼를 호출하도록 신경을 쓴다.

  • cubrid-cci 서브모듈의 CCI 클라이언트 라이브러리가 이 작업 트리에 없었으므로, 이 문서는 서버 측 CCI 면을 다루지만 JDBC의 cubrid-jdbc 가 실제로 호출하는 클라이언트 측 cci_prepare, cci_execute, cci_fetch API는 다루지 않는다. 다음 단계에서 서브모듈의 cci_handle.ccci_query.c를 다루는 절을 추가해야 한다.

  • db_session_set_holdable과 서버 측 보류 가능 커서 목록 (SESSION_STATE.holdable_cursors)의 관계가 완전히 추적되지 않았다. 구체적으로, JDBC 클라이언트가 보류 가능 커서를 잊어버렸을 때 — 트랜잭션 커밋, 세션 타임아웃, 명시적 db_query_end 중 무엇이 — 해제하는지가 불분명하다.

  • db_execute_statement_local은 실행 후 인덱스 stmt_ndx - 1의 파스 트리를 해제한다. 호출자가 세션 하나에서 db_execute_statementdb_execute_and_keep_statement를 혼용하고 db_rewind_statement를 호출할 때의 실패 모드가 불분명하다.

  • do_prepare_statementdo_execute_statement가 사용하는 정확한 NRP 오피코드는 src/communication/network_interface_cl.c에 있지만 여기서 열거하지 않았다. cubrid-network-protocol.md로의 상호 참조는 이름으로만 이루어진다.

  • ux_execute_all(다중 구문 실행)은 구조적으로 ux_execute와 비슷하지만 자동 커밋이 꺼져 있을 때 부분 실패 시 해당 구문만 롤백하도록 세이브포인트 로직을 삽입한다. 정확한 세이브포인트 이름 형식과 ux_execute_batch / ux_execute_array와의 상호작용은 여기서 분석하지 않았다.

이 문서는 CUBRID 소스를 직접 읽어 정리했다.

  • src/compat/db_admin.c — 연결과 트랜잭션 제어.
  • src/compat/db_vdb.c — 구문 컴파일/실행, 호스트 변수, 준비된 구문 지원.
  • src/compat/db_query.c — 결과 집합과 커서.
  • src/compat/db_class.c — 스키마 정의.
  • src/compat/db_obj.c — 객체 API.
  • src/compat/db_session.h, db_query.h — 구조체 정의.
  • src/transaction/boot_cl.cboot_restart_client.
  • src/communication/network_cl.cnet_client_* 요청 계열.
  • src/broker/cas.c, cas_function.c — CCI 디스패치 테이블과 fn_* 와이어 연결 고리.
  • src/broker/cas_execute.cux_* 파사드.

이 코드 분석 트리 내 상호 참조: cubrid-network-protocol.md(NRP 와이어 형식), cubrid-server-session.md(SESSION_STATE), cubrid-boot.md (boot_restart_client), cubrid-broker.md(브로커 데몬과 CAS), cubrid-query-rewrite.md(pt_compile / mq_translate), cubrid-semantic-check.md(후기 바인드 의미 단계), cubrid-cursor.md (cursor_* 계열), cubrid-xasl-cache.md(do_prepare_statement 캐시).

이론적 참조: X/Open CLI 명세(1995); Database System Concepts (Silberschatz, Korth, Sudarshan, 5장 §Embedded SQL); Database Internals (Petrov, 5장 §Transactions); 스크롤 가능한 커서 탐색 모델에 대한 ODBC 프로그래머 참조; 모양과 명명 관례에서 가장 가까운 외부 대응물인 Oracle Call Interface 프로그래머 가이드.