콘텐츠로 이동

(KO) CUBRID Cursor — 서버 측 list-file을 한 페이지씩 끌어오는 클라이언트 fetch 핸들과 holdable / scroll 상태 관리

목차

질의 평가가 끝난 관계형 엔진은 결과로 튜플의 집합 을 손에 쥔다. 클라이언트가 그 집합을 한 번에 통째로 받아 가는 일은 거의 없다. 받아 가는 대신 걸어 다닌다. 그 보행기가 바로 커서(cursor) 다. 커서는 결과 집합 위에 떠 있는 위치 핸들로, 호출 한 번에 한 행(또는 작은 묶음)을 내어 주며 클라이언트–서버 라운드트립을 여러 번 넘어가도 같은 자리를 기억한다. Database System Concepts (Silberschatz, 5장 Application Development)는 커서를 관계형 의미론(질의는 집합을 만들며, ORDER BY가 없는 한 집합엔 순서가 없다)과 호스트 언어 의미론(호스트는 반복하고 싶고, 분기하고 싶고, 행 단위로 갱신하고 싶다) 사이의 다리라 부른다. 다리는 위치 기반이므로 커서는 현재 위치진행 방향 을 들고 다녀야 한다. 나머지(블록 크기, prefetch, 락)는 모두 튜닝 다이얼이다.

ANSI/ISO SQL은 직교하는 네 개의 커서 속성을 정의한다. 모든 커서 구현은 이 4차원 공간 위 한 점에 안착한다.

  1. Scrollability(스크롤 가능성). FORWARD ONLY는 앞으로만 가고, SCROLLFETCH FIRST | LAST | PRIOR | NEXT | ABSOLUTE n | RELATIVE n을 모두 받는다. 교과서적 비용은, 스크롤 가능성을 허용하는 순간 결과를 머터리얼라이즈 해 두어야 한다는 것이다. 포워드 전용 커서는 연산자 트리에서 곧장(Volcano next() 호출 체인을 따라) 스트리밍할 수 있지만, 역방향 fetch는 버퍼링된 튜플 스트림 위로 random access를 요구한다.
  2. Sensitivity(감응성). INSENSITIVE 커서는 OPEN 시점의 데이터 스냅샷만 본다. 같은 트랜잭션이든 다른 트랜잭션이든 그 이후의 갱신은 커서가 토해 내는 결과에 영향을 주지 않는다. SENSITIVE 커서는 질의 술어를 만족시키는 커밋된 갱신을 본다. ASENSITIVE는 구현 정의(implementation-defined)다. 교과서적 구현은 insensitive면 결과를 머터리얼라이즈하고, sensitive면 fetch마다 재평가한다.
  3. Updatability(갱신 가능성). UPDATABLE 커서는 WHERE CURRENT OF 갱신/삭제를 허용한다. 비용은, 커서가 자기 튜플 스트림 안에 행을 식별할 수 있는 핸들 — OID, rowid, ctid 같은 을 함께 날라야 한다는 점이다. 그래야 엔진이 갱신 대상 행을 다시 찾는다.
  4. Holdability(보존성). WITH HOLD 커서는 자기를 연 트랜잭션의 COMMIT 을 넘어 살아남는다. WITH HOLD가 없으면 커서는 커밋 시점에 파괴된다. 이유는 두 가지다. 첫째, 결과 스트림을 받치는 임시 파일이 트랜잭션 종료와 함께 회수될 수 있고, 둘째, 커서가 잡고 있던 락이 모두 풀린다. holdability를 지원하려면 엔진은 결과 스트림을 트랜잭션에서 분리 한 뒤, 세션 범위 저장소에 다시 매달거나 세션 종료(또는 커서 close, 어느 쪽이든 먼저)까지 살아 있는 immutable read-only 산출물로 변환해야 한다.

따라서 모든 커서 구현은 (클라이언트 측 위치 상태) × (서버 측 결과 스트림) × (수명 바인딩) 의 3-튜플로 환원된다. 클라이언트 측은 한 튜플씩 앞뒤로 걸어가거나 first/last로 점프하는 동작을 서버에 다시 묻지 않고 빠르게 해내야 한다. 서버 측은 그 스트림을 다시 읽을 수 있는 형태로 머터리얼라이즈해 둬야 한다. 수명 바인딩은 그 스트림이 커밋·질의 close·트랜잭션 abort·세션 종료·연결 단절 중 어느 시점에 파괴되는지를 결정한다.

또 하나 따로 다루어야 할 결정은 결과 셋(result-set) vs. 커서 다. JDBC와 ODBC 클라이언트는 결과 셋 객체(컬럼 메타데이터를 얹은 커서의 얇은 wrapper)와 커서 자체를 구분한다. CUBRID는 둘을 하나로 합쳐 둔다. 클라이언트가 손에 쥐는 객체는 CURSOR_ID고, 컬럼 메타데이터는 그 안의 QFILE_LIST_ID 타입 리스트에 따로 실려 다닌다. 상위 wrapper(DB_QUERY_RESULT, 브로커 측의 T_QUERY_RESULT)는 커서를 값(by value)으로 들고 있다가 그 위에 result-set 모양의 메타데이터를 덧입힌다.

DBMS 공통 설계 패턴 (Common DBMS Design)

섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”

서버–클라이언트 RDBMS는 결국 몇 개의 정형 패턴 위에 안착한다. 다이얼은 셋이다. 결과를 어디에 두는가, 누가 그 결과를 와이어 너머로 페이지 단위로 운반하는가, 그리고 holdability 경계는 어떻게 강제되는가 가 그 셋이다. 어휘 자체는 엔진들 사이에서 거의 같다.

PostgreSQL — Portal. Postgres에서 커서에 대응하는 것은 Portal 이다(src/backend/utils/mmgr/portalmem.c, src/backend/tcop/pquery.c). Portal은 질의 plan, QueryDesc, 실행기 상태, 그리고 전략 을 한데 묶는다. 전략은 셋이다. 스트리밍 케이스의 PORTAL_ONE_SELECT, 머터리얼라이즈 케이스의 PORTAL_UTIL_SELECT, 다중 statement의 PORTAL_MULTI_QUERY. DECLARE CURSOR로 만들어진 커서는 Portal을 만들어 내며, FETCH 명령(PerformPortalFetch)이 PortalRun을 구동해 실행기 iterator 트리를 정방향(또는 전략이 PORTAL_ONE_SELECT이고 cursorOptions & CURSOR_OPT_SCROLL인 경우 역방향)으로 걸어간다. WITH HOLD portal(HoldPortal)은 커밋 시점에 남은 행을 Tuplestore로 옮겨 넣어 트랜잭션을 넘기 살아남게 한다. Portal은 트랜잭션보다 오래 사는 메모리 컨텍스트에 머문다.

MySQL — Stored procedure 커서. MySQL은 서버 측 커서를 stored procedure 안에서만 노출한다 (DECLARE c CURSOR FOR SELECT ...; OPEN c; FETCH c INTO v; CLOSE c;). 구현 (sql/sp_rcontext.cc, sql/sql_cursor.cc) 은 결과를 MEMORY 또는 MyISAM 임시 테이블 위의 Server_side_cursor에 써 두고, FETCH는 그 테이블을 시퀀스 번호로 걸어간다. 프로토콜 단에서 SCROLL / HOLD를 노출하는 클라이언트 API는 없다. 와이어 너머로 클라이언트가 받는 것은 배치된 row 묶음이고, 스크롤이라는 동작은 클라이언트 드라이버의 로컬 캐시 위에서 일어난다. holdability는 의미가 없다. stored procedure 자체가 트랜잭션 안에서만 끝까지 돌기 때문이다.

Oracle — REF CURSOR. Oracle의 SYS_REFCURSOR는 PL/SQL이 클라이언트로 되돌려 보내는 타입 있는 핸들이다. 클라이언트(OCI 또는 JDBC)는 OCIStmtFetch2로 행을 끌어 간다. 결과는 서버 측 영역(QEPROW_FRAME)에 머무르며 요청에 따라 다시 fetch된다. 스크롤은 OCI의 OCI_FETCH_FIRST, OCI_FETCH_LAST 같은 옵션으로 지원된다. holdability는 암묵적이다. REF CURSOR는 호출 블록에 묶이지 트랜잭션에 묶이지 않으므로, 자기를 만든 안쪽 트랜잭션을 자연스럽게 넘긴다.

SQL Server — 커서 타입. SQL Server는 ANSI 메뉴를 가장 풍성하게 펼쳐 둔다. STATIC(tempdb 위 스냅샷), KEYSET-driven(키 컬럼만 저장하고 나머지는 위치마다 다시 fetch), DYNAMIC(fetch마다 술어 재평가), FAST_FORWARD(forward-only, read-only, 재평가 없음). 저장소 기반은 tempdb worktable이다. WITH HOLD는 글로벌 커서의 CURSOR_HOLD_OVER_COMMIT로 지원된다.

CUBRID는 static-snapshot, optionally-scrollable, optionally- updatable 커서를 서버 측 list file(cubrid-list-file.md에서 설명된 머터리얼라이즈된 튜플 스트림) 위에 모두 얹어 구현한다. ANSI 용어로 보면 따라서 CUBRID 커서는 항상 insensitive다. 스냅샷은 OPEN 시점에 실행기가 list file에 써 둔 그 스냅샷이다. 클라이언트 측은 한 번에 한 페이지씩 운반하며, 페이지 체인 안에서 위치를 다시 잡는 방식으로 forward, backward, first, last를 지원한다. 질의를 다시 실행하지 않는다. holdability는 query manager의 트랜잭션 범위 테이블에서 list file을 떼어 내 세션의 holdable cursor 리스트에 다시 매다는 식으로 구현된다 (cubrid- server-session.mdSESSION_QUERY_ENTRY 참고).

이론 개념CUBRID 명칭
커서 객체(클라이언트 측)CURSOR_ID (src/query/cursor.h)
그 아래의 머터리얼라이즈 스트림QFILE_LIST_ID (query_list.h) — cubrid-list-file.md 참고
집합 위 커서 위치CURSOR_POSITION enum (C_BEFORE / C_ON / C_AFTER)
페이지 안의 위치current_vpid, current_tuple_no, current_tuple_offset
커서 opencursor_open
정방향 fetchcursor_next_tuple
역방향 fetchcursor_prev_tuple
head로 점프cursor_first_tuple
tail로 점프cursor_last_tuple
튜플 값 디코딩cursor_get_tuple_value / _value_list
close + freecursor_close / cursor_free
네트워크 페이지 운반qfile_get_list_file_page (클라이언트) / xqfile_get_list_file_page (서버)
Updatable 커서의 OID 컬럼is_oid_included 플래그, cursor_get_current_oid
역참조 OID prefetchcursor_prefetch_first_hidden_oid / _column_oids
Prefetch 락 모드cursor_set_prefetch_lock_mode
Peek vs copy 의미론cursor_set_copy_tuple_value / is_copy_tuple_value
Holdable 결과 플래그RESULT_HOLDABLE 비트 in QUERY_FLAG (query_list.h)
세션이 들고 있는 holdable 커서 상태SESSION_QUERY_ENTRY::list_id (in session.c)
holdable 커서 보존 hooksession_preserve_temporary_files (session.c)
커밋 시점 hand-offqmgr_clear_trans_wakeupxsession_store_query_entry_info
커밋 시점 destroy 경로qmgr_clear_trans_wakeup (non-holdable 분기)
커밋 후 reloadqmgr_get_query_entryxsession_load_query_entry_info
상위 결과 wrapperDB_QUERY_RESULT / DB_SELECT_RESULT (db_query.h)
브로커 측 statement 상태T_SRV_HANDLE::is_holdable, T_QUERY_RESULT (cas_execute.c)

CUBRID를 구분 짓는 아키텍처적 결정은 그 아래 깔린 list file 이다. 실행기의 모든 머터리얼라이즈 연산자가 이미 QFILE_LIST_ID를 만들어 내기 때문에(cubrid-list-file.md 참고), 커서 모듈은 자기 저장소를 따로 들 필요가 없다. 실행기가 만들어 둔 서버 측 산출물 위에 얇은 클라이언트 측 reader를 얹은 것일 뿐이다. 정렬, 해시 빌드, GROUP BY, 최종 결과가 모두 같은 모양으로 쓰여지고, 커서는 그것을 읽는다. 비용은 모든 커서 결과가 머터리얼라이즈된다는 점이다. 스트리밍 forward-only 커서를 위한 fast path가 따로 없다. 대신 얻는 보상은 한 가지 튜플 포맷, 한 가지 페이지 포맷, 한 가지 네트워크 프로토콜로 결과를 내는 모든 질의가 통일된다는 것이다.

커서 모듈은 네 개의 움직이는 부품으로 이뤄져 있다. 커서별 위치 상태를 들고 있는 CURSOR_ID struct, 한 번에 한 QFILE_LIST_ID 페이지를 와이어 너머로 끌어오는 page-fetch 루프, 길이 prefix가 박힌 packed 바이트를 타입 있는 DB_VALUE로 풀어 주는 튜플 디코딩 경로, 그리고 커서를 떠받치는 query entry, query manager의 트랜잭션 범위 테이블, 세션의 holdable list 사이에서 일어나는 holdability hand-off. 이 순서대로 따라간다.

flowchart LR
  subgraph CL["클라이언트 측 (CS 또는 SA)"]
    APP["애플리케이션 / JDBC / CCI / 브로커 (CAS)"]
    DBQR["DB_QUERY_RESULT<br/>(db_query.h)<br/>res.s.cursor_id"]
    CID["CURSOR_ID<br/>(cursor.h)"]
    BUF["buffer_area<br/>(IO_MAX_PAGE_SIZE)"]
    LISTID_CL["로컬 QFILE_LIST_ID 사본<br/>(open 시점에 deep copy)"]
  end
  subgraph NET["네트워크 (CS_MODE)"]
    GLP["NET_SERVER_LS_GET_LIST_FILE_PAGE<br/>(network_interface_cl.c)"]
  end
  subgraph SR["서버"]
    XGLP["xqfile_get_list_file_page<br/>(list_file.c)"]
    QMGR["qmgr_get_query_entry<br/>(query_manager.c)"]
    QENT["QMGR_QUERY_ENTRY<br/>list_id, temp_vfid, is_holdable"]
    LISTID_SR["QFILE_LIST_ID<br/>(머터리얼라이즈된 결과)"]
    PAGES["페이지 체인<br/>membuf + FILE_TEMP"]
    SESS["SESSION_STATE.queries<br/>SESSION_QUERY_ENTRY (holdable)"]
  end

  APP --> DBQR
  DBQR --> CID
  CID --> BUF
  CID --> LISTID_CL
  CID -- 한 번에 한 페이지씩 --> GLP
  GLP --> XGLP
  XGLP --> QMGR
  QMGR --> QENT
  QENT --> LISTID_SR
  LISTID_SR --> PAGES
  QENT -. COMMIT을 넘어 보존 .-> SESS

CURSOR_ID는 클라이언트가 들고 있는 권위 있는 뷰다. 깊은 사본QFILE_LIST_ID(스키마, 튜플 수, head/tail VPID를 서버에 다시 묻지 않고 걸어가기 위해 필요한 모든 메타데이터)와, IO_MAX_PAGE_SIZE 크기로 한 번 malloc한 buffer_area를 함께 들고 있다. 이 버퍼에는 직전 qfile_get_list_file_page가 채워 준 페이지 묶음이 그대로 실린다. 모든 진행 함수(cursor_next_tuple, cursor_prev_tuple, cursor_first_tuple, cursor_last_tuple)는 먼저 현재 페이지 안에서 걸어간다. 페이지 경계를 넘어가는 순간에만 cursor_fetch_page_having_tuple 이 호출되고, 이 함수가 qfile_get_list_file_page로 가는 단일 funnel이다.

// CURSOR_ID — src/query/cursor.h
typedef struct cursor_id CURSOR_ID;
struct cursor_id
{
QUERY_ID query_id; /* server-side query handle */
QFILE_LIST_ID list_id; /* deep copy of the result-stream id */
OID *oid_set; /* prefetch OID buffer (this page) */
MOP *mop_set; /* prefetch MOP buffer (parallel array) */
int oid_ent_count; /* sizeof oid_set / mop_set */
CURSOR_POSITION position; /* C_BEFORE | C_ON | C_AFTER */
VPID current_vpid; /* page currently in `buffer` */
VPID next_vpid; /* unused in current code */
VPID header_vpid; /* head of multi-page network buffer */
int on_overflow; /* big-tuple overflow flag */
int tuple_no; /* absolute tuple index */
QFILE_TUPLE_RECORD tuple_record; /* reassembly buffer for big tuples */
char *buffer; /* current page within buffer_area */
char *buffer_area; /* IO_MAX_PAGE_SIZE bytes from server */
int buffer_filled_size; /* bytes server actually returned */
int buffer_tuple_count; /* tuples on `buffer` */
int current_tuple_no; /* tuple index within `buffer` */
int current_tuple_offset; /* byte offset within `buffer` */
char *current_tuple_p; /* pointer to current tuple bytes */
int *oid_col_no; /* additional OID-bearing columns */
int current_tuple_length;
int oid_col_no_cnt;
DB_FETCH_MODE prefetch_lock_mode; /* lock mode for prefetched objects */
int current_tuple_value_index; /* memo for repeated cursor_get_tuple_value */
char *current_tuple_value_p;
bool is_updatable;
bool is_oid_included; /* first tuple value is hidden OID */
bool is_copy_tuple_value; /* true = copy DB_VALUE, false = peek */
};

이 struct 안에는 네 가지 관심사가 섞여 있다.

  • 식별자(Identity) — query_idlist_id. query_id는 네트워크 프로토콜이 사용하는 서버 측 핸들이고, list_id는 타입 리스트와 페이지 체인의 head/tail을 포함한 깊은 사본이다. 이 사본 덕분에 클라이언트는 forward/backward 진행을 서버에 묻지 않고 미리 계획할 수 있다.
  • 네트워크 버퍼 — buffer_area, buffer, buffer_filled_size, header_vpid. 클라이언트는 cursor_open 시점에 IO_MAX_PAGE_SIZE 바이트를 한 번 malloc한다. 한 번의 qfile_get_list_file_page 라운드트립이 여러 DB_PAGESIZE 페이지를 그 버퍼에 채워 줄 수 있다(서버는 들어가는 만큼 연속한 페이지를 patch해서 보낸다). 이 버퍼가 곧 로컬 페이지 캐시고, 아래 위치 필드는 그 버퍼 안의 오프셋이다.
  • 위치 상태 — position, tuple_no, current_vpid, current_tuple_no, current_tuple_offset, current_tuple_p, current_tuple_length. 커서 상태 머신은 이 필드들을 한꺼번에 맞물려 움직인다. position은 매크로 상태(모든 행 이전, 한 행 위, 모든 행 이후)이고, 나머지는 마이크로 상태(어느 페이지, 그 페이지 안 어떤 튜플, 그 튜플의 어느 바이트)다.
  • per-fetch 디코더 메모 — current_tuple_value_indexcurrent_tuple_value_p는 같은 튜플 위에서 cursor_get_tuple_value (idx)가 반복 호출될 때 직전 디코딩이 어디까지 갔는지 기억한다. 덕분에 컬럼 0..N을 순차로 읽는 보행은 O(N^2)이 아니라 O(N)이 된다.

세 개의 플래그가 커서의 모드를 못 박아 둔다.

  • is_oid_included — 결과가 updatable cursor 용도로 열렸기 때문에 실행기가 매 튜플 앞에 행의 OID를 운반하는 hidden 첫 컬럼을 박아 넣었다. cursor_get_current_oid가 이 컬럼을 읽고, db_query_get_tuple_value는 사용자 가시 컬럼 인덱스를 1만큼 뒤로 민다.
  • is_updatable — 호출자가 cursor_open을 부를 때 update 의미론을 요청했다. 현 시점에서 이 플래그가 통제하는 건 cursor_set_oid_columns 거부 여부뿐이다. is_updatable이 켜져 있으면 거부된다(hidden-OID 경로가 우선이기 때문).
  • is_copy_tuple_valuecursor_get_tuple_valuepr_data_readval (..., copy=true) (새로 할당된 DB_VALUE로 디코딩) 를 쓸지, pr_data_readval (..., copy=false) (DB_VALUE가 커서 네트워크 버퍼를 직접 가리키며, 다음 페이지 fetch 전까지만 유효) 를 쓸지를 결정한다. 기본은 true다. 브로커는 값을 즉시 다시 와이어로 인코딩할 것을 알 때 false로 뒤집는다.
stateDiagram-v2
  [*]      --> CLOSED : (메모리 미초기화)
  CLOSED   --> OPEN_BEFORE : cursor_open \n list_id deep-copy, buffer_area malloc
  OPEN_BEFORE --> OPEN_BEFORE : 결과에 행이 없음
  OPEN_BEFORE --> ON_ROW    : cursor_next_tuple \n 또는 cursor_first_tuple
  ON_ROW   --> ON_ROW       : cursor_next_tuple \n 페이지 안 또는 페이지 경계
  ON_ROW   --> ON_ROW       : cursor_prev_tuple
  ON_ROW   --> AFTER_LAST   : cursor_next_tuple로 마지막 통과
  AFTER_LAST --> ON_ROW     : cursor_prev_tuple \n last_vpid의 LAST_TPL로 점프
  AFTER_LAST --> AFTER_LAST : cursor_next_tuple
  ON_ROW   --> ON_ROW       : cursor_first_tuple \n cursor_last_tuple
  ON_ROW   --> CLOSED       : cursor_close (list_id 사본 + buffer_area 해제)
  AFTER_LAST --> CLOSED     : cursor_close
  OPEN_BEFORE --> CLOSED    : cursor_close

세 매크로 상태(C_BEFORE, C_ON, C_AFTER)는 cursor.hCURSOR_POSITION enum에 그대로 살아 있다. 전이는 SQL/CLI 커서 의미론과 일치한다. 갓 open된 커서는 첫 행 이전 에 놓이고, 성공적인 next/first가 그것을 한 행 위 로 올려 두며, 끝을 넘어가면 이후 로 떨어진다. 다시 돌아가는 길은 cursor_prev_tuple 또는 cursor_last_tuple 둘뿐이다(후자는 C_AFTER를 마지막 페이지의 LAST_TPL로 점프시키는 special-case 처리를 한다).

cursor_open — 깊은 사본과 네트워크 버퍼 할당

섹션 제목: “cursor_open — 깊은 사본과 네트워크 버퍼 할당”
// cursor_open — src/query/cursor.c (condensed)
bool
cursor_open (CURSOR_ID * cursor_id_p, QFILE_LIST_ID * list_id_p,
bool updatable, bool is_oid_included)
{
static QFILE_LIST_ID empty_list_id;
QFILE_CLEAR_LIST_ID (&empty_list_id);
cursor_id_p->is_updatable = updatable;
cursor_id_p->is_oid_included = is_oid_included;
cursor_id_p->position = C_BEFORE;
cursor_id_p->tuple_no = -1;
VPID_SET_NULL (&cursor_id_p->current_vpid);
/* ... condensed: more zeroing ... */
cursor_id_p->is_copy_tuple_value = true;
if (cursor_copy_list_id (&cursor_id_p->list_id, list_id_p) != NO_ERROR)
return false;
cursor_id_p->query_id = list_id_p->query_id;
if (cursor_id_p->list_id.type_list.type_cnt)
{
cursor_id_p->buffer_area = (char *) malloc (CURSOR_BUFFER_AREA_SIZE);
cursor_id_p->buffer = cursor_id_p->buffer_area;
if (is_oid_included)
cursor_allocate_oid_buffer (cursor_id_p);
}
return true;
}

두 가지 사실이 강조할 만하다. 첫째, cursor_copy_list_id타입 리스트의 deep copy(새로운 type_list.domp 배열을 할당해 원본을 memcpy)와 페이지 체인 head/tail VPID의 shallow clone을 동시에 한다. 원본에 last_pgptr가 있으면 이를 새 malloc 버퍼에 복사한다. 이 과정이 끝나면 커서의 list_id는 원본과 독립적이다. 원본을 free해도 커서는 멀쩡하다.

// cursor_copy_list_id — src/query/cursor.c (condensed)
int
cursor_copy_list_id (QFILE_LIST_ID * dest_list_id_p,
const QFILE_LIST_ID * src_list_id_p)
{
memcpy (dest_list_id_p, src_list_id_p, DB_SIZEOF (QFILE_LIST_ID));
dest_list_id_p->type_list.domp = NULL;
if (src_list_id_p->type_list.type_cnt)
{
size_t size = src_list_id_p->type_list.type_cnt * sizeof (TP_DOMAIN *);
dest_list_id_p->type_list.domp = (TP_DOMAIN **) malloc (size);
memcpy (dest_list_id_p->type_list.domp,
src_list_id_p->type_list.domp, size);
}
dest_list_id_p->tpl_descr.f_valp = NULL;
dest_list_id_p->sort_list = NULL; /* never used at crs_ level */
if (src_list_id_p->last_pgptr)
{
dest_list_id_p->last_pgptr = (PAGE_PTR) malloc (CURSOR_BUFFER_SIZE);
memcpy (dest_list_id_p->last_pgptr,
src_list_id_p->last_pgptr, CURSOR_BUFFER_SIZE);
}
return NO_ERROR;
}

둘째, 커서의 buffer_areaDB_PAGESIZE가 아니라 IO_MAX_PAGE_SIZE 다. 동기는 서버 측 xqfile_get_list_file_page에 있다. 이 함수는 네트워크 페이지가 꽉 찰 때까지 페이지들을 이어 붙인다. 즉 연속된 list-file 페이지 여러 개를 한 번의 와이어 응답에 패킹해서 한 라운드트립으로 여러 DB_PAGESIZE 청크를 운반한다. 클라이언트는 최대 IO_MAX_PAGE_SIZE 바이트를 받을 준비가 되어 있어야 하고, overflow-vpid / next-vpid 헤더를 보면서 그 버퍼 안에서 각 페이지의 경계를 찾아 걸어가야 한다.

// cursor_next_tuple — src/query/cursor.c (condensed)
int
cursor_next_tuple (CURSOR_ID * cursor_id_p)
{
cursor_initialize_current_tuple_value_position (cursor_id_p);
if (cursor_id_p->position == C_BEFORE)
{
if (VPID_ISNULL (&(cursor_id_p->list_id.first_vpid)))
return DB_CURSOR_END;
if (cursor_fetch_page_having_tuple (cursor_id_p,
&cursor_id_p->list_id.first_vpid, FIRST_TPL, 0) != NO_ERROR)
return DB_CURSOR_ERROR;
QFILE_COPY_VPID (&cursor_id_p->current_vpid,
&cursor_id_p->list_id.first_vpid);
cursor_id_p->position = C_ON;
cursor_id_p->tuple_no = -1;
cursor_id_p->current_tuple_no = -1;
cursor_id_p->current_tuple_length = 0;
/* fall through into the C_ON branch */
}
if (cursor_id_p->position == C_ON)
{
VPID next_vpid;
if (cursor_id_p->current_tuple_no < cursor_id_p->buffer_tuple_count - 1)
{
/* fast path: still in the same page, walk forward */
cursor_id_p->tuple_no++;
cursor_id_p->current_tuple_no++;
cursor_id_p->current_tuple_offset += cursor_id_p->current_tuple_length;
cursor_id_p->current_tuple_p += cursor_id_p->current_tuple_length;
cursor_id_p->current_tuple_length =
QFILE_GET_TUPLE_LENGTH (cursor_id_p->current_tuple_p);
}
else if (QFILE_GET_NEXT_PAGE_ID (cursor_id_p->buffer) != NULL_PAGEID)
{
/* slow path: cross page boundary, fetch next page */
QFILE_GET_NEXT_VPID (&next_vpid, cursor_id_p->buffer);
if (cursor_fetch_page_having_tuple (cursor_id_p, &next_vpid,
FIRST_TPL, 0) != NO_ERROR)
return DB_CURSOR_ERROR;
QFILE_COPY_VPID (&cursor_id_p->current_vpid, &next_vpid);
cursor_id_p->tuple_no++;
}
else
{
cursor_id_p->position = C_AFTER;
cursor_id_p->tuple_no = cursor_id_p->list_id.tuple_cnt;
return DB_CURSOR_END;
}
}
else if (cursor_id_p->position == C_AFTER)
return DB_CURSOR_END;
return DB_CURSOR_SUCCESS;
}

모양은 교과서적인 fast/slow split 위치 커서다.

  • Fast path (현재 페이지 안). 순수 포인터 산술이다. 튜플별 길이 prefix가 얼마나 전진할지 알려 주고, 페이지 헤더의 튜플 카운트가 언제 멈출지 알려 준다. 라운드트립 0회, 할당 0회, 튜플당 약 10 명령어.
  • Slow path (페이지 경계 통과). 다음 VPID로 cursor_fetch_page_having_tuple 을 호출한다. 이 호출은 로컬 네트워크 버퍼 캐시에 hit할 수도(서버가 여러 페이지를 한 응답에 패킹했고 그 중 다음 VPID가 들어 있다면), 아니면 새로운 qfile_get_list_file_page 라운드트립을 트리거할 수도 있다.

역방향 함수 cursor_prev_tuple은 대칭이다. CUBRID가 모든 튜플 앞에 써 두는 prev_tuple_length prefix와 모든 페이지 헤더의 prev_pgid 필드를 활용한다(페이지 포맷은 cubrid-list-file.md 참고). 매크로 QFILE_GET_PREV_TUPLE_LENGTHQFILE_GET_PREV_PAGE_ID가 한 번의 산술 연산으로 역방향 보행을 실현한다.

페이지 fetch funnel — cursor_fetch_page_having_tuple

섹션 제목: “페이지 fetch funnel — cursor_fetch_page_having_tuple”

페이지 경계를 넘는 모든 위치 변경은 한 함수로 funnel된다.

// cursor_fetch_page_having_tuple — src/query/cursor.c (condensed)
int
cursor_fetch_page_having_tuple (CURSOR_ID * cursor_id_p,
VPID * vpid_p, int position, int offset)
{
cursor_initialize_current_tuple_value_position (cursor_id_p);
if (!VPID_EQ (&(cursor_id_p->current_vpid), vpid_p))
if (cursor_buffer_last_page (cursor_id_p, vpid_p) != NO_ERROR)
return ER_FAILED;
if (cursor_id_p->buffer == NULL) return ER_FAILED;
if (cursor_point_current_tuple (cursor_id_p, position, offset) != NO_ERROR)
return ER_FAILED;
if (QFILE_GET_OVERFLOW_PAGE_ID (cursor_id_p->buffer) != NULL_PAGEID)
{
if (cursor_construct_tuple_from_overflow_pages (cursor_id_p, vpid_p)
!= NO_ERROR)
return ER_FAILED;
}
else
cursor_id_p->current_tuple_p =
cursor_id_p->buffer + cursor_id_p->current_tuple_offset;
if (cursor_id_p->buffer_tuple_count < 2)
return NO_ERROR;
if (cursor_has_first_hidden_oid (cursor_id_p))
return cursor_prefetch_first_hidden_oid (cursor_id_p);
else if (cursor_id_p->oid_col_no && cursor_id_p->oid_col_no_cnt)
return cursor_prefetch_column_oids (cursor_id_p);
return NO_ERROR;
}

다섯 가지 일을 한다.

  1. 요청된 VPID가 이미 로컬 네트워크 버퍼 안에 있으면(직전 라운드트립이 그것을 함께 패킹했기 때문에) 그것을 재사용한다.
  2. 그렇지 않으면 cursor_buffer_last_page를 부른다. 이 함수는 SA-mode 에서는 writer의 last_pgptr(실행기와 커서가 같은 주소 공간을 공유하는 경우)을 가리키게 하고, CS-mode에서는 cursor_get_list_file_page 를 호출하는데 이게 실제 네트워크 호출이다.
  3. cursor_point_current_tuple을 호출해 페이지 상대 위치 (current_tuple_no, current_tuple_offset, current_tuple_length) 를 다시 잡는다. position 인자는 FIRST_TPL = -1, LAST_TPL = -2, 또는 그냥 튜플 인덱스 정수다.
  4. 해당 튜플이 overflow 청크가 있다면(QFILE_GET_OVERFLOW_PAGE_ID (buffer) != NULL_PAGEID), overflow 체인을 걸어가며 청크들을 tuple_record.tpl(커서가 들고 있는 malloc된 reassembly 버퍼)에 복사해 큰 튜플 전체를 다시 이어 붙인다. 그렇지 않으면 현재 튜플은 네트워크 버퍼 안에 그대로 남는다.
  5. 벡터 OID prefetch. 결과에 hidden OID 컬럼이 있거나(질의가 updatable이라 실행기가 컬럼 0에 OID를 박아 둔 경우), 호출자가 cursor_set_oid_columns로 추가 OID-운반 컬럼을 등록해 두었다면, 페이지 전체를 한 바퀴 돌며 OID들을 oid_set에 모은 뒤 한 번의 locator_fetch_set 호출로 모두 워크스페이스로 가져온다. 이게 페이지 단위(page-grain) 최적화 다. 행마다 한 번씩 locator_fetch_object 라운드트립을 무는 대신, 페이지 경계마다 정확히 한 번 batch fetch가 일어난다. buffer_tuple_count < 2인 페이지(튜플 1개짜리)는 batch할 가치가 없으므로 gate된다.

페이지 운반 자체는 클라이언트 쪽에서는 한 번의 네트워크 라운드트립이다.

// qfile_get_list_file_page (client-side stub) — src/communication/network_interface_cl.c
int
qfile_get_list_file_page (QUERY_ID query_id, VOLID volid, PAGEID pageid,
char *buffer, int *buffer_size)
{
/* ... pack request, send NET_SERVER_LS_GET_LIST_FILE_PAGE ... */
return net_client_request2_no_malloc (
NET_SERVER_LS_GET_LIST_FILE_PAGE, request, sizeof (request),
reply, sizeof (reply), NULL, 0, buffer, buffer_size);
}

서버 측 핸들러(list_file.c 안)는 그것의 amplifier다.

// xqfile_get_list_file_page — src/query/list_file.c (condensed)
int
xqfile_get_list_file_page (THREAD_ENTRY * thread_p, QUERY_ID query_id,
VOLID vol_id, PAGEID page_id,
char *page_buf_p, int *page_size_p)
{
/* ... resolve query_id → QMGR_QUERY_ENTRY → QFILE_LIST_ID → QMGR_TEMP_FILE ... */
get_page:
/* append pages until a network page is full */
while ((*page_size_p + DB_PAGESIZE) <= IO_MAX_PAGE_SIZE)
{
page_p = qmgr_get_old_page (thread_p, &vpid, tfile_vfid_p);
QFILE_GET_OVERFLOW_VPID (&next_vpid, page_p);
if (next_vpid.pageid == NULL_PAGEID)
QFILE_GET_NEXT_VPID (&next_vpid, page_p);
/* trim trailing zero-bytes if this is a regular page */
if (QFILE_GET_TUPLE_COUNT (page_p) == QFILE_OVERFLOW_TUPLE_COUNT_FLAG
|| QFILE_GET_OVERFLOW_PAGE_ID (page_p) != NULL_PAGEID)
one_page_size = DB_PAGESIZE;
else
one_page_size = (QFILE_GET_LAST_TUPLE_OFFSET (page_p)
+ QFILE_GET_TUPLE_LENGTH (page_p
+ QFILE_GET_LAST_TUPLE_OFFSET (page_p)));
memcpy (page_buf_p + *page_size_p, page_p, one_page_size);
qmgr_free_old_page_and_init (thread_p, page_p, tfile_vfid_p);
*page_size_p += one_page_size;
VPID_COPY (&vpid, &next_vpid);
if (VPID_ISNULL (&vpid)) break;
}
return NO_ERROR;
}

중요한 두 가지 동작은 다중 페이지 패킹(while 루프) 과 마지막-튜플까지만 복사(one_page_size 계산) 다. 16KB 물리 페이지 안에 3KB의 튜플만 차 있는 페이지라면 3KB만 와이어로 보내면 된다. 나머지 빈 꼬리는 memcpy 전에 잘려 나간다. 트레이드오프는 overflow 케이스다. 후속 청크는 일반 튜플이 아니므로 페이지 전체를 그대로 복사해야 한다.

튜플 디코딩 — cursor_get_tuple_value

섹션 제목: “튜플 디코딩 — cursor_get_tuple_value”

페이지 위 튜플 포맷은 cubrid-list-file.md가 설명한 list-file의 길이 prefix 박힌 packed row 그대로다.

[ tuple_length (4) | prev_tuple_length (4) | val0 | val1 | ... ]
[ flag (4) | val_len (4) | <packed bytes, MAX_ALIGNMENT-padded> ]

커서 측 디코더는 cursor_get_tuple_value다.

// cursor_get_tuple_value — src/query/cursor.c (condensed)
int
cursor_get_tuple_value (CURSOR_ID * cursor_id_p, int index, DB_VALUE * value_p)
{
if (cursor_id_p->is_oid_included == true)
index++; /* shift past the hidden first column */
char *tuple_p = cursor_peek_tuple (cursor_id_p);
if (tuple_p == NULL) return ER_FAILED;
return cursor_get_tuple_value_from_list (cursor_id_p, index, value_p, tuple_p);
}

cursor_peek_tuple은 캐시된 current_tuple_p를 반환한다(position != C_ON이면 에러). 실제 디코딩은 cursor_get_tuple_value_from_list 가 한다.

// cursor_get_tuple_value_from_list — src/query/cursor.c (condensed)
static int
cursor_get_tuple_value_from_list (CURSOR_ID * cursor_id_p, int index,
DB_VALUE * value_p, char *tuple_p)
{
QFILE_TUPLE_VALUE_TYPE_LIST *type_list_p = &cursor_id_p->list_id.type_list;
OR_BUF buffer;
or_init (&buffer, tuple_p, QFILE_GET_TUPLE_LENGTH (tuple_p));
/* fast path: previous call left us pointing at column k, k <= index */
int i;
if (cursor_id_p->current_tuple_value_index >= 0
&& cursor_id_p->current_tuple_value_index <= index
&& cursor_id_p->current_tuple_value_p != NULL)
{
i = cursor_id_p->current_tuple_value_index;
tuple_p = cursor_id_p->current_tuple_value_p;
}
else
{
i = 0;
tuple_p += QFILE_TUPLE_LENGTH_SIZE;
}
for (; i < index; i++)
tuple_p += (QFILE_TUPLE_VALUE_HEADER_SIZE
+ QFILE_GET_TUPLE_VALUE_LENGTH (tuple_p));
cursor_id_p->current_tuple_value_index = i;
cursor_id_p->current_tuple_value_p = tuple_p;
QFILE_TUPLE_VALUE_FLAG flag = QFILE_GET_TUPLE_VALUE_FLAG (tuple_p);
tuple_p += QFILE_TUPLE_VALUE_HEADER_SIZE;
buffer.ptr = tuple_p;
return cursor_get_tuple_value_to_dbvalue (&buffer, type_list_p->domp[i],
flag, value_p,
cursor_id_p->is_copy_tuple_value);
}

current_tuple_value_index / current_tuple_value_p 안의 forward-walking 메모 덕분에 한 행에서 컬럼을 순차로 긁어 가는 호출 시퀀스 (get_value(0); get_value(1); ...; get_value(N-1))는 총 byte-skip 비용이 O(N^2)이 아니라 O(N)이 된다. 직전 호출이 컬럼 k에서 끝났고 이번 호출이 컬럼 k+1을 요구하면 루프는 0이 아니라 k 에서 시작한다. 메모는 모든 위치 변경에서 무효화된다. cursor_initialize_current_tuple_value_positioncursor_next_tuple, cursor_prev_tuple, cursor_fetch_page_having_tuple 모두에서 호출된다.

실제 byte → DB_VALUE 변환은 primitive 타입의 data_readval로 위임된다.

// cursor_get_tuple_value_to_dbvalue — src/query/cursor.c (condensed)
static int
cursor_get_tuple_value_to_dbvalue (OR_BUF * buffer_p, TP_DOMAIN * domain_p,
QFILE_TUPLE_VALUE_FLAG value_flag,
DB_VALUE * value_p, bool is_copy)
{
const PR_TYPE *pr_type = domain_p->type;
if (value_flag == V_UNBOUND)
{
db_value_domain_init (value_p, pr_type->id, domain_p->precision,
domain_p->scale);
return NO_ERROR; /* SQL NULL */
}
if (pr_type->id == DB_TYPE_VOBJ)
return cursor_copy_vobj_to_dbvalue (buffer_p, value_p);
if (pr_type->data_readval (buffer_p, value_p, domain_p, -1, is_copy,
NULL, 0) != NO_ERROR)
return ER_FAILED;
return cursor_fixup_vobjs (value_p);
}

cursor_fixup_vobjs는 디코딩 후 후처리 hook이다. OID를 운반하는 값을 MOP(managed-object pointer — locator/MOP 모듈 참고)로 바꿔 준다. DB_TYPE_OIDvid_oid_to_object를 거쳐 DB_TYPE_OBJECT가 되고, DB_TYPE_VOBJvid_vobj_to_object로 vmop이 되며, set/sequence/multiset 이 OID/VOBJ를 담고 있으면 안을 재귀적으로 걸어가 같은 식으로 fixup된다. 이 hook이 없으면 커서는 애플리케이션에 raw OID를 건네게 된다. 애플리케이션 코드는 워크스페이스 entry가 이미 채워진 객체 핸들을 받기를 기대하기 때문에, 이 hook이 그 계약을 떠받친다.

Updatable 커서 — hidden OID 컬럼과 OID prefetch

섹션 제목: “Updatable 커서 — hidden OID 컬럼과 OID prefetch”

cursor_open(... is_oid_included=true)로 열린 커서는, 실행기가 매 튜플 앞에 그 행의 OID(또는 view row면 VOBJ)를 운반하는 hidden 첫 컬럼을 박아 둔 결과를 본다. 이게 UPDATE WHERE CURRENT OF를 가능하게 만든다. 커서가 어느 행을 update의 대상으로 가리킬지 안다.

cursor_get_current_oid가 그 컬럼을 읽는다.

// cursor_get_current_oid — src/query/cursor.c
int
cursor_get_current_oid (CURSOR_ID * cursor_id_p, DB_VALUE * value_p)
{
assert (cursor_id_p->is_oid_included == true);
char *tuple_p = cursor_peek_tuple (cursor_id_p);
if (tuple_p == NULL) return ER_FAILED;
return cursor_get_first_tuple_value (tuple_p,
&cursor_id_p->list_id.type_list,
value_p,
cursor_id_p->is_copy_tuple_value);
}

사용자 가시 측 부수 효과는 cursor_get_tuple_value(idx)가 hidden 컬럼을 건너뛰기 위해 idx를 1만큼 뒤로 민다는 것뿐이다.

hidden OID 주변의 더 큰 최적화는 페이지 단위 벡터 prefetch 다. 페이지를 fetch할 때마다 cursor_fetch_page_having_tuplecursor_prefetch_first_hidden_oid(또는 호출자가 추가 컬럼을 등록한 경우 _column_oids) 를 호출한다.

// cursor_prefetch_first_hidden_oid — src/query/cursor.c (condensed)
static int
cursor_prefetch_first_hidden_oid (CURSOR_ID * cursor_id_p)
{
int tuple_count = QFILE_GET_TUPLE_COUNT (cursor_id_p->buffer);
QFILE_TUPLE current_tuple = cursor_id_p->buffer + QFILE_PAGE_HEADER_SIZE;
int oid_index = 0;
for (int i = 0; i < tuple_count; i++)
{
int current_tuple_length = QFILE_GET_TUPLE_LENGTH (current_tuple);
DB_TYPE type = TP_DOMAIN_TYPE (cursor_id_p->list_id.type_list.domp[0]);
char *tuple_p = (char *) current_tuple + QFILE_TUPLE_LENGTH_SIZE;
if (QFILE_GET_TUPLE_VALUE_FLAG (tuple_p) != V_BOUND)
{ current_tuple = tuple_p + current_tuple_length; continue; }
OID *current_oid_p = cursor_get_oid_from_tuple (tuple_p, type);
if (current_oid_p && oid_index < cursor_id_p->oid_ent_count)
{
COPY_OID (&cursor_id_p->oid_set[oid_index], current_oid_p);
oid_index++;
}
current_tuple = (char *) current_tuple + current_tuple_length;
}
return cursor_fetch_oids (cursor_id_p, oid_index,
cursor_id_p->prefetch_lock_mode,
(cursor_id_p->prefetch_lock_mode == DB_FETCH_WRITE)
? DB_FETCH_QUERY_WRITE : DB_FETCH_QUERY_READ);
}

세 가지 동작이 일어난다.

  1. 페이지의 모든 튜플을 한 번 걸어가며 첫 컬럼의 OID를 oid_set에 모은다.
  2. cursor_fetch_oidslocator_fetch_set(또는 OID가 정확히 1개면 locator_fetch_object)를 호출한다. 이게 locator-manager의 batch fetch primitive다. 이렇게 페이지 단위로 한 번의 네트워크 라운드트립 이 일어나면, 행마다 한 번씩 locator_fetch_object를 무는 것보다 훨씬 싸다.
  3. 락 모드는 호출자가 cursor_set_prefetch_lock_mode로 등록한 값이다. 커서가 SELECT ... FOR UPDATE로 열려 있으면 브로커/드라이버가 이 값을 DB_FETCH_WRITE로 뒤집어 prefetch가 X 락을 잡게 한다. 기본은 DB_FETCH_READ다.

oid_setmop_set은 병렬 배열이며, cursor_open 시점에 CEIL_PTVDIV(DB_PAGESIZE, sizeof(OID)) - 1로 사이즈된다(한 페이지에 들어갈 수 있는 최대 OID 수, 페이지 헤더 자리 1개 빼고). 둘 중 어느 한 쪽의 할당이 실패해도 치명적이지 않다. cursor_allocate_oid_bufferoid_ent_count를 0으로 두고 prefetch는 조용히 건너뛰어진다.

Holdability — COMMIT을 넘어 살아남기

섹션 제목: “Holdability — COMMIT을 넘어 살아남기”

holdability는 커서 모듈에서 가장 미묘한 관심사이며, 커서 추상이 엔진의 다른 부분 깊숙이까지 뻗어 들어가는 유일한 지점이다. 데이터 흐름은 다음과 같다.

sequenceDiagram
  autonumber
  participant CL  as 클라이언트 (브로커 / 앱)
  participant QM  as Query Manager
  participant TR  as 트랜잭션
  participant SE  as 세션

  CL->>QM: prepare/execute (RESULT_HOLDABLE 플래그)
  QM->>QM: query_p->is_holdable = true
  Note right of QM: 결과 list-file 생성, 튜플 기록
  CL->>TR: COMMIT
  TR->>QM: qmgr_clear_trans_wakeup(tran_index, is_abort=false)
  loop tran_entry_p->query_entry_list_p의 각 query마다
    QM->>QM: if query_p->is_holdable && !is_abort
    QM->>SE: xsession_store_query_entry_info (query_p)
    SE->>SE: qentry_to_sentry — list_id/temp_vfid 포인터 이전<br/>session_preserve_temporary_files — file_temp_preserve
    SE->>SE: SESSION_QUERY_ENTRY를 state_p->queries 앞에 prepend
    QM->>QM: query_p->list_id = NULL, temp_vfid = NULL
    QM->>QM: QMGR_QUERY_ENTRY 해제
  end
  Note over CL: COMMIT 반환; 커서는 여전히 query_id 보유
  CL->>QM: cursor_next_tuple → qfile_get_list_file_page (query_id)
  QM->>QM: qmgr_get_query_entry는 트랜잭션 테이블에서 못 찾음
  QM->>SE: xsession_load_query_entry_info (query_id)
  SE->>QM: sentry_to_qentry — QMGR_QUERY_ENTRY 재구성
  QM->>CL: 페이지 응답

핵심 invariant는 서버 측 QFILE_LIST_ID와 그것을 받치는 FILE_TEMP가 COMMIT을 넘어 살아남는다 는 것이다. 이유는 두 가지다.

  • query manager의 qmgr_clear_trans_wakeup(트랜잭션 종료 시점에 호출됨)이 is_holdable && !is_abort를 감지하면, list-file을 파괴하는 대신 소유권을 옮긴다. list_idtemp_vfid 포인터를 트랜잭션 범위의 QMGR_QUERY_ENTRY에서 새로 만든 세션 범위 SESSION_QUERY_ENTRY로 이전한다. query manager가 들고 있던 사본은 null로 바뀌고(query_p->list_id = NULL; query_p->temp_vfid = NULL;), 뒤이은 qfile_close_list / qmgr_free_query_temp_file_helper는 holdable 경로에서 no-op이 된다.
  • session_preserve_temporary_files는 임시 파일 체인을 따라 걸어가며 매 FILE_TEMP마다 file_temp_preserve를 호출한다. 이로써 file manager의 트랜잭션 종료 청소(file_tempcache_drop_tran)가 그 파일들을 건너뛴다.
// qmgr_clear_trans_wakeup — src/query/query_manager.c (the holdable branch, condensed)
if (query_p->is_holdable)
{
if (is_abort || is_tran_died)
xsession_clear_query_entry_info (thread_p, query_p->query_id);
else
{
xsession_store_query_entry_info (thread_p, query_p);
query_p->list_id = NULL;
query_p->temp_vfid = NULL;
}
}
/* fall-through: destroy whatever pointers remain (NULL for holdable+commit) */
if (query_p->list_id)
{
qfile_close_list (thread_p, query_p->list_id);
QFILE_FREE_AND_INIT_LIST_ID (query_p->list_id);
}
if (query_p->temp_vfid != NULL)
(void) qmgr_free_query_temp_file_helper (thread_p, query_p);
// session_store_query_entry_info — src/session/session.c (condensed)
void
session_store_query_entry_info (THREAD_ENTRY * thread_p,
QMGR_QUERY_ENTRY * qentry_p)
{
SESSION_STATE *state_p = session_get_session_state (thread_p);
if (state_p == NULL) return;
for (SESSION_QUERY_ENTRY *current = state_p->queries; current; current = current->next)
if (current->query_id == qentry_p->query_id)
{
/* idempotent — caller is in qmgr_clear_trans_wakeup, will null these */
qentry_p->list_id = NULL;
qentry_p->temp_vfid = NULL;
return;
}
SESSION_QUERY_ENTRY *sqentry_p = qentry_to_sentry (qentry_p);
/* qentry_to_sentry STEALS list_id and temp_file from qentry_p, nulling them */
session_preserve_temporary_files (thread_p, sqentry_p);
sqentry_p->next = state_p->queries;
state_p->queries = sqentry_p;
sessions.num_holdable_cursors++;
}

프로토콜의 후반부 — COMMIT 이후의 fetch — 는 qmgr_get_query_entry 에 들어 있다.

// qmgr_get_query_entry — src/query/query_manager.c (condensed)
QMGR_QUERY_ENTRY *
qmgr_get_query_entry (THREAD_ENTRY * thread_p, QUERY_ID query_id, int tran_index)
{
/* normal path: look up in this transaction's list */
pthread_mutex_lock (&tran_entry_p->mutex);
query_p = qmgr_find_query_entry (tran_entry_p->query_entry_list_p, query_id);
pthread_mutex_unlock (&tran_entry_p->mutex);
if (query_p != NULL) return query_p;
/* fallback: maybe it's a holdable result on the session */
query_p = qmgr_allocate_query_entry (thread_p, tran_entry_p);
query_p->query_id = query_id;
if (xsession_load_query_entry_info (thread_p, query_p) != NO_ERROR)
{
qmgr_free_query_entry (thread_p, tran_entry_p, query_p);
return NULL;
}
qmgr_add_query_entry (thread_p, query_p, tran_index);
return query_p;
}

COMMIT 이후의 첫 트랜잭션 fetch는 새 트랜잭션의 query-entry 리스트에서 해당 query_id를 찾지 못하고, fallback으로 떨어져 xsession_load_query_entry_info를 호출한다. 이 호출은 list-file 포인터를 SESSION_QUERY_ENTRY에서 갓 할당된 QMGR_QUERY_ENTRY로 복사하고, 그 entry를 새 트랜잭션에 매단 뒤, 페이지 응답을 이어 간다. 클라이언트 측 커서는 이 모든 일을 알지 못한다. cursor_next_tuple은 같은 호출을 계속 했고, 그 안의 qfile_get_list_file_page (query_id)도 같은 호출을 계속 했다. 유일한 전이 비용은 세션의 holdable list를 한 번 O(N) 걸어가는 것이며, 이는 MAX_HOLDABLE_CURSORS_COUNT로 상한이 잡혀 있다.

브로커의 역할 — RESULT_HOLDABLE 플래그

섹션 제목: “브로커의 역할 — RESULT_HOLDABLE 플래그”

이 모든 프로토콜을 구동하는 플래그는 JDBC/CCI 클라이언트가 요청한 바를 토대로 브로커(CAS) 계층에서 세팅된다.

// cas_execute.c (condensed; multiple call sites)
if (jdbc_holdable_request)
srv_handle->is_holdable = true;
db_session_set_holdable ((DB_SESSION *) srv_handle->session,
srv_handle->is_holdable);
/* ... later, after execute ... */
if (srv_handle->is_holdable == true)
{
srv_handle->q_result->is_holdable = true;
as_info->num_holdable_results++;
}

db_session_set_holdable은 그 비트를 세션의 prepared-statement 상태로 전파하고, db_execute_and_keep_statement가 서버로 보내는 QUERY_FLAGRESULT_HOLDABLE을 OR한다. 서버의 xqmgr_execute_query가 그것을 읽는다.

// (paraphrased) — query_manager.c
if (*flag_p & RESULT_HOLDABLE)
query_p->is_holdable = true;
else
query_p->is_holdable = false;

CAS 카운터 as_info->num_holdable_results는 이 연결에서 holdable인 커서가 몇 개인지에 대한 브로커 측 뷰다. 세션 단위에서 서버의 sessions.num_holdable_cursors와 일치한다(브로커 재시작과 연결 단절 같은 가장자리 케이스를 제외하면 — 미해결 질문 참고).

대부분의 호출자는 CURSOR_ID를 직접 만지지 않는다. 두 개의 주요 wrapper가 있다.

  • DB_QUERY_RESULT (compat/db_query.h). T_SELECT 타입 결과는 안쪽 res.s.cursor_id에 커서를 값으로 들고 있고, db_query_* 계열(db_query_first_tuple, db_query_next_tuple, db_query_seek_tuple 등)을 노출해 대응되는 cursor_* 호출로 위임한다. db_query_seek_tuple은 절대/상대/끝-상대 seek을 cursor_next_tuple / cursor_prev_tuple을 반복 호출하며 걸어가는 방식으로 처리한다. 단축 경로로 db_query_get_tplpos / db_query_set_tplpos 쌍이 있어, DB_QUERY_TPLPOS struct ((crs_pos, vpid, tpl_no, tpl_off) — 직전 방문한 튜플로 페이지를 다시 걷지 않고 커서를 다시 앉히는 데 필요한 모든 필드) 를 저장/복원할 수 있다.
  • T_SRV_HANDLE / T_QUERY_RESULT (broker/cas_handle.h). CAS 프로세스의 statement별 상태로, DB_QUERY_RESULT *와 드라이버 측 메타데이터(CAS wire 포맷의 컬럼 타입, prepared 핸들 id, holdability 비트) 를 함께 들고 있다. 브로커의 wire 프로토콜 fetch 핸들러(fn_fetch, fn_get_db_parameter, …) 가 wire 요청을 db_query_seek_tuple / db_query_get_tuple_value 호출로 번역한다.

cursor_free_list_id 매크로에 대한 메모

섹션 제목: “cursor_free_list_id 매크로에 대한 메모”

헤더는 흥미로운 매크로 두 개를 export한다.

cursor.h
#define cursor_free_list_id(list_id) \
do { ... free_and_init the inner pointers ... } while (0)
#define cursor_free_self_list_id(list_id) \
do { cursor_free_list_id (list_id); free_and_init (list_id); } while (0)

이 둘은 qfile_free_list_id의 대칭짝이 아니다. 이것들은 cursor_copy_list_id가 deep copy해서 만든 클라이언트 측 QFILE_LIST_ID 정리용이다. 매크로는 last_pgptr(cursor_copy_list_id 가 fresh로 malloc), tpl_descr.f_valp(클라이언트 측에서는 보통 NULL), sort_list(cursor_copy_list_id에 의해 클라이언트 측에서는 항상 NULL), 그리고 type_list.domp(malloc된 도메인 포인터 배열) 을 해제한다. 커서의 list_id는 포인터가 아니라 값으로 박혀 있기 때문에, cursor_free&cursor_id_p->list_id_self가 붙지 않은 형태를 호출한다.

심볼을 관심사별로 묶어 두었다. 줄번호는 본 updated: 시점의 관측값 이며 시간이 지나면 흐려진다. 심볼 이름을 anchor로 쓴다.

심볼역할
CURSOR_ID (struct)클라이언트 측 핸들 (cursor.h)
CURSOR_POSITION enumC_BEFORE / C_ON / C_AFTER (cursor.h)
cursor_open생성자 — list_id deep-copy, buffer_area 할당, 필요 시 oid_set 할당
cursor_close소멸자 wrapper — cursor_free 호출 후 위치 필드 zeroing
cursor_freedeep-copy된 list_id 안쪽 포인터, buffer_area, tuple_record.tpl, oid_set, mop_set 해제
cursor_copy_list_iddeep copy 루틴(cursor_open이 호출); domp[]last_pgptr을 fresh malloc
cursor_free_list_id 매크로위와 매칭되는 shallow free(cursor_free가 호출) — last_pgptr, tpl_descr.f_valp, sort_list, type_list.domp 해제
cursor_free_self_list_id 매크로소유 버전 — struct 자체도 추가로 free
cursor_allocate_oid_bufferhidden-OID prefetch용 oid_set / mop_set 사이즈 결정 및 할당
cursor_set_oid_columns추가 OID-운반 컬럼 등록; is_oid_included 또는 is_updatable이 이미 켜져 있으면 거부
cursor_set_copy_tuple_valuecursor_get_tuple_value의 copy-vs-peek 토글
cursor_set_prefetch_lock_modecursor_prefetch_*_oids의 락 모드 토글
심볼역할
cursor_next_tuple정방향 fetch; fast path는 buffer 안에서 끝, slow path는 cursor_fetch_page_having_tuple 트리거
cursor_prev_tuple역방향 fetch; 튜플의 prev_tuple_length와 페이지의 prev_pgid 사용
cursor_first_tuplehead로 점프 — list_id.first_vpid fetch, position=FIRST_TPL
cursor_last_tupletail로 점프 — list_id.last_vpid fetch, position=LAST_TPL
cursor_point_current_tupleposition+offset에서 current_tuple_no, current_tuple_offset, current_tuple_length 세팅; FIRST_TPL = -1, LAST_TPL = -2도 인식
cursor_initialize_current_tuple_value_position위치 변경 때마다 per-tuple 디코딩 메모를 무효화
cursor_peek_tuplecurrent_tuple_p 반환 — position != C_ON이면 에러
심볼역할
cursor_fetch_page_having_tuple네트워크 라운드트립으로 가는 단일 funnel; 페이지 fetch + 위치 + overflow reassembly + OID prefetch를 통합
cursor_buffer_last_pageSA-mode에서는 writer의 last_pgptr을 가리키게, 그 외에는 cursor_get_list_file_page를 호출
cursor_get_list_file_page로컬 네트워크 버퍼에서 hit를 찾고; miss면 qfile_get_list_file_page 호출
qfile_get_list_file_page클라이언트 측 네트워크 stub (network_interface_cl.c) — wire 요청 NET_SERVER_LS_GET_LIST_FILE_PAGE
xqfile_get_list_file_page서버 측 핸들러 (list_file.c) — IO_MAX_PAGE_SIZE가 찰 때까지 페이지 패킹
cursor_construct_tuple_from_overflow_pages큰 튜플을 overflow 체인을 따라 tuple_record.tpl로 reassemble
cursor_allocate_tuple_areareassembly 버퍼용 malloc/realloc
심볼역할
cursor_get_tuple_value사용자 노출 디코더; is_oid_included이면 index를 1 shift
cursor_get_tuple_value_list모든 컬럼을 도는 편의 wrapper
cursor_get_tuple_value_from_listskip-ahead 메모를 활용한 컬럼 walker
cursor_get_first_tuple_value컬럼 0 전용 walk(cursor_get_current_oid가 사용)
cursor_get_tuple_value_to_dbvaluepr_type->data_readval(또는 DB_TYPE_VOBJcursor_copy_vobj_to_dbvalue)로 dispatch
cursor_fixup_vobjs디코딩 후 hook — DB_TYPE_OID/DB_TYPE_VOBJDB_TYPE_OBJECT(MOP)로 바꾸고 set 안으로 재귀
cursor_fixup_set_vobjs위의 set/multiset/sequence 변형
cursor_copy_vobj_to_dbvalueDB_TYPE_VOBJ packed 값을 vmop으로 디코딩
심볼역할
cursor_has_first_hidden_oid술어: is_oid_included && oid_ent_count > 0 && type_list.domp[0]이 DB_TYPE_OBJECT
cursor_prefetch_first_hidden_oid첫 컬럼 OID들을 페이지 단위로 모음
cursor_prefetch_column_oidsoid_col_no[]에 등록된 컬럼들에서 OID들을 페이지 단위로 모음
cursor_get_oid_from_tuple튜플에서 단일 OID/VOBJ 값을 읽음
cursor_get_oid_from_vobjVOBJ → base instance unwrap
cursor_fetch_oidslocator_fetch_object(단일) 또는 locator_fetch_set(batch) 호출
cursor_get_current_oidhidden 첫 컬럼의 사용자 노출 reader
심볼파일역할
RESULT_HOLDABLEsrc/query/query_list.h클라이언트가 holdable 커서를 요청하기 위해 세팅하는 wire-protocol 비트
db_session_set_holdablesrc/compat/db_session.c브로커에서 세션으로 holdable 비트 전파
T_SRV_HANDLE::is_holdable / T_QUERY_RESULT::is_holdablesrc/broker/cas_handle.h브로커 측 핸들별 holdable 플래그
as_info->num_holdable_resultssrc/broker/cas_execute.c브로커 측 카운터
QMGR_QUERY_ENTRY::is_holdablesrc/query/query_manager.h서버 측 query별 holdable 플래그
qmgr_clear_trans_wakeupsrc/query/query_manager.c트랜잭션 종료 hook — holdable entry를 세션으로 라우팅
xsession_store_query_entry_infosrc/session/session_sr.csession_store_query_entry_info의 server-entry wrapper
session_store_query_entry_infosrc/session/session.clist-file 소유권을 query manager에서 세션으로 이전
qentry_to_sentrysrc/session/session.clist_id/temp_file 포인터를 steal(원본을 zeroing)
session_preserve_temporary_filessrc/session/session.c모든 backing 임시 파일에 file_temp_preserve 호출
xsession_load_query_entry_infosrc/session/session_sr.csession_load_query_entry_info의 server-entry wrapper
session_load_query_entry_infosrc/session/session.c역방향 — holdable entry를 찾아 포인터를 다시 복사
sentry_to_qentrysrc/session/session.c역방향 복사, 새 query entry에 is_holdable = true를 세팅
qmgr_get_query_entrysrc/query/query_manager.chot-path lookup, holdable fallback 포함
session_remove_query_entry_infosrc/session/session.c커서 close 시 holdable entry 제거
session_remove_query_entry_allsrc/session/session.c연결 단절 시 일괄 제거
심볼파일역할
DB_QUERY_RESULT::res::s::cursor_idsrc/compat/db_query.h결과 셋 wrapper의 임베드 지점
db_query_first_tuple / _last_tuple / _next_tuple / _prev_tuple / _seek_tuplesrc/compat/db_query.ccursor_*로의 얇은 위임자
db_query_get_tplpos / _set_tplpossrc/compat/db_query.c위치를 DB_QUERY_TPLPOS로 저장/복원
db_query_get_tuple_object / _valuesrc/compat/db_query.ccursor_get_current_oid / cursor_get_tuple_value 래핑
pt_new_query_result_descriptorsrc/parser/query_result.c파싱된 질의로부터 DB_QUERY_RESULT를 생성 — 컴파일된 SELECT마다 cursor_open이 호출되는 자리
parse_evaluate.c 의 cursor 호출src/parser/parse_evaluate.c파서 안의 inline 서브쿼리 평가용(open, next, close)
cas_execute.cis_holdable 세팅src/broker/cas_execute.cRESULT_HOLDABLE이 프로토콜로 진입하는 자리
심볼파일라인
CURSOR_ID (struct)src/query/cursor.h52
CURSOR_POSITIONsrc/query/cursor.h44
cursor_free_list_id 매크로src/query/cursor.h86
cursor_free_self_list_id 매크로src/query/cursor.h105
cursor_opensrc/query/cursor.c1194
cursor_closesrc/query/cursor.c1381
cursor_freesrc/query/cursor.c1342
cursor_copy_list_idsrc/query/cursor.c105
cursor_allocate_oid_buffersrc/query/cursor.c1140
cursor_set_oid_columnssrc/query/cursor.c1322
cursor_set_copy_tuple_valuesrc/query/cursor.c1291
cursor_set_prefetch_lock_modesrc/query/cursor.c1267
cursor_next_tuplesrc/query/cursor.c1482
cursor_prev_tuplesrc/query/cursor.c1568
cursor_first_tuplesrc/query/cursor.c1652
cursor_last_tuplesrc/query/cursor.c1696
cursor_get_tuple_valuesrc/query/cursor.c1734
cursor_get_tuple_value_listsrc/query/cursor.c1778
cursor_get_tuple_value_from_listsrc/query/cursor.c424
cursor_get_tuple_value_to_dbvaluesrc/query/cursor.c375
cursor_get_first_tuple_valuesrc/query/cursor.c483
cursor_fetch_page_having_tuplesrc/query/cursor.c992
cursor_buffer_last_pagesrc/query/cursor.c946
cursor_get_list_file_pagesrc/query/cursor.c506
cursor_point_current_tuplesrc/query/cursor.c911
cursor_construct_tuple_from_overflow_pagessrc/query/cursor.c666
cursor_allocate_tuple_areasrc/query/cursor.c639
cursor_initialize_current_tuple_value_positionsrc/query/cursor.c85
cursor_peek_tuplesrc/query/cursor.c1420
cursor_get_current_oidsrc/query/cursor.c1449
cursor_fixup_vobjssrc/query/cursor.c282
cursor_fixup_set_vobjssrc/query/cursor.c185
cursor_copy_vobj_to_dbvaluesrc/query/cursor.c333
cursor_has_first_hidden_oidsrc/query/cursor.c727
cursor_prefetch_first_hidden_oidsrc/query/cursor.c786
cursor_prefetch_column_oidssrc/query/cursor.c841
cursor_fetch_oidssrc/query/cursor.c740
cursor_get_oid_from_tuplesrc/query/cursor.c622
cursor_get_oid_from_vobjsrc/query/cursor.c591
cursor_print_list (debug)src/query/cursor.c1062
qfile_get_list_file_page (client)src/communication/network_interface_cl.c6676
xqfile_get_list_file_page (server)src/query/list_file.c2312
qmgr_clear_trans_wakeupsrc/query/query_manager.c2271
qmgr_get_query_entry (holdable fallback)src/query/query_manager.c566
session_store_query_entry_infosrc/session/session.c2508
session_load_query_entry_infosrc/session/session.c2593
session_remove_query_entry_infosrc/session/session.c2652
session_remove_query_entry_allsrc/session/session.c2622
qentry_to_sentrysrc/session/session.c2406
sentry_to_qentrysrc/session/session.c2484
session_preserve_temporary_filessrc/session/session.c2442
RESULT_HOLDABLEsrc/query/query_list.h584
DB_SELECT_RESULT::cursor_idsrc/compat/db_query.h74
DB_CURSOR_SUCCESS / END / ERRORsrc/compat/dbtype_def.h176
  • vs. cubrid-list-file.md. list-file 문서는 QFILE_LIST_ID를 하나의 질의 실행 내부 의 생산자/소비자 경계로 다룬다. qfile_open_list가 쓰고, qfile_open_list_scan이 읽는다. 커서는 소비자가 또 다른 XASL 연산자가 아니라 네트워크 클라이언트일 때, 실행기 측 list-scan의 자리 를 대신 떠맡는다. 페이지 위 튜플 포맷은 동일하다. tuple_length, prev_tuple_length, value flag, value length, packed bytes. 큰 튜플을 같은 비용(overflow 체인 reassemble)을 치르고, 다중 페이지 라운드트립을 같은 비용(IO_MAX_PAGE_SIZE-크기의 버퍼)을 치른다. 커서 모듈에서 새로 생기는 것은 네트워크 버퍼 캐시(직전 응답에 패킹되지 않은 페이지를 넘기 전까지는 buffer_area 안에서 걸어감)와 OID prefetch 최적화다. 실행기 안의 qfile_open_list_scan은 후자를 필요로 하지 않는다. 실행기는 이미 OID를 MOP 형태로 들고 있기 때문이다.

  • vs. cubrid-server-session.md. session 문서는 SESSION_QUERY_ENTRYSESSION_STATE에 매달린 세 개의 named- catalogue 리스트(세션 변수, prepared statement와 함께) 중 하나로 나열하고, session_store_query_entry_info가 “query manager가 매 holdable 결과마다 호출하며, query manager의 entry를 세션으로 복사하면서 list_idtemp_vfid 포인터를 훔친다”고 기술한다. 본 문서는 그 계약의 소비자 측이다. 커서가 클라이언트가 그 살아 있는 결과를 읽는 도구다. session 문서는 또한 sessions.num_holdable_cursors 글로벌 카운터를 짚고 있고, 커서 측은 브로커 쪽 as_info->num_holdable_results를 올린다. 둘은 브로커 재시작을 제외하면 lockstep이어야 한다.

  • vs. cubrid-query-executor.md. executor 문서는 S_LIST_SCAN을 SCAN_ID arm 중 하나 — 서버 측, in-process QFILE_LIST_ID 소비자 — 로 설명한다. 커서는 같은 산출물의 클라이언트 측, cross- network 소비자다. 둘을 잇는 다리가 xqfile_get_list_file_page 서버 진입점이다. query manager에 질의의 QFILE_LIST_ID를 요청하고, S_LIST_SCAN이 쓰는 것과 똑같은 게이트키퍼인 qmgr_get_old_page로 페이지 체인을 걸어가며, 페이지 바이트를 네트워크 응답으로 복사한다. 두 소비자는 상태를 공유하지 않는다. 커서의 위치는 순수 클라이언트 측, S_LIST_SCAN의 위치는 순수 서버 측 — 이지만, 그 아래 깔린 튜플 포맷, 페이지 포맷, 저장소 기반은 공유한다.

  • VOBJ에 대한 단방향 의존성. 커서는 cursor_fixup_vobjs에서 vid_oid_to_objectvid_vobj_to_object를 호출한다. 이들은 virtual-objects / view-instance 서브시스템의 일부이며 (src/object/virtual_object.c 참고), 워크스페이스 (src/object/work_space.c)와 locator 클라이언트 (src/object/locator_cl.c)가 초기화되어 있어야 한다. 그래서 cursor.c클라이언트 라이브러리 변형(CS_MODE와 SA_MODE)에만 컴파일된다. SERVER_MODE에서도 파일 자체는 빌드에 포함되지만 (서버 측 _get_list_file_page 헬퍼인 xqfile_get_list_file_pagecursor.c가 아니라 list_file.c에 산다), 커서 자체는 서버에서 쓰이지 않는다.

  • cursor_openstatic QFILE_LIST_ID empty_list_id는 왜? 주석은 TODO: remove static empty_list_id라고 적혀 있다. 이 변수는 local-static이며, cursor_copy_list_id가 덮어쓰기 전에 커서의 list_id를 zeroing하는 용도로만 쓰인다. QFILE_CLEAR_LIST_ID(&empty_list_id)가 매 호출마다 실행되므로 cursor_open을 동시에 호출하는 두 스레드가 torn write를 볼 수 있다. 실무적으로는 변수 내용이 매번 비트 단위로 동일하게 다시 계산되므로 torn write가 무해하지만, “정적 저장소 위 데이터 레이스” 경고를 부르는 고전적 구조이고, TODO가 이를 인정한다.

  • holdable 커서의 COMMIT을 넘은 위치 상태 — 무엇이 살아남는가? 세션이 저장하는 것은 list_id, temp_vfid, num_tmp, total_count, query_flag다. 커서 위치 상태 — current_vpid, current_tuple_no, current_tuple_offset 등 — 는 전혀 저장하지 않는다. 위치는 CURSOR_ID 위의 순수 클라이언트 측 상태이므로, open된 커서 안의 COMMIT은 클라이언트 입장에선 조용히 투명하다. 다음 cursor_next_tuple 은 같은 current_vpid를 보고 그 뒤 페이지를 요청한다. 이게 브로커 프로세스 계층(브로커의 T_SRV_HANDLE과 그 안에 박힌 DB_QUERY_RESULT는 COMMIT을 넘어 보존된다, 즉 일반 사용에서는 yes) 을 넘는지는 통합 단계의 디테일이지 커서 모듈의 디테일이 아니다.

  • cursor_set_oid_columns vs. is_updatable. 현재 코드는 is_updatable이 켜져 있으면 cursor_set_oid_columns를 거부한다. 그런데 is_updatable오직 이 거부만 켤 뿐, 커서 모듈의 다른 어디도 이 비트를 읽지 않는다. updatable 커서는 사실상 is_oid_included로 표현되고, is_updatable은 API를 오용하기 어렵게 만드는 가드레일이다. 첫 컬럼이 아닌 자리에 OID가 있는 미래의 updatable 커서를 지원해야 할지는 미정이다.

  • 다중 페이지 패킹과 header_vpid 필드. 커서의 header_vpid는 마지막 네트워크 버퍼 fill이 받았던 VPID를 기록한다. 이로써 뒤이은 cursor_get_list_file_page가 버퍼 안을 정방향으로 걸어가며 각 패킹된 페이지의 VPID를 요청과 비교할 수 있다. 이 walk는 O(packed-pages)이며, miss마다 header_vpid에서 다시 시작한다. 최악의 경우(커서가 버퍼에 없는 페이지들 위로 앞뒤로 지그재그하는 경우) 같은 네트워크 페이지를 반복해서 다시 fetch하게 된다. 마지막 fetch가 가져온 것을 그대로 들고 있는다 외에 eviction 정책은 없다. 커서 위에 더 큰 LRU 페이지 캐시를 두는 것이 mixed- direction 워크로드에 도움이 될지는 미해결 질문이다.

  • 대규모 커서 — 왜 스트리밍이 없는가? 커서는 풀 머터리얼라이즈 비용을 모두 치른다(첫 cursor_next_tuple이 반환되기 전에 결과 전체가 list-file에 들어 있다). Postgres의 PORTAL_ONE_SELECT는 forward-only 커서를 머터리얼라이즈 없이 실행기로부터 직접 스트리밍한다. CUBRID의 forward-only 전용 cursor_open이 list-file을 지나치고 실행기 iterator 트리에 직접 결합할 수 있을지는 미해결 아키텍처 문제다. 전제 조건은 실행기가 cursor_next_tuple 호출 사이에서도 살아 있어야 한다는 것인데, 이는 qexec_execute_query가 완결까지 한 번에 돌고 끝나는 현재 모델과 충돌한다.

  • 연결 단절 vs. 브로커 재시작 시 holdable 커서 가시성. TCP 소켓이 죽으면 net_server_conn_downsession_remove_query_entry_all 을 호출해 모든 holdable 커서를 비운다. 자기 CAS 프로세스를 재활용하는 브로커(예: BROKER_RESTART_TIME 만료)는 연결을 깨끗이 닫고, holdable 커서는 파괴된다. kill -9를 당한 브로커는 커널이 TCP 소켓을 닫고 같은 청소 코드가 돈다. 즉 holdable 커서는 브로커 재활용을 넘기지 못하며, 오직 브로커 내부 의 COMMIT만 이득을 본다. 이게 문서화된 계약인지 부수효과인지는 분명치 않다.

  • src/query/cursor.c — 커서 모듈 전체: 위치 보행, 페이지 fetch, 디코딩, OID prefetch, 수명을 다루는 약 1800 라인
  • src/query/cursor.hCURSOR_ID struct, CURSOR_POSITION enum, cursor_* exported API, cursor_free_list_id 매크로 패밀리
  • src/query/list_file.cxqfile_get_list_file_page 서버 측 핸들러(페이지 패킹 운반의 짝꿍)
  • src/query/query_list.hRESULT_HOLDABLE 플래그, cursor.c가 소비하는 페이지 헤더 / 튜플 값 매크로들
  • src/query/query_manager.cqmgr_clear_trans_wakeup(commit hand-off), qmgr_get_query_entry(post-commit reload), is_holdable 플래그
  • src/query/query_manager.hQMGR_QUERY_ENTRY::is_holdable
  • src/session/session.csession_store_query_entry_info, session_load_query_entry_info, qentry_to_sentry, sentry_to_qentry, session_preserve_temporary_files, session_remove_query_entry_*
  • src/communication/network_interface_cl.cqfile_get_list_file_page 클라이언트 stub, NET_SERVER_LS_GET_LIST_FILE_PAGE 발신
  • src/compat/db_query.h / db_query.cDB_QUERY_RESULT, DB_SELECT_RESULT::cursor_id, db_query_seek_tuplecursor_* 로 위임하는 db_query_* 패밀리
  • src/parser/query_result.cpt_new_query_result_descriptor, 컴파일된 SELECT마다 cursor_open이 호출되는 자리
  • src/parser/parse_evaluate.ccursor_open / _next_tuple / _close를 통한 inline 서브쿼리 평가
  • src/broker/cas_execute.cT_SRV_HANDLE::is_holdabledb_session_set_holdable / RESULT_HOLDABLE로의 전파
  • 자매 문서: cubrid-list-file.md, cubrid-server-session.md, cubrid-query-executor.md