콘텐츠로 이동

CUBRID Overflow File — 코드 수준 심층 분석

이 문서의 위치: 상위 분석 문서인 cubrid-overflow-file.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 모든 분기와 필드를 코드 수준에서 추적한다. 각 장은 독립적으로 읽을 수 있지만, 순서대로 읽으면 하나의 초대형 레코드가 첫 overflow 페이지 체인에서 갱신·읽기·회수에 이르는 흐름 전체를 따라갈 수 있다.

목차:

Ch제목상태
1자료 구조 전체 지도
2파일 프로비저닝과 소유 참조
3overflow_insert로 체인 만들기
4체인 읽기와 순회
5overflow_update로 체인을 제자리에서 갱신하기
6체인 삭제와 페이지 반환
7B+Tree OID 리스트 변형 구현
8크래시 복구와 예외 경로

overflow 파일 관리자(src/storage/overflow_file.c / .h)는 한 페이지에 담을 수 없는 단일 논리 레코드를 단일 연결 페이지 체인(private singly-linked chain) 으로 저장한다. 모든 진입점은 고정 크기 페이지 버퍼(PAGE_PTR, DB_PAGESIZE 바이트)를 두 C 구조체 중 하나로 재해석하며, 구조체 선택은 오직 체인 내 위치 로만 결정된다. 첫 번째 페이지는 overflow_first_part, 그 외 모든 페이지는 overflow_rest_part다. 이 모듈에는 별도의 온-디스크 페이지 헤더 구조체, 슬롯 디렉터리, 체크섬 필드가 없다 — 페이지 버퍼 포인터를 직접 구조체 포인터로 캐스트하여 사용한다.

이 챕터는 한 가지 질문에 답한다: 각 overflow 페이지에는 무엇이 있으며, 두 포맷은 필드 수준에서 어떻게 다른가? DBMS가 overflow 페이지, 첫 페이지의 길이 필드, 잠금 없는 전용 체인을 왜 사용하는지 하는 배경 설명은 이 문서가 아닌 고수준 동반 문서(Common DBMS Design)에서 다룬다.

1.1 두 페이지 포맷 — 필드별 비교

섹션 제목: “1.1 두 페이지 포맷 — 필드별 비교”

두 구조체는 src/storage/overflow_file.h에 선언되어 있으며, 전체를 인용할 만큼 짧다:

// overflow_first_part -- src/storage/overflow_file.h
struct overflow_first_part
{
VPID next_vpid;
int length;
char data[1]; /* Really more than one */
};
// overflow_rest_part -- src/storage/overflow_file.h
struct overflow_rest_part
{
VPID next_vpid;
char data[1]; /* Really more than one */
};

VPID(src/storage/storage_common.h{ PAGEID pageid; VOLID volid; } 쌍)는 온-디스크 페이지 주소, 즉 연결 리스트의 “다음 포인터”다. data[1]은 C의 trailing-array 관용구다 — 원소 하나로 선언하여 offsetof(...,data)가 명확히 정의되도록 하되, 실제로는 페이지 끝까지 data가 이어진다. 주석 /* Really more than one */이 이를 명시한다. 코드는 data[0] 이후 원소를 인덱스로 접근하지 않고, (char *) ...->data를 취해 memcpy로 일정 바이트를 복사한다.

overflow_first_part:

필드역할존재 이유
next_vpid이 체인의 두 번째 페이지 VPID; 레코드가 한 페이지에 모두 들어가면 NULL-VPID.연결 리스트 포인터. 나머지 레코드로 가는 유일한 경로.
length전체 논리 레코드의 바이트 길이(모든 페이지 합산). 이 페이지의 바이트 수가 아님.체인에 내용 종료 센티널이 없으므로, 독자는 출력 버퍼 크기 결정과 복사 중단 시점을 위해 전체 길이를 미리 알아야 한다. 동반 문서 ### Length stored on the first page 참고.
data[1]페이로드 바이트 스트림의 시작; 레코드의 첫 DB_PAGESIZE - offsetof(...,data) 바이트가 여기에 있다.실제 레코드 내용. [1]은 trailing-array 관용구; 실제 범위는 페이지 끝까지다.

overflow_rest_part:

필드역할존재 이유
next_vpid다음 페이지의 VPID; 체인의 마지막 페이지면 NULL-VPID.연결 리스트 포인터. NULL이 체인 종료 표시다.
data[1]이전 페이지 data가 끝난 곳부터 이어지는 페이로드 바이트 스트림.추가 레코드 내용. length 없음 — 아래 참고.

1.2 첫 번째 페이지에만 length가 있는 이유

섹션 제목: “1.2 첫 번째 페이지에만 length가 있는 이유”

length는 페이지당 수량이 아니라 레코드 단위 수량이므로, 외부에서 주소를 아는 유일한 페이지인 헤드 VPID에 딱 한 번 저장한다. 모든 독자는 헤드를 먼저 픽스(fix)한 뒤 length를 읽고 나서 체인을 순회한다 — overflow_get_length는 헤드를 OVERFLOW_FIRST_PART *로 캐스트하여 ->length를 직접 반환하며, rest_part에는 접근하지 않는다. rest_part가 길이를 보고할 필요는 없다: 순회는 VPID_ISNULL(&next_vpid)에서 종료되고, 바이트 카운트는 헤드의 length로 이미 한정되어 있기 때문이다. 페이지마다 length를 두면 sizeof(int) 공간이 낭비되고 중복 상태 일관성 부담이 생긴다. 동반 문서 ### Length stored on the first page 참고.

1.3 헤더 크기 차이와 페이지당 용량

섹션 제목: “1.3 헤더 크기 차이와 페이지당 용량”

overflow_rest_partint length 필드를 생략하므로 헤더가 첫 페이지보다 sizeof(int)(4바이트) 작다. 코드는 헤더 크기를 절대로 하드코딩하지 않고, DB_PAGESIZE - offsetof(...)로 페이지당 페이로드 용량을 유도한다:

  • 첫 번째 페이지: DB_PAGESIZE - offsetof(OVERFLOW_FIRST_PART, data) = DB_PAGESIZE - (sizeof(VPID) + sizeof(int)).
  • 나머지 페이지: DB_PAGESIZE - offsetof(OVERFLOW_REST_PART, data) = DB_PAGESIZE - sizeof(VPID).

따라서 나머지 페이지는 첫 번째 페이지보다 sizeof(int)(4바이트) 더 많은 페이로드를 담는다. 같은 비대칭성이 overflow_insert의 페이지 수 추정을 이끈다 — 첫 페이지의 더 작은 용량을 레코드에서 먼저 차감하고, 남은 바이트를 더 큰 나머지 페이지 용량으로 나눈다:

// overflow_insert -- src/storage/overflow_file.c
length = recdes->length - (DB_PAGESIZE - (int) offsetof (OVERFLOW_FIRST_PART, data));
if (length > 0)
{
i = DB_PAGESIZE - offsetof (OVERFLOW_REST_PART, data);
npages = 1 + CEIL_PTVDIV (length, i);
}
else
{
npages = 1;
}

여기서 빼기 후의 length는 헤드 이후의 잔여 바이트 수이고, i는 나머지 페이지 용량, npages는 헤드 1개에 ceil(잔여 / i)를 더한 값(레코드가 한 페이지에 들어가면 1)이다.

불변 조건 — 헤더 크기는 유도하며, 리터럴을 쓰지 않는다. 모든 페이로드 용량·페이지 수 계산은 offsetof(OVERFLOW_FIRST_PART, data) 또는 offsetof(OVERFLOW_REST_PART, data)를 읽으며, 상수를 직접 쓰지 않는다 — 이렇게 해야 정렬이나 필드 변경이 있어도 4바이트 비대칭이 올바르게 유지된다. offsetof를 하드코딩된 숫자로 교체하면 페이지 수 추정이 부족해질 수 있다: 복사 루프 끝의 assert (length == 0)이 디버그 빌드에서 발화하거나, 릴리스 빌드에서 레코드가 조용히 잘린다.

1.4 체인 불변 조건과 페이지가 잠기지 않는 이유

섹션 제목: “1.4 체인 불변 조건과 페이지가 잠기지 않는 이유”

구조적 계약:

불변 조건 — 헤드 VPID가 유일한 외부 핸들이며, data[]는 페이지에 걸쳐 이어지는 하나의 연속 바이트 스트림이고, 체인은 전용(private)이다. 포함 레코드(힙 relocation-overflow 레코드 또는 B+트리 리프/overflow OID 슬롯)에 저장되는 주소는 오직 ovf_vpid, 즉 헤드 페이지뿐이다. 다른 모든 페이지는 헤드에서 next_vpid를 따라가야만 도달할 수 있으며, 두 논리 레코드가 한 체인을 공유하지 않는다. 체인이 전용이고 이미 잠긴 포함 레코드를 거쳐야만 도달 가능하므로, overflow 페이지 자체는 잠기지 않는다 — 물리적 일관성을 위해 페이지 버퍼 latch(pgbuf_fix)로 픽스할 뿐, lock manager로 관리하지 않는다. overflow_insert 헤더 주석은 이를 명시한다(“Overflow pages are not locked … since they are not shared … and its address is only know by accessing the relocation overflow record data which has been appropriately locked”). 동반 문서 ### Pages not individually locked 참고. 이 규칙이 위반되어 두 레코드가 같은 체인을 가리킨다면, 하나를 삭제할 때 다른 쪽이 아직 참조 중인 페이지가 해제되어 생존 레코드가 손상된다.

“연속 바이트 스트림” 부분은 두 구조체를 상호 운용 가능하게 만든다: 독자는 바이트 kfirst_part에 있고 바이트 k+1rest_part에 있다는 사실을 신경 쓰지 않아도 된다. overflow_get_nbytescopyfrom을 현재 페이지 data 끝까지 진행시키고, next_vpid를 픽스한 뒤, copyfrom을 새 페이지의 data로 재설정한다 — 두 구조체 모두 data[]로 끝나므로 이음새가 없다.

두 구조체와 체인의 관계:

flowchart LR
  ref["포함 레코드\n헤드 VPID만 보유"] --> H
  subgraph chain["단일 전용 overflow 체인"]
    H["overflow_first_part\nnext_vpid -> P1\nlength = 전체 바이트 수\ndata[ ...페이로드... ]"]
    P1["overflow_rest_part\nnext_vpid -> P2\ndata[ ...페이로드... ]"]
    P2["overflow_rest_part\nnext_vpid = NULL-VPID\ndata[ ...페이로드... ]"]
  end
  H --> P1 --> P2

Figure 1-1. 구조체 관계: length를 가진 overflow_first_part 헤드 하나, 그 뒤에 0개 이상의 overflow_rest_part 페이지, 마지막은 NULL next_vpid로 종료. 외부에서 아는 주소는 헤드 VPID뿐이다.

1.5 overflow_next_vpid — 위치가 포맷을 결정하는 방식

섹션 제목: “1.5 overflow_next_vpid — 위치가 포맷을 결정하는 방식”

페이지 자체에는 first part인지 rest part인지를 알려주는 플래그가 없다; 위치 가 결정한다 — 이 페이지의 VPID가 헤드 VPID와 같은가? overflow_next_vpid는 이 분기를 수행하는 유일한 헬퍼다:

// overflow_next_vpid -- src/storage/overflow_file.c
if (VPID_EQ (ovf_vpid, vpid))
{
*vpid = ((OVERFLOW_FIRST_PART *) pgptr)->next_vpid;
}
else
{
*vpid = ((OVERFLOW_REST_PART *) pgptr)->next_vpid;
}

VPID_EQ (ovf_vpid, vpid) 검사가 head/rest 판별자다: 페이지 자신의 VPID가 헤드 VPID와 같으면 OVERFLOW_FIRST_PART *로 캐스트(lengthdata 앞에 있는 포맷), 그렇지 않으면 OVERFLOW_REST_PART *로 캐스트한다. 두 구조체 모두 next_vpid를 첫 번째 필드로 배치하므로 링크 만 읽을 때는 어느 캐스트든 올바르다 — 하지만 lengthdata 같이 링크 이후 필드에 접근할 때는 올바른 캐스트가 필요하며, 그래서 판별자가 존재한다. data/length를 건드리는 독자들은 이 헬퍼를 호출하는 대신 동일한 VPID_EQ(addr_vpid_ptr, ovf_vpid) 검사를 직접 재현한다.

체인의 각 페이지에 부수 효과를 적용하는 작업은 overflow_traverse에 집중되어 있으며, enum으로 매개변수화하여 하나의 워커가 전체 체인 작업 두 가지를 모두 처리한다:

// OVERFLOW_DO_FUNC -- src/storage/overflow_file.c
typedef enum
{
OVERFLOW_DO_DELETE,
OVERFLOW_DO_FLUSH
} OVERFLOW_DO_FUNC;

두 멤버는 페이지당 부수 효과를 나타낸다: OVERFLOW_DO_DELETE는 방문할 때마다 각 페이지를 해제하는 파괴적 워크(overflow_delete 뒤의 동작), OVERFLOW_DO_FLUSH는 각 페이지를 WAL-flush하는 읽기 전용 워크(overflow_flush 뒤의 동작)이다. overflow_traverse는 페이지마다 이를 스위치한다: OVERFLOW_DO_DELETE는 먼저 ovf_vfid != NULL을 확인하고(if (ovf_vfid == NULL) goto exit_on_error;, 파일 ID 없이는 페이지를 해제할 수 없으므로) overflow_delete_internal을 호출한다; OVERFLOW_DO_FLUSHoverflow_flush_internal을 호출하고; default: 분기는 break no-op이다. 호출자는 인수 하나만 다르다 — overflow_delete는 실제 ovf_vfid를, overflow_flushNULL을 전달 — 따라서 enum 하나로 두 워크를 구분한다. 각 분기의 상세 동작은 챕터 6에서 다룬다.

1.7 file_type 단언과 페이지 타입 태그

섹션 제목: “1.7 file_type 단언과 페이지 타입 태그”

이 서브시스템을 서로 다른 레이어에서 보호하는 두 개의 독립적인 타입 태그가 있다; 혼동하지 말 것.

파일 타입 — FILE_TYPE(src/storage/file_manager.h에서 정의). overflow_insert는 세 가지 파일 종류 중 하나 안에서만 체인을 허용하며, 단언으로 강제한다:

// overflow_insert -- src/storage/overflow_file.c
assert (file_type == FILE_TEMP /* sort files */
|| file_type == FILE_BTREE_OVERFLOW_KEY /* b-tree overflow key */
|| file_type == FILE_MULTIPAGE_OBJECT_HEAP /* heap overflow file */ );
FILE_TYPE사용처overflow_insert의 로깅 동작
FILE_MULTIPAGE_OBJECT_HEAP힙 페이지에 담기 너무 큰 힙 레코드.로깅됨 (각 새 페이지의 redo + 헤드에 LOG_DUMMY_OVF_RECORD).
FILE_BTREE_OVERFLOW_KEY리프에 인라인하기 너무 큰 B+트리 키.로깅됨, 힙과 동일.
FILE_TEMP정렬 / 쿼리 임시 스필 파일.로깅 안 함; 할당에 file_init_temp_page_type 사용. file_type != FILE_TEMP 가드가 모든 log_append_* 호출을 건너뜀.

overflow_update는 이를 file_type == FILE_MULTIPAGE_OBJECT_HEAP만으로 좁히며, 소스 주석은 다른 두 타입에 대해서는 감사되지 않았다고 경고한다(챕터 5).

페이지 타입 — PAGE_OVERFLOW(src/storage/storage_common.hPAGE_TYPE enum에서 정의). 파일 종류에 관계없이, 모든 체인 페이지는 페이지 버퍼에서 PAGE_OVERFLOW로 태깅된다. overflow_insertPAGE_TYPE ptype = PAGE_OVERFLOW;를 설정하고 &ptypefile_alloc_multiple에 전달한다; 다른 모든 진입점은 페이지를 픽스한 후 태그를 단언한다:

// repeated guard across overflow_file.c
#if !defined (NDEBUG)
(void) pgbuf_check_page_ptype (thread_p, addr.pgptr, PAGE_OVERFLOW);
#endif /* !NDEBUG */

불변 조건 — 픽스된 모든 overflow 페이지는 PAGE_OVERFLOW다. insert가 할당 시 스탬프를 찍고; 복구도 재스탬프한다(overflow_rv_page_update_redo는 내용을 재적용하기 전에 pgbuf_set_page_ptype(..., PAGE_OVERFLOW) 호출); 모든 읽기·수정·삭제·flush 경로는 pgbuf_check_page_ptype으로 디버그 단언한다. next_vpid로 도달한 페이지가 overflow 페이지가 아니면 디버그 빌드에서 단언이 발화한다 — 체인이 손상된 것이다. FILE_TYPEPAGE_TYPE은 직교한다: 세 가지 허용 파일 타입 모두 PAGE_OVERFLOW 페이지를 생성한다.

1.8 한 체인의 바이트 수준 레이아웃

섹션 제목: “1.8 한 체인의 바이트 수준 레이아웃”

세 페이지에 걸친 레코드의 바이트 맵. V = sizeof(VPID), L = sizeof(int), P = DB_PAGESIZE.

head page (overflow_first_part) offset
+-----------------------------------------+
| next_vpid : VPID -> page 2 | 0
| length : int = TOTAL record bytes | V
| data[] : payload 0 .. P-V-L-1 | V+L = offsetof(FIRST,data)
+-----------------------------------------+ P
| next_vpid
v
rest page 2 (overflow_rest_part)
+-----------------------------------------+
| next_vpid : VPID -> page 3 | 0
| data[] : next P-V bytes (L more room)| V = offsetof(REST,data)
+-----------------------------------------+ P
| next_vpid
v
rest page 3 (overflow_rest_part) [LAST]
+-----------------------------------------+
| next_vpid : NULL-VPID (VPID_ISNULL) | 0 <- chain terminator
| data[] : final bytes; tail = slack | V
+-----------------------------------------+ P
sum of all three data[] runs == head.length (read-stop condition)

Figure 1-2. 세 페이지 체인의 포인터/레이아웃 맵. 헤드는 V+L을 예약하고; 각 나머지 페이지는 V만 예약하여 L(=4바이트) 더 많은 페이로드 공간을 확보한다. 마지막 페이지의 next_vpid는 NULL이며 data[] 끝부분이 미사용일 수 있다 — 레코드 경계는 페이지 충만도가 아니라 헤드의 length가 결정한다.

마지막 페이지의 미사용 끝부분은 실제 여유 공간이다: overflow_get_capacity가 이를 *ovf_free_space로 보고하며, overflow_update는 새 내용이 기존 페이지 범위에 여전히 들어맞을 때 이를 활용해 레코드를 제자리에서 늘린다(챕터 5).

  1. 두 포맷, 위치로 선택. 페이지의 VPID가 외부에서 알려진 헤드 VPID와 같으면 overflow_first_part, 그렇지 않으면 overflow_rest_part다. 페이지 자체에 플래그 없음 — overflow_next_vpidVPID_EQ 검사가 판별자다.
  2. length는 헤드에만 있으며, 페이지별 바이트 수가 아닌 전체 레코드 바이트 수를 의미한다. 독자(overflow_get_length, overflow_get_nbytes, overflow_get_capacity)는 순회 전에 헤드에서 한 번 읽는다.
  3. 나머지 페이지는 헤드보다 4바이트 더 많은 페이로드를 담는다 — int length 생략으로 인한 차이이며, sizeof(int) = offsetof(FIRST,data) - offsetof(REST,data)다. 모든 용량 계산은 offsetof를 사용하며 리터럴을 쓰지 않는다 — 강제된 불변 조건이다.
  4. data[1]은 trailing-array 관용구: 선언된 길이는 1이지만 페이지 끝까지 확장된다; 코드는 (char *) ...->datamemcpy를 사용하며, i > 0data[i]를 인덱스로 쓰지 않는다.
  5. 체인은 전용이며 잠기지 않는다. 헤드 VPID가 유일한 외부 핸들이고; 다른 페이지는 next_vpid 링크로만 도달 가능하며; 어떤 페이지도 공유되지 않는다; 페이지는 페이지 버퍼 latch를 받을 뿐 lock manager의 잠금을 받지 않는다.
  6. OVERFLOW_DO_FUNC(OVERFLOW_DO_DELETE / OVERFLOW_DO_FLUSH) 는 공유 overflow_traverse 워커를 파괴적 또는 읽기 전용 워크로 전환하는 단일 분기자다.
  7. 두 개의 직교하는 타입 태그가 서브시스템을 보호한다: FILE_TYPE(FILE_TEMP, FILE_BTREE_OVERFLOW_KEY, FILE_MULTIPAGE_OBJECT_HEAP 중 하나이며 insert의 로깅 여부도 결정) 과 PAGE_OVERFLOW(모든 파일 타입의 모든 overflow 페이지에 적용되며 픽스할 때마다 단언됨).

Chapter 2: 파일 프로비저닝과 소유 참조

섹션 제목: “Chapter 2: 파일 프로비저닝과 소유 참조”

체인이 존재하려면 두 가지가 먼저 갖춰져야 한다: 페이지를 할당할 파일(VFID)과, 홈 오브젝트가 헤드 페이지를 가리키는 참조. 이 챕터는 파일이 어디서 오는지헤드 VPID를 어느 필드가 보유하는지를 추적한다 — 힙 big-record 경로와 B+Tree의 두 경로별로, 파일이 생성되고 참조 슬롯이 준비되는 시점까지만 다룬다(바이트 수준의 삽입 루프는 3챕터). 상위 개요 문서(cubrid-overflow-file.md)는 힙마다, B+Tree마다 파일이 하나인지, 왜 페이지를 개별적으로 잠그지 않는지를 설명하며, 이 챕터는 분기 레벨에서 동작 흐름을 다룬다.

2.1 두 가지 프로비저닝 모델 — lazy vs. lazy-with-preallocation

섹션 제목: “2.1 두 가지 프로비저닝 모델 — lazy vs. lazy-with-preallocation”

두 모델 모두 overflow 파일을 지연 생성(lazy) 방식으로 만든다 — 첫 번째 레코드가 흘러 넘치기 전까지는 파일이 존재하지 않는다. 차이점은 이 챕터에서 아래와 같이 분석한다:

항목힙 (heap_ovf_find_vfid)B+Tree (btree_create_overflow_key_file)
파일 타입FILE_MULTIPAGE_OBJECT_HEAPFILE_BTREE_OVERFLOW_KEY
사전 할당 페이지 수1최소 3
VFID 저장 위치HEAP_HDR_STATS.ovf_vfid (헤더 슬롯 HEAP_HEADER_AND_CHAIN_SLOTID)BTID_INT::ovfid + 루트 헤더 ovfid
복구 레코드RVHF_STATS (undo + redo)RVBT_UPDATE_OVFID (undoredo)
디스크립터 armFILE_OVF_HEAP_DESFILE_OVF_BTREE_DES
생성자 vs. 탐색 전용heap_ovf_insert (docreate=true); heap_ovf_update/heap_ovf_delete (docreate=false)키 overflow 시 leaf-insert 경로; find 함수 없음 — BTID_INT::ovfid에서 직접 존재 여부 확인

힙의 모든 big record는 힙의 단일 FILE_MULTIPAGE_OBJECT_HEAP을 공유하며, B+Tree의 두 가지 overflow 용도는 두 개의 별도 파일로 분리된다(§2.5).

flowchart LR
  HHDR["HEAP_HDR_STATS.ovf_vfid"] -- names --> HFILE["FILE_MULTIPAGE_OBJECT_HEAP\ndesc FILE_OVF_HEAP_DES"]
  BROOT["Root header ovfid"] -- persisted --> BINT["BTID_INT::ovfid\nin-mem mirror"]
  BINT -- names --> BFILE["FILE_BTREE_OVERFLOW_KEY\ndesc FILE_OVF_BTREE_DES"]

Figure 2-1 — 소유 파일 참조 구조. 힙은 VFID를 힙 헤더 레코드에 보관하고, B+Tree는 루트 헤더에 저장하면서 BTID_INT::ovfid를 인메모리 미러로 유지한다.

2.2 heap_ovf_find_vfid — 힙의 find-or-create

섹션 제목: “2.2 heap_ovf_find_vfid — 힙의 find-or-create”

heap_ovf_find_vfid는 overflow 파일이 필요한 모든 힙 호출자의 단일 진입점이다. docreate 인자가 동작 방식을 결정한다 — “없으면 생성”(삽입)과 “조회만”(수정/삭제). 헤더 latch 방향도 이에 따른다: 생성자는 WRITE, 조회자는 READ.

// heap_ovf_find_vfid -- src/storage/heap_file.c
mode = (docreate == true ? PGBUF_LATCH_WRITE : PGBUF_LATCH_READ);
addr_hdr.pgptr = pgbuf_fix (thread_p, &vpid, OLD_PAGE, mode, latch_cond);
if (addr_hdr.pgptr == NULL)
return NULL; /* <- branch A: header fix failed */
// ... condensed: pgbuf_check_page_ptype (PAGE_HEAP) ...
if (spage_get_record (..., HEAP_HEADER_AND_CHAIN_SLOTID, &hdr_recdes, PEEK) != S_SUCCESS)
{ pgbuf_unfix_and_init (thread_p, addr_hdr.pgptr); return NULL; } /* <- branch B: header unreadable */
heap_hdr = (HEAP_HDR_STATS *) hdr_recdes.data;

헤더 페이지는 잘 알려진 위치(hfid->vfid / hfid->hpgid)에 위치한다. 레코드는 PEEK 방식으로 읽히므로 heap_hdr는 라이브 포인터이고, 이후 변경은 RVHF_STATS가 보호하는 인플레이스 수정이 된다.

전체 분기 흐름:

flowchart TB
  A["헤더 페이지 fix\nmode = docreate ? WRITE : READ"] --> B{pgptr == NULL?}
  B -- 예 --> R1["return NULL"]
  B -- 아니오 --> D{spage_get_record\npeek HEAP_HDR_STATS 성공?}
  D -- 실패 --> R2["unfix; return NULL"]
  D -- 성공 --> E{VFID_ISNULL\novf_vfid?}
  E -- "아니오, 존재함" --> F["VFID_COPY\nheap_hdr->ovf_vfid에서 복사"]
  E -- "예, 없음" --> G{docreate?}
  G -- "false" --> H["ovf_vfid = NULL\n호출자에게 파일 없음 반환"]
  G -- "true" --> J["log_sysop_start\nfile_create_with_npages MOH, 1"]
  J --> K{create AND tde lookup\nAND tde apply 모두 성공?}
  K -- "아니오, 어느 단계든" --> X["log_sysop_abort\novf_vfid = NULL; goto exit"]
  K -- 예 --> P["log_append_undo RVHF_STATS\nVFID_COPY into heap_hdr\nlog_append_redo RVHF_STATS\npgbuf_set_dirty; log_sysop_commit"]
  F --> Z["exit: unfix; return ovf_vfid"]
  H --> Z
  P --> Z
  X --> Z

Figure 2-2 — heap_ovf_find_vfid의 모든 분기. K 노드는 세 개의 독립적 실패 지점 (create, TDE lookup, TDE apply)을 통합하며, 각각 sysop을 중단하고 NULL을 반환한다.

생성하지 않는 경우(VFID_ISNULL이 false이거나 docreate == false)가 정상 상태다: 파일이 한 번 생성되면 이후 호출은 모두 VFID_COPY 경로를 탄다. 생성 arm은 **최상위 시스템 오퍼레이션(top system operation)**으로 감싸진다:

// heap_ovf_find_vfid -- src/storage/heap_file.c (create arm, condensed)
log_sysop_start (thread_p);
HFID_COPY (&des.heap_overflow.hfid, hfid); /* <- descriptor: owning heap */
des.heap_overflow.class_oid = heap_hdr->class_oid; /* <- + class, for TDE */
if (file_create_with_npages (..., FILE_MULTIPAGE_OBJECT_HEAP, 1, &des, ovf_vfid) != NO_ERROR)
{ log_sysop_abort (thread_p); ovf_vfid = NULL; goto exit; } /* <- rolled back */
// ... TDE lookup + apply, each with its own abort+goto ...
log_append_undo_data (thread_p, RVHF_STATS, &addr_hdr, sizeof (*heap_hdr), heap_hdr);
VFID_COPY (&heap_hdr->ovf_vfid, ovf_vfid); /* <- the actual header mutation */
log_append_redo_data (thread_p, RVHF_STATS, &addr_hdr, sizeof (*heap_hdr), heap_hdr);
pgbuf_set_dirty (thread_p, addr_hdr.pgptr, DONT_FREE);
log_sysop_commit (thread_p);

순서가 중요하다: VFID_COPY는 두 로그 호출 사이에 위치하므로, undo는 복사 전의 전체 HEAP_HDR_STATS를(null VFID), redo는 복사 후를(신규 VFID) 캡처한다 — RVHF_STATS가 구조체 전체를 통째로 재생하기 때문이다. TDE는 헤더가 파일을 가리키기 전에, 동일한 sysop 안에서 적용된다.

불변 조건: 힙 헤더 VFID 쓰기와 파일 생성은 외부 트랜잭션 관점에서 원자적이다. file_create_with_npages를 감싸는 log_sysop_start / log_sysop_commit 괄호와 RVHF_STATS undo/redo 쌍이 이를 강제한다. 이 불변 조건이 깨지면 — 파일은 생성됐으나 헤더 쓰기가 유실되면 — 힙은 고아 FILE_MULTIPAGE_OBJECT_HEAP을 갖게 되고, 다음 삽입 시 두 번째 파일이 생성된다.

2.3 힙 big record의 홈 참조 — REC_BIGONE

섹션 제목: “2.3 힙 big record의 홈 참조 — REC_BIGONE”

heap_ovf_find_vfid파일만 반환한다. overflow_insert(3챕터)가 VPID로 생성하는 체인의 헤드 페이지heap_ovf_insert에 의해 홈 참조로 변환된다:

// heap_ovf_insert -- src/storage/heap_file.c
if (heap_ovf_find_vfid (thread_p, hfid, &ovf_vfid, true, ...) == NULL
|| overflow_insert (thread_p, &ovf_vfid, &ovf_vpid, recdes, FILE_MULTIPAGE_OBJECT_HEAP) != NO_ERROR)
return NULL; /* <- find/create or insert failed */
ovf_oid->pageid = ovf_vpid.pageid;
ovf_oid->volid = ovf_vpid.volid;
ovf_oid->slotid = NULL_SLOTID; /* Irrelevant */

홈 슬롯 본체는 OID이지만, 의미 있는 필드는 volidpageid뿐이다 — 첫 번째 overflow 페이지를 가리키며, slotidNULL_SLOTID로 강제된다. overflow 레코드에는 슬롯 디렉토리가 없다(각 페이지의 data[]에서 바이트를 이어 붙이는 구조이므로), /* Irrelevant */ 주석이 역참조를 시도하는 독자에게 경고를 준다.

불변 조건: REC_BIGONE 홈 OID는 항상 slotid == NULL_SLOTID를 갖는다. 이 단일 할당 지점에서 강제되며, big-record 경로가 헤드 페이지를 VPID로 fix할 때 전제된다. null이 아닌 slotid는 포워딩 포인터가 오염된 징표다. heap_ovf_insertovf_oid만 채우며, REC_BIGONE 레코드 자체는 힙 삽입 레이어가 기록한다(read/update 챕터).

2.4 btree_create_overflow_key_file — B+Tree의 생성

섹션 제목: “2.4 btree_create_overflow_key_file — B+Tree의 생성”

B+Tree에는 find 방식의 함수가 없다: 존재 여부는 VFID_ISNULL (&btid_int->ovfid)를 직접 확인하며, 생성은 BTID_INT::ovfid를 채우는 전용 함수가 담당한다:

// btree_create_overflow_key_file -- src/storage/btree.c
VFID_SET_NULL (&btid->ovfid);
des.btree_key_overflow.btid = *btid->sys_btid; /* structure copy */
des.btree_key_overflow.class_oid = btid->topclass_oid; // ... assert class_oid not null ...
error_code = file_create_with_npages (thread_p, FILE_BTREE_OVERFLOW_KEY, 3, &des, &btid->ovfid);
if (error_code != NO_ERROR)
return error_code; /* <- branch A: create failed, ovfid stays NULL */
error_code = heap_get_class_tde_algorithm (thread_p, &btid->topclass_oid, &tde_algo);
if (error_code != NO_ERROR)
{ VFID_SET_NULL (&btid->ovfid); return error_code; } /* <- branch B: roll back VFID */
error_code = file_apply_tde_algorithm (thread_p, &btid->ovfid, tde_algo);
if (error_code != NO_ERROR)
{ VFID_SET_NULL (&btid->ovfid); return error_code; } /* <- branch C: roll back VFID */
return error_code; /* NO_ERROR: ovfid now valid */

분기 커버리지와 힙 버전과의 차이점:

  • 진입 시 VFID_SET_NULL — 어느 하위 단계에서 실패하더라도(branch A, B, C) 호출자에게 null ovfid가 남으며, 반쯤 설정된 VFID는 절대 남지 않는다. 이 함수는 인메모리 BTID_INT::ovfid만 채우고 log_sysop_start를 열지 않는다; 지속 가능한 루트 쓰기는 호출자의 책임이다.
  • 최소 3페이지 사전 할당 — 힙의 1페이지와 달리, 이후 overflow key 할당의 파일 확장 비용을 분산한다. TDE 상속은 힙과 동일한 방식으로 처리한다 (heap_get_class_tde_algorithm).

호출자 — leaf-insert 경로 — 가 이를 내구성 있게, 안전하게 처리한다:

// btree leaf-insert path -- src/storage/btree.c (condensed; root is write latched)
if (VFID_ISNULL (&btid_int->ovfid)) /* <- re-check under root WRITE latch */
{
log_sysop_start (thread_p);
error_code = btree_create_overflow_key_file (thread_p, btid_int);
if (error_code != NO_ERROR)
{ log_sysop_abort (thread_p); goto error; }
log_append_undoredo_data2 (thread_p, RVBT_UPDATE_OVFID, ..., *root_page, HEADER,
sizeof (VFID), sizeof (VFID), &root_header->ovfid, &btid_int->ovfid);
VFID_COPY (&root_header->ovfid, &btid_int->ovfid); /* <- persist into root header */
pgbuf_set_dirty (thread_p, *root_page, DONT_FREE);
log_sysop_commit (thread_p);
}

힙 경로에는 없는 두 가지 포인트:

  • null 체크가 루트 write latch 하에서 반복된다 — 소스 주석 “Check that overflow key file was not created by another.” 이 재확인 없이는 동시 삽입자 두 명이 각자 파일을 생성할 수 있다; latch 덕분에 늦은 쪽은 먼저 온 쪽의 VFID를 관찰하게 된다.
  • RVBT_UPDATE_OVFIDundoredo2 방식으로 루트 HEADER 슬롯에 스코프되어 VFID 필드만 로그에 기록한다(old=undo, new=redo). RVHF_STATS처럼 헤더 전체를 기록하지 않는다. sysop은 호출자에 위치하므로, 생성 실패 시 호출 지점에서 abort된다.

불변 조건: B+Tree당 FILE_BTREE_OVERFLOW_KEY는 최대 하나이며, 생성 후에는 루트 헤더 ovfidBTID_INT::ovfid가 동일하다. latch 하의 재확인과 RVBT_UPDATE_OVFID undoredo가 루트 쓰기를 파일 생성 sysop에 묶어 이를 강제한다. 이 불변 조건이 깨지면 파일이 누출되고 overflow key 체인이 분기된다.

2.5 페이지 타입 규칙 — PAGE_OVERFLOW vs PAGE_BTREE

섹션 제목: “2.5 페이지 타입 규칙 — PAGE_OVERFLOW vs PAGE_BTREE”

세 가지 체인 종류는 두 가지 페이지 타입으로 나뉜다; 디스크 레벨의 식별자는 파일이 아니라 페이지 타입이다:

체인 종류소유 파일페이지 타입헤드 참조 필드리프 플래그
힙 big recordFILE_MULTIPAGE_OBJECT_HEAPPAGE_OVERFLOWREC_BIGONE OID 본체 {volid, pageid}n/a
B+Tree overflow keyFILE_BTREE_OVERFLOW_KEY (ovfid)PAGE_OVERFLOWleaf/non-leaf 레코드에 내장된 헤드 VPIDBTREE_LEAF_RECORD_OVERFLOW_KEY (0x4000)
B+Tree overflow OID listbtree_get_new_page를 통한 트리 파일의 페이지PAGE_BTREELEAF_REC.ovfl (a VPID)BTREE_LEAF_RECORD_OVERFLOW_OIDS (0x2000)

각주: LEAF_REC{ VPID ovfl; short key_len; }이며, OID 목록 overflow가 없을 때 ovflVPID_SET_NULL이다. overflow key 참조는 인라인 키 바이트 대신 레코드 본체를 재사용한다.

overflow key 경로는 overflow_insert를 통하며, PAGE_TYPE ptype = PAGE_OVERFLOW로 설정하고 이를 페이지당 초기화 함수로 file_alloc_multiple에 전달하며, 각 페이지에 pgbuf_check_page_ptype (..., PAGE_OVERFLOW)를 단언한다. overflow OID list 경로는 overflow_file.c를 전혀 건드리지 않는다: 해당 페이지는 btree_start_overflow_pagebtree_get_new_page에서 오고, 초기화 함수는 btree_initialize_new_page이다:

// btree_initialize_new_page -- src/storage/btree.c
pgbuf_set_page_ptype (thread_p, page, PAGE_BTREE); /* <- not PAGE_OVERFLOW */
spage_initialize (thread_p, page, UNANCHORED_KEEP_SEQUENCE, BTREE_MAX_ALIGN, ...); /* slotted */

결정적으로, btree_get_new_pagebtid->sys_btid->vfid메인 트리 파일 — 에서 할당한다. ovfid가 아니다. 따라서 OID list overflow 페이지는 “평범한 B+Tree 페이지”이다: 슬롯 방식, PAGE_BTREE(7챕터). overflow key 체인은 힙 big record와 바이트 레벨에서 동일하다: PAGE_OVERFLOW, 슬롯 디렉토리 없음, OVERFLOW_*_PART 헤더.

불변 조건: 페이지 타입이 두 가지 B+Tree overflow 용도를 구분한다. LEAF_REC.ovfl로 도달하는 페이지는 PAGE_BTREE여야 하고, overflow key VPID로 도달하는 페이지는 PAGE_OVERFLOW여야 한다. pgbuf_check_page_ptype 단언이 이를 강제하며, 혼동하면 OVERFLOW_FIRST_PART 스트림이 슬롯 페이지 접근에 공급된다.

세 가지 헤드 참조는 모두 체인 헤드를 향한 전방 포인터이며, 체인 자체가 아니다; 내부 링크(next_vpid / BTREE_OVERFLOW_HEADER.next_vpid)는 3, 4, 7챕터에서 다룬다.

  1. 지연 프로비저닝, 홈 오브젝트 헤더에 저장. 힙은 VFID를 HEAP_HDR_STATS.ovf_vfid에, B+Tree는 루트 헤더 ovfidBTID_INT::ovfid 인메모리 미러에 보관한다.
  2. heap_ovf_find_vfid는 find-or-create를 하나의 함수에서 처리한다. docreate=true(오직 heap_ovf_insert에서만)는 sysop 아래 1페이지 FILE_MULTIPAGE_OBJECT_HEAP을 생성하고, 클래스 TDE를 적용하며, RVHF_STATS undo/redo를 기록한다. docreate=false는 기존 VFID를 읽는다.
  3. btree_create_overflow_key_fileBTID_INT::ovfid만 채우고, 호출자가 이를 영속화한다. 최소 3페이지를 사전 할당하고 클래스 TDE를 적용하며, 실패 시 VFID를 null로 돌린다. 호출자는 루트 write latch 하에서 VFID_ISNULL을 재확인하고 RVBT_UPDATE_OVFID로 루트 쓰기를 기록한다.
  4. 힙 홈 참조는 slotid = NULL_SLOTIDREC_BIGONE OID이다. {volid, pageid}만 유효하다 — overflow 레코드에는 슬롯 디렉토리가 없다.
  5. 디스크 레벨 식별자는 파일이 아니라 페이지 타입이다. 힙 big-record와 B+Tree overflow key 체인은 PAGE_OVERFLOW를 사용하고, overflow OID list 체인은 btree_get_new_page / btree_initialize_new_page로 할당되며 PAGE_BTREE를 사용한다.
  6. 각 체인 종류는 고유한 소유 파일을 갖는다. B+Tree overflow key 체인은 BTID_INT::ovfid (FILE_BTREE_OVERFLOW_KEY)에, overflow OID list 체인은 메인 트리 파일 (btid->sys_btid->vfid)에 위치한다. 리프 플래그 BTREE_LEAF_RECORD_OVERFLOW_KEY (0x4000) vs BTREE_LEAF_RECORD_OVERFLOW_OIDS (0x2000)로 어느 쪽을 따를지 판단한다.
  7. 모든 VFID 쓰기는 크래시 앞에서 원자적이다. 두 서브시스템 모두 파일 생성과 헤더/VFID 쓰기를 sysop으로 감싸므로, 크래시 후 파일도 참조도 없거나 둘 다 있거나 — 파일만 누출된 중간 상태는 없다.

Chapter 3: overflow_insert로 체인 만들기

섹션 제목: “Chapter 3: overflow_insert로 체인 만들기”

이 챕터는 하나의 질문에 답한다. 여러 페이지에 걸친 RECDESVFID가 주어졌을 때, CUBRID는 어떻게 전체 overflow 체인 — 페이지 할당, 페이로드 분배, 선행 로깅(WAL) — 을 단일 순방향 패스에서 원자적으로 구성하는가? 그 답이 overflow_insert다. 이를 호출하는 두 caller, heap_ovf_insertbtree_store_overflow_keyRECDES를 조립하고 FILE_TYPE을 선택한 뒤, 첫 페이지의 VPID를 caller 친화적인 주소로 변환하는 역할만 한다. 이 세 함수와 모든 분기를 낱낱이 해부한다.

체인 레이아웃(OVERFLOW_FIRST_PART / OVERFLOW_REST_PART)과 “overflow가 왜 존재하는가”에 관한 이론은 Chapter 1과 동반 문서 cubrid-overflow-file.md를 참고하라. 여기서는 쓰기 경로에 집중한다.

3.1 writer가 채우는 두 구조체 (요약 복습)

섹션 제목: “3.1 writer가 채우는 두 구조체 (요약 복습)”

overflow_insert는 페이지에 두 가지 헤더 형태를 기록한다 — OVERFLOW_FIRST_PART(페이지 0)와 OVERFLOW_REST_PART(페이지 1..n-1). 각 필드의 상세 설명은 Chapter 1에서 다뤘으며, 쓰기 경로에서 실제로 건드리는 필드는 세 가지다. next_vpid는 두 구조체 모두에 있으며, vpids[i+1]에서 가져온 전방 링크로서 마지막 페이지와 npages == 1인 경우 페이지 0에서는 NULL이다. length는 first 구조체에만 있으며, 전체 레코드의 바이트 수를 담아 reader가 체인을 끝까지 탐색하지 않고도 버퍼 크기를 결정할 수 있게 한다(Chapter 4). data[1]은 두 구조체 모두에 있는 유연 배열로, 헤더 뒤에 memcpy되며 offsetof(..., data)가 페이지당 페이로드 용량이 된다.

OVERFLOW_REST_PARTlength를 생략하기 때문에, rest 페이지의 페이로드 용량은 first 페이지보다 정확히 sizeof(int) 만큼 크다 — 이 비대칭이 3.2절의 수식이 성립하는 근거다.

디스크에 손을 대기 전, overflow_insert는 정확한 페이지 수를 계산한다 — 나중에 보정하는 추정치가 아니라 정확한 값이며, 뒤따르는 assert(length == 0)(3.7)이 그 정확성을 증명한다.

// overflow_insert -- src/storage/overflow_file.c
length = recdes->length - (DB_PAGESIZE - (int) offsetof (OVERFLOW_FIRST_PART, data));
if (length > 0)
{
i = DB_PAGESIZE - offsetof (OVERFLOW_REST_PART, data); /* rest-page payload capacity */
npages = 1 + CEIL_PTVDIV (length, i); /* 1 first page + ceil(rest/cap) */
}
else
{
npages = 1; /* whole record fits in page 0 */
}

length는 “페이지 0에 들어가지 않는 바이트 수”라는 의미로 재활용된다. <= 0이면 npages == 1 경로가 실행된다. 그렇지 않으면 CEIL_PTVDIV가 나머지를 rest 페이지 용량으로 올림 나눗셈하고, 1 +로 페이지 0을 다시 더한다.

불변 조건 — 페이지 수는 정확한 값이며 상한값이 아니다. overflow_insert는 정확히 npages개를 할당하며, 루프는 레코드를 바이트 단위까지 소비해야 한다(assert(length == 0), 3.7). 두 offsetof 값이 어긋나면 루프는 바이트를 기록하지 못하거나 할당되지 않은 vpids[npages]를 인덱싱하게 된다.

3.3 VPID 배열: 스택 버퍼 대 malloc, 그리고 OOM 분기

섹션 제목: “3.3 VPID 배열: 스택 버퍼 대 malloc, 그리고 OOM 분기”

overflow_insertnpages + 1개의 VPID가 필요하다 — 페이지마다 하나, 그리고 sentinel 슬롯 하나. 일반적인 경우 힙 할당을 피하기 위해 스택 버퍼를 유지하고, 체인이 클 때만 malloc한다.

// overflow_insert -- src/storage/overflow_file.c
#define OVERFLOW_ALLOCVPID_ARRAY_SIZE 64 /* (top of file) */
VPID vpids_buffer[OVERFLOW_ALLOCVPID_ARRAY_SIZE + 1]; /* +1 for the sentinel slot */
// ...
if (npages > OVERFLOW_ALLOCVPID_ARRAY_SIZE)
{
vpids = (VPID *) malloc ((npages + 1) * sizeof (VPID));
if (vpids == NULL)
{
er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_OUT_OF_VIRTUAL_MEMORY, 1, (npages + 1) * sizeof (VPID));
return ER_OUT_OF_VIRTUAL_MEMORY; /* <- direct return: nothing acquired yet */
}
}
else
{
vpids = vpids_buffer; /* point at the stack buffer */
}
  • npages > 64 malloc OOMer_set단순 return, goto exit_on_error아니다: 아직 sysop이 시작되지 않았고 fix된 페이지도 없으므로 abort하거나 해제할 것이 없다.
  • npages <= 64vpidsvpids_buffer의 별칭이 된다. if (vpids != vpids_buffer)가 유일한 free 신호다. 임계값은 공간/속도 트레이드오프 — 대부분의 레코드는 64 페이지 미만이다.

debug 전용(!NDEBUG) 사전 초기화 및 할당 후 assert. NDEBUG가 꺼진 빌드에서는 for (i...) VPID_SET_NULL(&vpids[i])로 모든 슬롯을 null로 채운 뒤, file_alloc_multiple 이후 대칭 루프에서 !VPID_ISNULL(&vpids[i])를 assert한다. 이 null 초기화는 debug 전용이지만, sentinel VPID_SET_NULL(&vpids[npages])모든 빌드에서 실행된다. 루프는 마지막 페이지의 next_vpid에 이를 기록하는데(i == npages-1일 때 vpids[i+1]vpids[npages]를 읽는다), 이로써 체인이 종료된다. 이 sentinel이 없으면 마지막 전방 링크가 garbage가 되어 Chapter 4의 순회가 끝나지 않는다.

3.4 시스템 오퍼레이션 열기와 전체 페이지 일괄 할당

섹션 제목: “3.4 시스템 오퍼레이션 열기와 전체 페이지 일괄 할당”
// overflow_insert -- src/storage/overflow_file.c
log_sysop_start (thread_p);
is_sysop_started = true;
error_code = file_alloc_multiple (thread_p, ovf_vfid,
file_type != FILE_TEMP ? file_init_page_type : file_init_temp_page_type,
&ptype, npages, vpids);
if (error_code != NO_ERROR)
{ ASSERT_ERROR (); goto exit_on_error; }
*ovf_vpid = vpids[0]; /* head VPID returned to caller */

log_sysop_start는 중첩 시스템 오퍼레이션을 연다. 성공 시 외부 트랜잭션에 연결되고, 실패 시 abort된다(둘 다 3.7에서 처리). 이로써 체인 생성은 원자적이 된다. file_alloc_multiplenpages개를 한 번에 확보하며(vpids[0..npages-1] 채움), initializer는 file_type에 따라 분기한다. FILE_TEMP이면 file_init_temp_page_type, 그 외에는 file_init_page_type — 둘 다 &ptype으로 PAGE_OVERFLOW를 스탬핑한다. 실패 시 goto exit_on_error. 유일한 출력 *ovf_vpid = vpids[0]이 체인 head다.

3.5 페이지별 복사 루프 — first-part 대 rest-part 분기

섹션 제목: “3.5 페이지별 복사 루프 — first-part 대 rest-part 분기”

단일 for 루프가 각 페이지를 fix하고, 헤더와 페이로드 슬라이스를 기록하고, 로깅하고, 해제한다. 구조적 분기는 i == 0(first part)과 else(rest) 두 가지뿐이다.

// overflow_insert -- src/storage/overflow_file.c
data = recdes->data;
length = recdes->length; /* re-used: now the running remainder */
for (i = 0; i < npages; i++)
{
addr.pgptr = pgbuf_fix (thread_p, &vpids[i], OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH);
if (addr.pgptr == NULL)
{ ASSERT_ERROR_AND_SET (error_code); goto exit_on_error; } /* fix failed: abort whole chain */
// ... !NDEBUG pgbuf_check_page_ptype (addr.pgptr, PAGE_OVERFLOW) ...
if (i == 0)
{
first_part = (OVERFLOW_FIRST_PART *) addr.pgptr;
first_part->next_vpid = vpids[i + 1]; /* link to page 1 (or sentinel NULL) */
first_part->length = length; /* TOTAL length, written once */
copyto = (char *) first_part->data;
copy_length = DB_PAGESIZE - offsetof (OVERFLOW_FIRST_PART, data);
if (length < copy_length) copy_length = length; /* last/only page: clamp */
if (file_type != FILE_TEMP)
log_append_empty_record (thread_p, LOG_DUMMY_OVF_RECORD, &addr); /* first page only */
}
else
{ /* rest part: identical, minus the length field */
rest_parts = (OVERFLOW_REST_PART *) addr.pgptr;
rest_parts->next_vpid = vpids[i + 1]; /* sentinel NULL on last page */
copyto = (char *) rest_parts->data;
copy_length = DB_PAGESIZE - offsetof (OVERFLOW_REST_PART, data);
if (length < copy_length) copy_length = length;
}
memcpy (copyto, data, copy_length);
// ... redo logging (3.6) ...
data += copy_length; /* advance source cursor */
length -= copy_length; /* shrink remainder */
pgbuf_set_dirty_and_free (thread_p, addr.pgptr);
}

각 페이지는 한 이터레이션 안에서 fix-then-free되므로 동시에 최대 한 페이지만 latch를 보유한다. pgbuf_fix 실패 시 3.7의 abort가 나머지를 정리한다. length < copy_length 클램프는 마지막 부분 페이지에서 발동하며, npages == 1인 경우(단독 페이지)에도 동일하게 적용된다.

flowchart TD
  FIX["pgbuf_fix vpids[i] WRITE latch"]
  FIX -->|NULL| ERR["ASSERT_ERROR_AND_SET; goto exit_on_error"]
  FIX -->|ok| Q{"i == 0 ?"}
  Q -->|yes| FIRST["first_part: next_vpid, length,\nfirst-part 용량, clamp;\nFILE_TEMP 아니면 LOG_DUMMY_OVF_RECORD"]
  Q -->|no| REST["rest_parts: next_vpid,\nrest-part 용량, clamp"]
  FIRST --> CP["memcpy copy_length"]
  REST --> CP
  CP --> REDO{"FILE_TEMP 아니고\nno_logging != true ?"}
  REDO -->|yes| LOGR["log_append_redo_data RVOVF_NEWPAGE_INSERT"]
  REDO -->|no| ADV["data += copy_length; length -= copy_length"]
  LOGR --> ADV
  ADV --> FREE["pgbuf_set_dirty_and_free"]
  FREE --> NEXT{"i+1 < npages ?"}
  NEXT -->|yes| FIX
  NEXT -->|no| DONE["assert length == 0"]

Figure 3-1. 복사 루프 한 패스: fix 실패, first/rest 헤더 분기, redo 로깅 분기.

3.6 로깅: 더미 첫 페이지 마커와 페이지별 redo

섹션 제목: “3.6 로깅: 더미 첫 페이지 마커와 페이지별 redo”

두 가지 로그 레코드가 발행된다(3.5 코드 발췌에 모두 보인다).

  • LOG_DUMMY_OVF_RECORD (log_append_empty_record, i == 0 분기) — 레코드로, FILE_TEMP가 아닌 체인의 페이지 0에만, 한 번만 기록된다. 로그 상에서 체인 헤드를 고정하여 recovery/vacuum이 이를 소유 레코드와 연관지을 수 있게 한다(Chapter 8). temp 파일은 복구 대상이 아니므로 생략한다.
  • RVOVF_NEWPAGE_INSERT (log_append_redo_data, memcpy 이후) — 페이지마다 기록되며, file_type != FILE_TEMP 그리고 thread_p->no_logging != true 두 조건을 모두 만족해야 발행된다. 로깅 길이 copy_length + CAST_BUFLEN(copyto - addr.pgptr)는 기록된 접두 전체(헤더 데이터)를 포함하므로 redo 한 번으로 페이지를 재구성할 수 있다. no_logging은 실제 페이지를 할당하면서도 redo를 억제하는 벌크 경로를 위한 것이다.

불변 조건 — temp 체인은 절대 로깅하지 않고, persistent 체인은 no_logging이 아닌 한 항상 redo를 로깅한다. 이 게이팅은 3.4의 페이지 초기화 분기와 반드시 일치해야 한다. temp 파일에 더미 레코드를 남기면 recovery가 크래시 후 살아남지 못한 체인을 따라가고, persistent 체인에서 redo를 생략하면 페이지가 stale 상태로 남는다.

두 종료 경로는 sysop 브래킷을 중심으로 서로를 대칭적으로 반영한다.

// overflow_insert -- src/storage/overflow_file.c
assert (length == 0); /* 3.2's "exact" invariant, proven here */
#if defined (CUBRID_DEBUG)
if (length > 0)
{ assert (false); er_log_debug (ARG_FILE_LINE, "ovf_insert: ** SYSTEM ERROR ... # of pages ... incorrect");
error_code = ER_FAILED; goto exit_on_error; } /* recoverable miscalc, not a hard assert */
#endif
log_sysop_attach_to_outer (thread_p); /* SUCCESS: undone with the outer op, not standalone */
if (vpids != vpids_buffer) free_and_init (vpids);
return NO_ERROR;
exit_on_error:
if (is_sysop_started) log_sysop_abort (thread_p); /* roll back every page alloc + write */
if (vpids != vpids_buffer) free_and_init (vpids);
return error_code;

성공 시, length가 0이 아니라면 3.2의 수식과 루프가 불일치한 것이다. CUBRID_DEBUG에서만 이를 goto exit_on_error를 통한 복구 가능한 ER_FAILED 롤백으로 에스컬레이션하며, 일반 빌드에서는 bare assert에 그친다. log_sysop_attach_to_outer는 sysop을 독립 커밋(sysop_commit)하는 대신 외부 트랜잭션의 undo 체인에 연결하므로, 체인은 외부 오퍼레이션과 함께 undo된다. 에러 경로에서는 is_sysop_started 플래그가 abort를 보호한다 — log_sysop_start 이전에 label에 도달하는 goto는 없다(초기 malloc-OOM은 label을 피해 return한다). 따라서 label에 도착하면 sysop을 abort하여 file_alloc_multiple을 되돌리고 redo를 파기한다. 두 종료 경로는 모두 vpids != vpids_buffer free 가드를 공유한다.

불변 조건 — 체인 생성은 전부 아니면 전무다. overflow_insertNO_ERROR를 반환하며 완전히 기록되고, 로깅되고, 외부에 연결된 체인을 남기거나, 에러를 반환하며 한 페이지도 남기지 않는다 — sysop 브래킷이 이를 강제한다. 루프 중간에 커밋하면 크래시 시 할당됐지만 비어있는, 소유자 없는 페이지가 남게 된다.

3.8 overflow_insert를 호출하는 두 래퍼

섹션 제목: “3.8 overflow_insert를 호출하는 두 래퍼”

두 개의 얇은 래퍼가 overflow_insert의 인수를 준비한다. **heap_ovf_insert(힙 멀티페이지 오브젝트)**는 힙의 overflow VFID를 확인하고, FILE_MULTIPAGE_OBJECT_HEAP으로 overflow_insert를 호출한 뒤, head VPIDOID로 변환한다.

// heap_ovf_insert -- src/storage/heap_file.c
if (heap_ovf_find_vfid (thread_p, hfid, &ovf_vfid, true, PGBUF_UNCONDITIONAL_LATCH) == NULL
|| overflow_insert (thread_p, &ovf_vfid, &ovf_vpid, recdes, FILE_MULTIPAGE_OBJECT_HEAP) != NO_ERROR)
return NULL; /* no overflow file, or insert failed */
ovf_oid->pageid = ovf_vpid.pageid; ovf_oid->volid = ovf_vpid.volid;
ovf_oid->slotid = NULL_SLOTID; /* overflow OIDs have no slot */
return ovf_oid;

||는 두 실패 분기를 하나로 묶는다(true 인수는 overflow 파일이 없을 경우 생성한다). 성공 시 head VPIDovf_oidpageid/volid를 채우며, slotid = NULL_SLOTID — 슬롯이 아닌 페이지 단위로 주소가 지정된다(Chapter 2).

btree_store_overflow_key(초과 크기 인덱스 키). DB_VALUE 키를 RECDES로 직렬화한 뒤 FILE_BTREE_OVERFLOW_KEYoverflow_insert를 호출한다.

// btree_store_overflow_key -- src/storage/btree.c
if (src_type != dst_type) /* key domain differs from index domain */
{ key_ptr = &new_key;
status = tp_value_cast (key, key_ptr, tp_domain, false);
if (status != DOMAIN_COMPATIBLE) { assert (false); goto exit_on_error; }
size = btree_get_disk_size_of_key (key_ptr); }
rec.data = (char *) db_private_alloc (thread_p, size); /* OOM -> er_set + goto exit_on_error */
// ... or_init + pr_type->index_writeval (failure -> goto exit_on_error); rec.length = serialized len ...
if (overflow_insert (thread_p, &overflow_file_vfid, first_overflow_page_vpid, &rec, FILE_BTREE_OVERFLOW_KEY) != NO_ERROR)
{ ASSERT_ERROR (); goto exit_on_error; }

노드 타입이 tp_domain을 사전 선택하며, 도메인 불일치 시 키를 캐스팅한다(non-DOMAIN_COMPATIBLEassert(false) 처리). db_private_alloc OOM, index_writeval 실패, overflow_insert 실패는 각각 goto exit_on_error로 이어진다(rec.data/key_ptr 해제, ER_FAILED로 정규화). 힙과 달리 btree는 *raw VPID*를 유지한다 — 인덱스가 이미 체인을 소유하기 때문이다.

  1. 페이지 수는 선행에서 정확하게 계산된다: npages = 1 + CEIL_PTVDIV(rest_bytes, rest_capacity) 또는 1. 첫 페이지는 sizeof(int) 만큼 페이로드가 적다(length 필드를 담기 때문) — 이 부담이 되는 비대칭은 assert(length == 0)으로 정확성이 증명된다.
  2. 64슬롯 스택 버퍼로 대부분의 경우 malloc을 피한다. npages > OVERFLOW_ALLOCVPID_ARRAY_SIZE일 때만 malloc하며(OOM 시 직접 return), vpids != vpids_buffer가 유일한 free 신호다. vpids[npages] sentinel은 모든 빌드에서 마지막 next_vpid를 종료한다.
  3. 체인은 단일 시스템 오퍼레이션 안에서 구성된다log_sysop_startfile_alloc_multiple … 루프 … 성공 시 log_sysop_attach_to_outer / 에러 시 log_sysop_abort: 전부 아니면 전무.
  4. 루프의 유일한 구조적 분기는 first-part 대 rest-part다. 페이지 0은 next_vpid + 전체 length + 페이로드를 기록하고, rest 페이지는 length를 생략한다. 각 이터레이션은 한 페이지를 fix, 기록, 로깅, 해제한다 — 단일 페이지 latch 풋프린트.
  5. 로깅은 파일 타입으로 게이팅되며, 래퍼는 인수만 조립한다. LOG_DUMMY_OVF_RECORD는 페이지 0에 한 번, RVOVF_NEWPAGE_INSERT는 페이지마다 — 둘 다 비temp 파일에서만(redo는 no_logging으로도 게이팅). heap_ovf_insertFILE_MULTIPAGE_OBJECT_HEAP을 선택하고 NULL_SLOTID를 가진 OID를 반환한다. btree_store_overflow_keyFILE_BTREE_OVERFLOW_KEY를 선택하고 raw VPID를 유지한다.

3장에서 체인을 구성했다. 총 length를 보유하는 OVERFLOW_FIRST_PART 헤드 페이지 하나, 그리고 next_vpid로 연결된 0개 이상의 OVERFLOW_REST_PART 페이지가 NULL까지 이어지는 구조다. 이 장에서 다루는 질문은 헤드 VPID가 주어졌을 때, 호출자가 체인의 전부 또는 일부를 어떻게 읽으며, 그 순회를 어떻게 안전하게 유지하는가이다. 읽기 인터페이스는 작다 — 공개 진입점 세 개, 진단용 순회 하나, 플러시 순회 하나, 내부 함수 두 개 — 이 중 어느 것도 lock을 잡지 않는다. 홈 레퍼런스의 row/key lock이 접근 관문 역할을 한다(companion §“Pages not individually locked”).

새로운 구조체는 도입되지 않는다. 읽기는 1장에서 정의한 것을 소비한다(overflow_first_part = {VPID next_vpid; int length; char data[1];}, overflow_rest_part = {VPID next_vpid; char data[1];}). 각 필드의 상세 설계 근거는 1장을 참고하고, 여기서는 읽기 시점의 역할만 정리한다.

필드역할이유
overflow_first_part.next_vpid헤드의 data[]를 소비한 뒤 따라가는 2번째 페이지 링크헤드의 offset-0에서만 읽는다(VPID_EQ로 판단); 여기가 NULL이면 단일 페이지 레코드
overflow_first_part.length전체 바이트 수를 O(1)로 미리 파악하는 권위 있는 값헤드 전용이므로 길이 조회 시 순회 불필요; 바이트 예산(4.4)과 페이지 수 계산(4.9)의 기준값
overflow_first_part.data[]첫 번째 페이로드 슬라이스앞쪽 OR_MVCC_MAX_HEADER_SIZE 바이트가 스냅샷 게이트가 읽는 MVCC 헤더
overflow_rest_part.next_vpid다음 rest 페이지 링크동일한 offset-0 위치; NULL이 체인 종료를 표시 — 바이트가 남아 있는데 NULL이면 데이터 손상(4.4)
overflow_rest_part.data[]페이로드 연속 데이터rest 페이지는 length가 없으므로 헤드보다 sizeof(int) 만큼 더 많은 페이로드를 담는다

불변 조건 (ID 기반 헤드/rest 구별). 읽기 중 어떤 페이지가 헤드인지는 그 VPID가 호출자의 ovf_vpid와 같은지로만 결정된다 — “is-head” 플래그는 없고 VPID_EQ 비교(overflow_next_vpid, 4.2)만 있다. rest 페이지 VPID를 넘기면, length 4바이트를 data[] 시작 부분으로 잘못 읽어 조용히 오순회한다. 따라서 모든 진입점은 홈 REC_BIGONE / 리프 레퍼런스에서 가져온 첫 번째 페이지 VPID를 받아야 한다.

flowchart LR
  HEAD["헤드 페이지 == ovf_vpid\nnext_vpid -> R1\nlength = 전체 길이\ndata[ MVCC hdr then body ]"]
  R1["rest 페이지\nnext_vpid -> R2\ndata[ body ]"]
  R2["rest 페이지\nnext_vpid = NULL\ndata[ body end ]"]
  HEAD -->|next_vpid| R1 -->|next_vpid| R2

Figure 4-1 — 리더가 보는 구조: 헤드는 오직 호출자의 VPID와의 동등성으로만 구별되며, length는 헤드에만 존재한다.

4.2 overflow_next_vpid — 헤드/rest 선택자

섹션 제목: “4.2 overflow_next_vpid — 헤드/rest 선택자”
// overflow_next_vpid -- src/storage/overflow_file.c
if (VPID_EQ (ovf_vpid, vpid)) /* <- still the original head? */
*vpid = ((OVERFLOW_FIRST_PART *) pgptr)->next_vpid; /* link sits after length */
else
*vpid = ((OVERFLOW_REST_PART *) pgptr)->next_vpid; /* rest layout, offset 0 */

분기는 두 개, 오류 경로는 없다. overflow_traverse만이 이 함수를 호출한다(매 반복마다 ovf_vpid에서 다시 순회하며 “아직 헤드인가?”를 물어야 한다). overflow_get_nbytesoverflow_get_capacity는 판별 로직을 인라인으로 구현한다 — 루프 전에 헤드를 처리하고, 루프 본체는 rest 페이지만 다룬다.

4.3 overflow_get_length — O(1) 헤드 단독 읽기

섹션 제목: “4.3 overflow_get_length — O(1) 헤드 단독 읽기”
// overflow_get_length -- src/storage/overflow_file.c
pgptr = pgbuf_fix (thread_p, ovf_vpid, OLD_PAGE, PGBUF_LATCH_READ, PGBUF_UNCONDITIONAL_LATCH);
if (pgptr == NULL) return -1; /* <- only failure branch */
length = ((OVERFLOW_FIRST_PART *) pgptr)->length; /* head-only field */
pgbuf_unfix_and_init (thread_p, pgptr);
return length;

오류 분기 하나(-1 반환), 성공 경로 하나, 읽기 전용이므로 PGBUF_LATCH_READ. -1 센티넬 값은 btree_load_overflow_key(4.7)가 버퍼 크기를 결정하기 전에 확인하는 값이다.

4.4 overflow_get_nbytes — 부분 읽기, 분기별 분석

섹션 제목: “4.4 overflow_get_nbytes — 부분 읽기, 분기별 분석”

이 장의 핵심 함수다. start_offset부터 max_nbytes 바이트를 읽고, MVCC 게이트를 적용하고, 버퍼 크기 부족 힌트를 전달하며, 페이지 경계를 넘어 복사한다. overflow_get(4.5)과 heap_ovf_get(4.6)이 이 함수를 래핑한다.

Step 1 — 헤드 fix: pgbuf_fix로 헤드를 고정한다. fix 실패 시 S_ERROR 반환.

Step 2 — MVCC 게이트 (heap 경로 전용): 스냅샷이 있을 때만 실행된다. heap_get_mvcc_rec_header_from_overflowoverflow_get_first_page_data (pgptr)(실제 접근자, ((OVERFLOW_FIRST_PART *) pgptr)->data 반환)을 RECDES에 연결한 뒤 or_mvcc_get_header로 헤드의 data[]를 디코딩한다.

// overflow_get_nbytes -- src/storage/overflow_file.c
if (mvcc_snapshot != NULL)
{
heap_get_mvcc_rec_header_from_overflow (pgptr, &mvcc_header, NULL); /* decode head data[] */
if (mvcc_snapshot->snapshot_fnc (thread_p, &mvcc_header, mvcc_snapshot) == TOO_OLD_FOR_SNAPSHOT)
{
/* not satisfied ONLY on TOO_OLD; TOO_NEW (e.g. just-updated record locked at select) is accepted */
pgbuf_unfix_and_init (thread_p, pgptr);
return S_SNAPSHOT_NOT_SATISFIED;
}
}

세 가지 판정 중 TOO_OLD_FOR_SNAPSHOT만 중단시킨다. TOO_NEW와 만족된 경우는 그냥 통과하므로, 방금 갱신된 big 레코드에 대한 SELECT ... FOR UPDATE도 lock을 걸기 위한 후보로 계속 처리된다. mvcc_snapshot == NULL이면(overflow-key 경로, 4.7) 이 블록 전체를 건너뛴다.

Step 3–4 — 바이트 예산 계산, 버퍼 크기 판정.

// overflow_get_nbytes -- src/storage/overflow_file.c
*remaining_length = first_part->length;
if (max_nbytes < 0) max_nbytes = *remaining_length - start_offset; /* "rest from offset" */
else if (max_nbytes > *remaining_length - start_offset) max_nbytes = *remaining_length - start_offset; /* clamp */
if (max_nbytes < 0) { max_nbytes = 0; *remaining_length = 0; } /* offset past record end -> empty */
else *remaining_length -= max_nbytes;
// ... condensed ...
if (max_nbytes > recdes->area_size) /* too small */
{ pgbuf_unfix_and_init (...); recdes->length = -max_nbytes; return S_DOESNT_FIT; } /* neg = needed bytes */
else if (max_nbytes == 0) /* empty / past-end */
{ pgbuf_unfix_and_init (...); recdes->length = 0; return S_SUCCESS; }
recdes->length = max_nbytes;

네 가지 예산/판정 분기 모두 인라인으로 주석 처리되어 있다. 핵심 세부 사항은 S_DOESNT_FIT 반환 시 필요한 크기를 음수 recdes->length로 전달한다는 점이다(호출자가 이 값을 보고 재시도한다 — 4.6).

Step 5 — 탐색 후 복사 루프, 헤드의 data[]에서 시작한다.

// overflow_get_nbytes -- src/storage/overflow_file.c (loop, condensed)
data = recdes->data; copyfrom = (char *) first_part->data; next_vpid = first_part->next_vpid;
while (max_nbytes > 0)
{
if (start_offset > 0) /* seek: copy_length = min(start_offset, bytes-left-on-page); advance copyfrom */
{ ... ; start_offset -= copy_length; copyfrom += copy_length; }
if (start_offset == 0) /* copy: copy_length = min(max_nbytes, bytes-left-on-page), page-bounded */
{ ... ; if (copy_length > 0) { memcpy (data, copyfrom, copy_length); data += copy_length; max_nbytes -= copy_length; } }
pgbuf_unfix_and_init (thread_p, pgptr); /* <- unfix BEFORE fixing the next page */
if (max_nbytes > 0)
{
if (VPID_ISNULL (&next_vpid)) /* corruption guard, see callout */
{ er_set (..., ER_HEAP_OVFADDRESS_CORRUPTED, 3, ovf_vpid->volid, ovf_vpid->pageid, NULL_SLOTID); return S_ERROR; }
pgptr = pgbuf_fix (thread_p, &next_vpid, OLD_PAGE, PGBUF_LATCH_READ, PGBUF_UNCONDITIONAL_LATCH);
if (pgptr == NULL) { recdes->length = 0; return S_ERROR; }
rest_parts = (OVERFLOW_REST_PART *) pgptr; /* every later page is a rest page */
copyfrom = (char *) rest_parts->data; next_vpid = rest_parts->next_vpid;
}
}
return S_SUCCESS;

...은 현재 고정된 페이지에 남은 바이트만큼 copy_length를 제한한다. 탐색과 복사는 한 반복 안에서 이어진다(페이지 끝까지 탐색 후 즉시 복사로 fall-through).

불변 조건 (한 번에 하나의 페이지만 latch; 체인 길이 ≥ length 바이트). 각 페이지는 다음 페이지를 fix하기 전에 pgbuf_unfix_and_init된다 — 이로써 read-latch 점유 범위를 최소화하고 latch 순서 데드락을 방지한다. unfix-N/fix-(N+1) 구간 동안의 구조적 변경은 홈 레퍼런스의 lock이 직렬화한다(companion Open Question 5). 그리고 max_nbytes > 0인 상태에서 next_vpid가 NULL이면 체인이 선언된 length보다 일찍 끝난 것이므로, 코드는 ER_HEAP_OVFADDRESS_CORRUPTED를 발생시킨다 — 쓰기 측이 length와 페이지 수를 일치시켜야 한다는 의무의 대응 쌍이다.

한 줄짜리 래퍼다. overflow_getoverflow_get_nbytes (..., 0, -1, &remaining_length, mvcc_snapshot)을 반환한다 — start_offset = 0, max_nbytes = -1(“전부”), remaining_length는 버림. heap_ovf_get(4.6)과 btree_load_overflow_key(4.7)가 전체 레코드를 읽을 때 이 함수를 호출한다.

heap_ovf_getREC_BIGONE OID를 VPID로 변환하고, CHN(cache-coherency-number) 빠른 경로를 추가한다.

// heap_ovf_get -- src/storage/heap_file.c (condensed)
ovf_vpid.pageid = ovf_oid->pageid; ovf_vpid.volid = ovf_oid->volid;
if (chn != NULL_CHN) /* peek only the MVCC-header prefix to compare CHN */
{
scan = overflow_get_nbytes (thread_p, &ovf_vpid, recdes, 0, OR_MVCC_MAX_HEADER_SIZE, &rest_length, mvcc_snapshot);
if (scan == S_SUCCESS && chn == or_chn (recdes))
return S_SUCCESS_CHN_UPTODATE; /* cached copy current; skip full read */
}
return overflow_get (thread_p, &ovf_vpid, recdes, mvcc_snapshot); /* full read */

S_SUCCESS + CHN 일치 시에만 S_SUCCESS_CHN_UPTODATE로 단락된다. 그 외(짧은 읽기 S_SNAPSHOT_NOT_SATISFIED / S_DOESNT_FIT, 또는 NULL_CHN)는 전체 overflow_get으로 fall-through한다. heap_get_bigone_contentS_DOESNT_FIT 재시도 루프를 소유한다.

// heap_get_bigone_content -- src/storage/heap_file.c (condensed)
if (scan_cache != NULL && (ispeeking == PEEK || recdes->data == NULL || ...)) /* reusable area available */
{
scan_cache->assign_recdes_to_area (*recdes);
while ((scan = heap_ovf_get (thread_p, forward_oid, recdes, NULL_CHN, NULL)) == S_DOESNT_FIT)
scan_cache->assign_recdes_to_area (*recdes, (size_t) (-recdes->length)); /* grow to needed size, retry */
if (scan != S_SUCCESS) recdes->data = NULL;
}
else
scan = heap_ovf_get (thread_p, forward_oid, recdes, NULL_CHN, NULL); /* caller-owned buffer, no retry */
return scan;

두 분기 모두 인라인 주석으로 설명된다. 재사용 가능 영역 분기는 S_DOESNT_FIT에서 루프하고, 호출자 소유 버퍼 분기는 재시도하지 않는다. 양쪽 모두 NULL_CHNmvcc_snapshot = NULL을 전달한다 — 스냅샷은 REC_BIGONE에 도달한 가시 버전 순회 상류에서 이미 확인되었다(companion §“Walk: heap read of a big record”).

4.7 btree_load_overflow_key — 크기 확인, 할당, 읽기, 항상 복사

섹션 제목: “4.7 btree_load_overflow_key — 크기 확인, 할당, 읽기, 항상 복사”

유일한 non-heap 소비자다.

// btree_load_overflow_key -- src/storage/btree.c (condensed)
rec.area_size = overflow_get_length (thread_p, first_overflow_page_vpid);
if (rec.area_size == -1) return ER_FAILED; /* length-query failure */
rec.data = (char *) db_private_alloc (thread_p, rec.area_size); /* NULL -> ER_OUT_OF_VIRTUAL_MEMORY */
if (overflow_get (thread_p, first_overflow_page_vpid, &rec, NULL) != S_SUCCESS) goto exit_on_error; /* NULL snapshot */
or_init (&buf, rec.data, rec.length);
pr_type->index_readval (&buf, key, btid->key_type, -1, true /*copy*/, NULL, 0); /* always copy overflow keys */

비자명한 선택이 두 가지 있다. mvcc_snapshot = NULL은 지름길이 아니라 의미상 올바른 값이다. overflow key에는 MVCC 헤더가 없으며, 해당 key에 붙은 OID 리스트의 가시성이 접근 가능성을 결정한다. 그리고 index_readval (..., /*copy=*/true, ...)는 항상 복사하며 절대 peek하지 않는다. overflow_get은 반환 전에 페이지를 unfix하기 때문에, overflow key는 인라인 key처럼 리프 latch 하에서 peek할 수 없기 때문이다. 사전 크기 계산 덕분에 이 overflow_getS_DOESNT_FIT을 반환하지 않는다.

4.8 overflow_traverseoverflow_flush — 워커 스켈레톤

섹션 제목: “4.8 overflow_traverse와 overflow_flush — 워커 스켈레톤”

삭제(6장)와 플러시가 overflow_traverse를 공유한다. 여기서는 OVERFLOW_DO_FLUSH 분기만 다룬다.

// overflow_traverse -- src/storage/overflow_file.c (condensed)
next_vpid = *ovf_vpid;
while (!(VPID_ISNULL (&next_vpid)))
{
pgptr = pgbuf_fix (thread_p, &next_vpid, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH);
if (pgptr == NULL) goto exit_on_error;
vpid = next_vpid;
overflow_next_vpid (ovf_vpid, &next_vpid, pgptr); /* read link BEFORE acting on page */
switch (func)
{
case OVERFLOW_DO_DELETE: ... /* needs ovf_vfid != NULL; deallocates page -- Chapter 6 */
case OVERFLOW_DO_FLUSH:
if (overflow_flush_internal (thread_p, pgptr) != NO_ERROR) goto exit_on_error;
break;
default: break;
}
}
return ovf_vpid;

순회는 페이지를 콜백에 넘기기 전에 overflow_next_vpid다음 링크를 읽는다 — 삭제 시 페이지가 곧 해제되므로 필수적이며, 두 분기 모두에 일관되게 적용된다. overflow_flush_internal은 단일 분기(pgbuf_flush_with_wal, WAL: 페이지보다 로그 먼저)이고, overflow_flushoverflow_traverse (..., NULL, ovf_vpid, OVERFLOW_DO_FLUSH)를 호출하는 void 래퍼다. 플러시임에도 PGBUF_LATCH_WRITE로 페이지를 고정하며, DELETE 분기의 ovf_vfid != NULL 가드는 실행되지 않는다. exit_on_error: /* TODO: suspect pgbuf_unfix */는 알려진 미완성 부분이다. 마지막으로 고정된 페이지의 fix가 누수될 수 있다(companion Cross-check).

4.9 overflow_get_capacity — 전체 진단 순회

섹션 제목: “4.9 overflow_get_capacity — 전체 진단 순회”

Capacity 순회는 next_vpid를 NULL까지 따라가는 대신 length를 기준으로 모든 페이지를 순회하며, 크기, 페이지 수, 헤더 오버헤드, 말미 여유 공간을 보고한다.

// overflow_get_capacity -- src/storage/overflow_file.c (condensed)
*ovf_size = remain_length = first_part->length; /* size output = total length */
*ovf_num_pages = *ovf_overhead = *ovf_free_space = 0; /* other three outputs start zeroed */
hdr_length = offsetof (OVERFLOW_FIRST_PART, data); next_vpid = first_part->next_vpid; /* head header larger */
while (remain_length > 0)
{
if (remain_length > DB_PAGESIZE)
remain_length -= DB_PAGESIZE - hdr_length; /* not last page: subtract payload capacity */
else
{ *ovf_free_space = DB_PAGESIZE - remain_length; remain_length = 0; } /* last page: tail is free */
*ovf_num_pages += 1; *ovf_overhead += hdr_length;
if (remain_length > 0) /* unfix; same VPID_ISNULL guard + fix as 4.4; then: */
{ hdr_length = offsetof (OVERFLOW_REST_PART, data); next_vpid = ((OVERFLOW_REST_PART *) pgptr)->next_vpid; }
}
pgbuf_unfix_and_init (thread_p, pgptr);

세 분기(마지막 아닌 페이지 / 마지막 페이지 / 다음 페이지 fix)는 모두 인라인으로 주석 처리되어 있다. 다음 페이지 fix는 4.4의 VPID_ISNULL 가드와 fix 실패 종료 경로를 공유한다. 두 번째 반복부터 hdr_length가 헤드→rest로 전환되어 오버헤드 계산이 정확하게 유지된다. exit_on_error에서는 출력 네 개를 모두 0으로 초기화하여 부분 순회 결과가 반만 계산된 capacity로 보고되는 일이 없도록 한다. overflow_get_nbytes와 마찬가지로, capacity는 링크 체인보다 length를 신뢰한다.

  1. length는 헤드에만 존재한다overflow_get_length는 fix-읽기-unfix 한 번으로 끝나고, 다른 모든 리더는 length를 처음에 한 번만 읽는다.
  2. 헤드와 rest의 구별은 VPID 동등성(VPID_EQ)이며 플래그가 아니다. 인라인 리더는 루프 전에 헤드를 처리하고, 루프 내의 모든 페이지는 rest 페이지다.
  3. MVCC 게이트는 TOO_OLD_FOR_SNAPSHOT에서만 중단한다TOO_NEWSELECT ... FOR UPDATE가 방금 갱신된 big 레코드를 lock할 수 있게 통과시킨다. mvcc_snapshot == NULL이면(overflow key) 스킵된다.
  4. 버퍼 크기 부족은 대역 외 신호다S_DOESNT_FIT + 음수 recdes->length. heap_get_bigone_content는 루프로 크기를 늘리고, btree_load_overflow_key는 사전 크기 계산으로 이 상황을 회피한다.
  5. 한 번에 하나의 페이지만 latch된다(unfix-before-fix). 체인 안정성은 홈 레퍼런스의 lock에 위임한다.
  6. length 바이트를 소비하기 전에 next_vpid가 NULL이면 데이터 손상이다 — 두 리더 모두 ER_HEAP_OVFADDRESS_CORRUPTED를 발생시킨다.
  7. overflow_flushoverflow_traverse의 무해한 분기다 (페이지당 pgbuf_flush_with_wal, VFID 없음). DELETE는 6장에서 다룬다.

Chapter 5: overflow_update로 체인을 제자리에서 갱신하기

섹션 제목: “Chapter 5: overflow_update로 체인을 제자리에서 갱신하기”

독자의 질문: 새 값이 기존 저장된 값보다 길거나 짧게 재작성될 때, overflow_insert(Chapter 3)나 overflow_delete(Chapter 6)처럼 체인을 통째로 헐고 재구성하지 않으면서, 기존 체인을 페이지 단위로 늘리거나 줄이는 방법은 무엇인가?

답은 overflow_update다. 이 함수는 체인의 헤드부터 테일까지 순차적으로 걸어가며 각 페이지의 페이로드를 제자리에 덮어쓰고, 새 길이가 소진되거나 초과되는 전환 지점에서만 토폴로지(next_vpid 링크, 페이지 할당)를 건드린다. 이 챕터는 모든 분기를 해부한다. OVERFLOW_FIRST_PART / OVERFLOW_REST_PART 필드는 Chapter 1에서 다룬다.

5.1 힙만 이 함수를 호출하는 이유 — FILE_MULTIPAGE_OBJECT_HEAP assert

섹션 제목: “5.1 힙만 이 함수를 호출하는 이유 — FILE_MULTIPAGE_OBJECT_HEAP assert”

overflow_update는 호출자를 제한하는 두 개의 assert로 시작한다:

// overflow_update -- src/storage/overflow_file.c
assert (ovf_vfid != NULL && !VFID_ISNULL (ovf_vfid));
/* used only for heap for now... this doesn't consider temporary files. -- author's comment, condensed */
assert (file_type == FILE_MULTIPAGE_OBJECT_HEAP); /* <- in-place update is a heap-only path */

B+Tree OID 리스트 변형(Chapter 7)은 제자리 갱신을 수행하지 않는다. 키/OID 변경은 항상 삭제 후 재삽입이다. 오직 힙의 REC_BIGONE 레코드만이 heap_ovf_update를 거쳐 overflow_update에 도달한다. 이 assert는 계약을 코드화한 것이다: 이 함수는 임시 파일(다른 페이지 초기화 함수가 필요)을 처리하지 않으며, 영구 힙 overflow 타입에 대해서만 검증되었다. 구조적 귀결로서 헤드 페이지는 절대 해제되지 않는다 — 5.5절 참조.

heap_ovf_update는 힙의 OID 세계를 VFID/VPID로 변환하는 역할을 한다:

// heap_ovf_update -- src/storage/heap_file.c
if (heap_ovf_find_vfid (thread_p, hfid, &ovf_vfid, false, PGBUF_UNCONDITIONAL_LATCH) == NULL)
return NULL; /* <- could not resolve overflow file VFID */
ovf_vpid.pageid = ovf_oid->pageid;
ovf_vpid.volid = ovf_oid->volid; /* <- head VPID lives in the OID */
if (overflow_update (thread_p, &ovf_vfid, &ovf_vpid, recdes, FILE_MULTIPAGE_OBJECT_HEAP) != NO_ERROR)
{ ASSERT_ERROR (); return NULL; } /* <- propagate failure as NULL */
else
return ovf_oid; /* <- OID unchanged: head never moved */

heap_ovf_find_vfidfalse를 넘기는 점에 주목하자 — 파일을 새로 만들지 말고 이미 존재해야 한다는 의미다. 성공 시 동일한 ovf_oid가 반환된다. 헤드가 불변이므로 relocation 레코드를 다시 쓸 필요가 없다. 호출자는 heap_update_physicalREC_BIGONE -> REC_BIGONE 분기이며, 새 이미지에 대해서도 heap_is_big_length가 여전히 참일 때만 이 경로가 실행된다.

체인 토폴로지(REC_BIGONE 슬롯 → 헤드 OVERFLOW_FIRST_PARTOVERFLOW_REST_PART … → NULL 테일)는 Chapter 1의 자료구조 지도다. 제자리 갱신은 각 페이지의 data[]를 변경하고 선택적으로 next_vpid를 재기록하지만, REC_BIGONE 슬롯의 헤드 VPID는 절대 바뀌지 않는다.

5.3 메인 루프 골격과 반복 간 상태

섹션 제목: “5.3 메인 루프 골격과 반복 간 상태”

본체는 sysop으로 감싼 하나의 while (length > 0) 루프다:

// overflow_update -- src/storage/overflow_file.c
next_vpid = *ovf_vpid; /* <- start at the head */
data = recdes->data;
length = recdes->length; /* <- bytes still to write */
log_sysop_start (thread_p); /* <- atomic bracket; see 5.6 */
while (length > 0)
{
addr.pgptr = pgbuf_fix (thread_p, &next_vpid, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH);
if (addr.pgptr == NULL) { ASSERT_ERROR_AND_SET (error_code); goto exit_on_error; }
addr_vpid_ptr = pgbuf_get_vpid_ptr (addr.pgptr);
/* ... first-page branch vs rest-page branch ... */
}

네 개의 지역 변수가 반복 간 상태를 유지하며 분기를 선택한다:

지역 변수역할존재 이유
length아직 쓰지 않은 새 이미지 바이트 수루프 가드; 0이 되면 종료/축소 경로를 트리거
old_lengthundo before-image로 아직 커버되지 않은 기존 이미지 바이트 수페이지당 로깅할 before-image 크기를 결정
isnewpage이번 갱신에서 file_alloc으로 페이지를 할당했으면 truerest 분기가 새 페이지를 NULL-종료해야 함을 알림
rest_parts헤드를 지난 뒤 non-NULL성장/종료 시 first_part vs rest_parts 중 어디에 링크할지 결정

before-image 분할 규칙 (5.4 전반에서 참조). old_length헤드에서 한 번만 first_part->length로부터 읽힌다. 기존 페이로드를 아직 담고 있는 각 페이지에서 코드는 RVOVF_PAGE_UPDATE undo를 로깅하고, 해당 페이지가 보유하던 만큼 old_length를 감소시킨다 — rest가 넘치면 전체 DB_PAGESIZE, 아니면 정확히 hdr_length + old_length (그 후 old_length = 0). 기존 바이트 수를 로깅하는 것이 undo 시 원래 페이지를 정확히 복원할 수 있게 하며, 페이지마다 추적함으로써 더 짧은 새 이미지도 더 긴 기존 이미지를 복원할 수 있다. 5.4의 두 분기 모두 이 규칙을 그대로 사용하며, 헤드는 추가로 old_length를 초기화한다.

5.4 루프 내부: 두 가지 페이지 분기

섹션 제목: “5.4 루프 내부: 두 가지 페이지 분기”

VPID_EQ (addr_vpid_ptr, ovf_vpid)가 방금 fix한 페이지가 헤드인지를 판별한다.

// overflow_update (first-page branch) -- src/storage/overflow_file.c
first_part = (OVERFLOW_FIRST_PART *) addr.pgptr;
old_length = first_part->length; /* <- whole old image length, read once (seeds 5.3 rule) */
copyto = (char *) first_part->data;
next_vpid = first_part->next_vpid; /* <- remember existing successor */
hdr_length = offsetof (OVERFLOW_FIRST_PART, data);
/* copy_length = min(length, DB_PAGESIZE - hdr_length): fill page if more follows, else exact fit */
if ((length + hdr_length) > DB_PAGESIZE) copy_length = DB_PAGESIZE - hdr_length;
else copy_length = length;
/* Before-image, logged unconditionally (head always held payload) -- the 5.3 split: */
if (hdr_length + old_length > DB_PAGESIZE) /* old image spills past the head */
{ log_append_undo_data (thread_p, RVOVF_PAGE_UPDATE, &addr, DB_PAGESIZE, addr.pgptr);
old_length -= DB_PAGESIZE - hdr_length; } /* <- one full page of old payload consumed */
else /* old image fits on the head: log hdr_length+old_length bytes, then old_length = 0 */
{ log_append_undo_data (thread_p, RVOVF_PAGE_UPDATE, &addr, hdr_length + old_length, addr.pgptr);
old_length = 0; }
first_part->length = length; /* <- store the NEW total length on the head */
log_append_empty_record (thread_p, LOG_DUMMY_OVF_RECORD, &addr);

first_part->length새 전체 길이가 된다(length는 오직 헤드만 보관한다). 뒤따르는 LOG_DUMMY_OVF_RECORD는 복구/스캔이 새로 스탬프된 overflow 헤드를 인식하는 데 사용하는 마커를 재발행한다 — redo 시 이것이 중요한 이유는 Chapter 8에서 다룬다.

// overflow_update (rest-page branch) -- src/storage/overflow_file.c
rest_parts = (OVERFLOW_REST_PART *) addr.pgptr;
copyto = (char *) rest_parts->data;
if (isnewpage == true)
{
VPID_SET_NULL (&next_vpid);
rest_parts->next_vpid = next_vpid; /* <- a page WE just allocated terminates the chain */
}
else
next_vpid = rest_parts->next_vpid; /* <- pre-existing page: keep following the old links */
hdr_length = offsetof (OVERFLOW_REST_PART, data);
/* copy_length computed exactly as in 5.4.1 (fill page vs fits) */
if (old_length > 0) /* <- only log undo if old payload still reaches here */
{
/* ... identical before-image split as 5.3 / 5.4.1 ... */
}

isnewpage성장 경로를 제어한다. 5.4.3에서 방금 할당된 페이지는 next_vpid에 쓰레기 값을 담고 있으므로, 이 분기는 NULL로 종료한다(성장 도중 크래시가 발생해도 초기화되지 않은 링크를 따라가지 않게 된다). 기존 페이지라면 next_vpid를 읽어 기존 토폴로지를 따른다. if (old_length > 0) 가드는 축소 대응 로직이다 — 이전 before-image들이 모든 기존 바이트를 이미 커버했다면, 곧 해제될 테일 페이지들의 undo는 로깅하지 않는다. 링크 opcode와 file_dealloc이 그것을 감당한다.

5.4.3 공통 페이로드 복사, 그 후 성장 또는 종료

섹션 제목: “5.4.3 공통 페이로드 복사, 그 후 성장 또는 종료”

두 분기에서 copyto/copy_length/hdr_length가 설정된 후:

// overflow_update (shared) -- src/storage/overflow_file.c
memcpy (copyto, data, copy_length); /* <- overwrite payload in place */
data += copy_length; length -= copy_length;
if (thread_p->no_logging != true)
log_append_redo_data (thread_p, RVOVF_PAGE_UPDATE, &addr, copy_length + hdr_length, (char *) addr.pgptr);

redo는 copy_length + hdr_length 바이트 — 헤더 + 새 페이로드 — 를 로깅하여 after-image로부터 갱신 후 페이지를 재구성한다. (no_logging은 벌크 로드 고속 경로로, redo만 억제한다.) 그 후 체인 토폴로지를 결정하는 분기가 이어진다:

// overflow_update (grow path) -- src/storage/overflow_file.c
if (length > 0)
{
if (VPID_ISNULL (&next_vpid)) /* <- ran out of existing pages: GROW */
{
error_code = file_alloc (thread_p, ovf_vfid, file_init_page_type, &ptype, &next_vpid, NULL);
if (error_code != NO_ERROR)
{ ASSERT_ERROR (); pgbuf_set_dirty_and_free (thread_p, addr.pgptr); goto exit_on_error; }
log_append_undoredo_data (thread_p, RVOVF_NEWPAGE_LINK, &addr, 0, sizeof (next_vpid), NULL, &next_vpid);
isnewpage = true; /* <- next iteration NULL-terminates the new page */
if (rest_parts == NULL)
first_part->next_vpid = next_vpid; /* <- still on head: link grows from first_part */
else
rest_parts->next_vpid = next_vpid; /* <- link grows from current rest page */
}
pgbuf_set_dirty_and_free (thread_p, addr.pgptr);
}

성장은 페이로드가 남아 있고 next_vpid가 NULL일 때만 실행된다. RVOVF_NEWPAGE_LINK의 undoredo는 undo 길이가 0이다 — 새 링크의 undo는 “링크 없음”이며, file_alloc 자체의 레코드가 롤백 시 페이지를 회수한다. 새 VPID는 first_part(아직 헤드에 있을 때) 또는 rest_parts로부터 링크된다. next_vpid가 NULL이 아니면 할당을 건너뛰고 기존 후계 페이지로 진행한다. file_alloc 실패 시 래치 누수 없이 페이지를 dirty-and-free한 뒤 goto exit_on_error로 이동하고, sysop abort가 롤백을 처리한다(5.6절).

// overflow_update (terminate/shrink) -- src/storage/overflow_file.c
else /* length == 0 : no more payload */
{
VPID_SET_NULL (&tmp_vpid);
log_append_undoredo_data (thread_p, RVOVF_CHANGE_LINK, &addr, sizeof (next_vpid), sizeof (next_vpid),
&next_vpid, &tmp_vpid); /* <- undo restores old next_vpid; redo sets NULL */
if (rest_parts == NULL)
VPID_SET_NULL (&first_part->next_vpid); /* <- single-page result: head's link cut */
else
VPID_SET_NULL (&rest_parts->next_vpid); /* <- cut after the last written rest page */
pgbuf_set_dirty_and_free (thread_p, addr.pgptr);
while (!(VPID_ISNULL (&next_vpid))) /* <- free every surplus tail page */
{
addr.pgptr = pgbuf_fix (...); // ... fix; NULL -> exit_on_error ...
tmp_vpid = next_vpid;
rest_parts = (OVERFLOW_REST_PART *) addr.pgptr;
next_vpid = rest_parts->next_vpid; /* <- read successor BEFORE unfixing */
pgbuf_unfix_and_init (thread_p, addr.pgptr);
error_code = file_dealloc (thread_p, ovf_vfid, &tmp_vpid, file_type);
if (error_code != NO_ERROR) { ASSERT_ERROR (); goto exit_on_error; }
}
break; /* <- leave the while(length>0) loop */
}

이 분기는 length == 0일 때 한 번 실행된다. RVOVF_CHANGE_LINK는 기존 next_vpid를 undo로, NULL을 redo로 로깅한다. 링크는 마지막으로 쓰인 페이지(rest_parts가 NULL이면 헤드, 즉 값이 한 페이지에 맞으면 헤드; 아니면 마지막 rest 페이지)에서 NULL로 끊긴다. 이후 테일 해제 루프가 각 잉여 페이지를 fix하고, unfix 전에 next_vpid를 미리 읽어 체인이 해제 과정에서 끊기지 않도록 한 뒤, unfix하고 file_dealloc한다. NULL에서 루프가 종료되면 break로 외부 루프를 빠져나온다. 실패 시에는 sysop abort로 점프한다.

Figure 5-1은 이 함수의 흐름을 추적한다.

flowchart TD
  S["sysop 시작; 헤드에서 출발"] --> L{"length > 0 ?"}
  L -- 아니오 --> ATT["외부 트랜잭션에 attach; NO_ERROR 반환"]
  L -- 예 --> FIX["next_vpid WRITE fix"]
  FIX -- NULL --> ERR["exit_on_error"]
  FIX --> HEAD{"VPID_EQ 헤드 ?"}
  HEAD -- 예 --> FP["첫 번째 페이지 분기"]
  HEAD -- 아니오 --> RP["나머지 페이지 분기"]
  FP --> CP["memcpy; redo after-image 로깅"]
  RP --> CP
  CP --> M{"length > 0 ?"}
  M -- 예 --> G{"next_vpid NULL ?"}
  G -- 예 --> ALLOC["성장: file_alloc; 링크"]
  G -- 아니오 --> DF["후계 재사용; dirty+free; 루프"]
  ALLOC --> DF
  DF --> L
  M -- 아니오 --> TERM["종료: 링크 절단"]
  TERM --> TL{"next_vpid NULL ?"}
  TL -- 아니오 --> DEA["축소: 테일 페이지 해제"]
  DEA --> TL
  TL -- 예 --> ATT
  ERR --> AB["sysop abort; 오류 반환"]

Figure 5-1. overflow_update 제어 흐름 — 모든 분기와 오류 출구.

5.5 헤드는 절대 제거되지 않는다는 불변 조건

섹션 제목: “5.5 헤드는 절대 제거되지 않는다는 불변 조건”

불변 조건: 힙 overflow 체인은 해당 값이 살아 있는 동안 항상 헤드 페이지(OVERFLOW_FIRST_PART)를 유지한다. overflow_update는 이를 구조적으로 강제한다. 헤드는 가장 먼저 fix되고 항상 앞부분 copy_length 바이트를 수신하며, 5.4.4의 테일 해제 루프는 마지막으로 쓰인 페이지 이후(즉 next_vpid부터 시작)에 있는 페이지만 해제한다 — 헤드는 절대 해제 대상이 되지 않는다. 이미지가 몇 바이트로 줄어들더라도 헤드는 next_vpid가 NULL로 설정된 채 살아남는다. 축소가 헤드를 해제할 수 있다면, REC_BIGONE OID가 허상 포인터(dangling pointer)가 되어 그것을 resolve하려는 모든 독자(Chapter 4)가 오류를 낼 것이다 — 헤드를 고정하는 것이 heap_ovf_update가 변경되지 않은 ovf_oid를 반환할 수 있는 근거다.

갱신 전체는 하나의 중첩 원자 단위다:

// overflow_update -- src/storage/overflow_file.c
log_sysop_start (thread_p);
/* ... while loop, allocations, deallocations ... */
log_sysop_attach_to_outer (thread_p); return NO_ERROR; /* success: splice into enclosing txn */
exit_on_error:
log_sysop_abort (thread_p); return error_code; /* failure: undo writes, alloc, dealloc */

성공 시 sysop은 독립적으로 커밋되지 않고 외부 트랜잭션에 attach된다 — 모든 페이지 재작성, 링크 변경, file_alloc/file_dealloc이 상위 힙 갱신의 일부가 된다. 어떤 오류(pgbuf_fix NULL, file_alloc/file_dealloc 실패)가 발생하든 goto exit_on_errorlog_sysop_abort를 실행하여 지금까지 로깅된 undo를 재생하고, 체인을 이전 상태 그대로 남긴다. 5.4에서 언급된 각 opcode가 복구 시 어떻게 적용되는지는 Chapter 8에서 자세히 다룬다.

  1. 제자리 갱신은 힙 전용이다. assert (file_type == FILE_MULTIPAGE_OBJECT_HEAP)이 다른 호출자를 차단한다. B+Tree 값 변경은 삭제 후 재삽입이다.
  2. 걷기(walk)는 memcpy로 페이지별 페이로드를 덮어쓰며, 페이지마다 RVOVF_PAGE_UPDATE undo(before-image)와 redo(after-image)를 로깅한다. length는 헤드만 보관한다.
  3. old_length가 undo를 주도한다. 5.3의 단일 분할 규칙(헤드에서 초기화, 페이지마다 감소)에 따른다. if (old_length > 0) 가드는 소멸 예정 테일 페이지의 undo를 억제하므로, 더 짧은 새 이미지도 원본을 복원할 수 있다.
  4. 성장은 페이로드가 남아 있고 next_vpid가 NULL일 때만 페이지를 할당하며(file_alloc + RVOVF_NEWPAGE_LINK), isnewpage를 설정하고 first_part 또는 rest_parts로부터 링크한다.
  5. 축소/종료length == 0일 때 한 번 실행된다. RVOVF_CHANGE_LINK가 후미 링크를 끊고, 테일 루프가 각 잉여 페이지를 fix → 선독(next_vpid) → unfix → file_dealloc 순으로 처리한다.
  6. 헤드는 절대 해제되지 않으므로(5.5절), REC_BIGONE OID는 유효한 상태를 유지하고 heap_ovf_update는 변경되지 않은 OID를 반환한다.
  7. 전체 오퍼레이션은 하나의 sysop이다. 성공 시 log_sysop_attach_to_outer, 모든 오류 경로에서 log_sysop_abort — 상위 트랜잭션과 원자적으로 연결된다.

Chapter 6: 체인 삭제와 페이지 반환

섹션 제목: “Chapter 6: 체인 삭제와 페이지 반환”

큰 레코드 값이 완전히 사라질 때, overflow 체인의 각 페이지는 어떻게 해제되며, MVCC 하에서는 누가 그 방아쇠를 당기는가? 물리적 반환 자체는 간결하다 — overflow_deleteoverflow_traverse(OVERFLOW_DO_DELETE) → 페이지별 file_dealloc. 흥미로운 부분은 호출자 쪽이다. REC_BIGONE에 대한 MVCC 삭제는 체인을 해제하지 않는다. MVCC 삭제는 head overflow 페이지에 delete-MVCCID를 기록하고 home heap 슬롯을 살아있는 REC_BIGONE인 채로 내버려 두므로, vacuum(또는 비-MVCC 삭제)이 해당 행을 더 이상 아무도 볼 수 없다고 판단할 때까지 체인이 유지된다. 체인 구조는 Chapter 1, 구성은 Chapter 3, 페이지별 락-없음 근거는 companion 문서(cubrid-overflow-file.md)를 참고하라.

6.1 반환 핵심 — overflow_delete와 순회

섹션 제목: “6.1 반환 핵심 — overflow_delete와 순회”

overflow_deleteOVERFLOW_DO_DELETE 판별자를 넘기며 공용 순회-콜백 함수 overflow_traverse에 작업을 위임한다(OVERFLOW_DO_FLUSH에도 같은 함수가 쓰인다). 이 함수는 각 페이지를 fix한 뒤, 현재 페이지에 작업하기 전에 next 링크를 먼저 읽는다:

// overflow_traverse -- src/storage/overflow_file.c (condensed)
next_vpid = *ovf_vpid;
while (!(VPID_ISNULL (&next_vpid)))
{
pgptr = pgbuf_fix (thread_p, &next_vpid, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH);
if (pgptr == NULL)
{ goto exit_on_error; } /* <- fix 실패: NULL 반환 */
vpid = next_vpid;
overflow_next_vpid (ovf_vpid, &next_vpid, pgptr); /* <- 해제 전에 링크 읽기 */
switch (func)
{
case OVERFLOW_DO_DELETE:
if (ovf_vfid == NULL) /* assert: dealloc에는 VFID 필요 */
{ goto exit_on_error; }
if (overflow_delete_internal (thread_p, ovf_vfid, &vpid, pgptr) != NO_ERROR)
{ goto exit_on_error; }
break;
case OVERFLOW_DO_FLUSH: /* ... condensed ... */ break;
default: break;
}
}
return ovf_vpid;

실제 switch (func)에는 세 개의 분기가 있다 — OVERFLOW_DO_DELETE, OVERFLOW_DO_FLUSH(overflow_flush_internal 호출), 그리고 default: break; 무동작 — 하지만 삭제 순회에서 사용되는 것은 첫 번째뿐이다. 세 가지 오류 경로는 모두 exit_on_error → NULL으로 수렴한다(Figure 6-1): pgbuf_fix NULL(탈출; free는 커밋 이후의 postpone이므로 abort는 무해함, §6.2), ovf_vfid == NULL 단언(소유 파일 없이 dealloc 불가 — overflow_flush가 의도적으로 NULL을 넘기기 때문에 분기별로 처리), overflow_delete_internal 실패(전파). 네 번째 경우인 next_vpid NULL 루프 종료는 오류 경로가 아니다. while을 빠져나와 성공 센티넬인 return ovf_vpid로 이어진다. exit_on_error 자체는 unfix를 수행하지 않는다(/* TODO: suspect pgbuf_unfix */ 주석 참조). 따라서 루프 바디에서 fix에 성공한 후 오류가 발생하는 경우 페이지가 누수된다.

불변 조건 — “현재 페이지를 해제하기 전에 next-page 링크를 읽어라.” overflow_traverseoverflow_next_vpidoverflow_delete_internal이 unfix·dealloc하기 전에 호출한다. 순서가 뒤바뀌면 해제된 페이지에서 next 링크를 역참조하게 되어 — use-after-free로 체인 꼬리가 손상된다.

(overflow_next_vpid는 타입을 인식한다 — head에서는 OVERFLOW_FIRST_PART로, 그 외에는 OVERFLOW_REST_PART로 캐스팅한다 — 하지만 두 구조체 모두 next_vpid를 같은 앞쪽 오프셋에 두므로 캐스팅은 형식적이다.)

flowchart TD
  A["overflow_delete(ovf_vfid, ovf_vpid)"] --> B["overflow_traverse(..., OVERFLOW_DO_DELETE)"]
  B --> C{"next_vpid가 NULL?"}
  C -- "예" --> Z["return ovf_vpid (성공)"]
  C -- "아니오" --> D["pgbuf_fix(next_vpid, WRITE)"]
  D --> E{"pgptr == NULL?"}
  E -- "예" --> X["exit_on_error -> NULL 반환"]
  E -- "아니오" --> F["vpid := next_vpid\noverflow_next_vpid -> next_vpid 전진"]
  F --> G{"ovf_vfid == NULL?"}
  G -- "예" --> X
  G -- "아니오" --> H["overflow_delete_internal(ovf_vfid, vpid, pgptr)"]
  H --> I{"NO_ERROR?"}
  I -- "아니오" --> X
  I -- "예" --> C

Figure 6-1 — overflow_delete 순회: 페이지 fix, next-link 수집, dealloc; 오류 분기는 exit_on_error로 수렴.

6.2 페이지별 반환 — overflow_delete_internalFILE_UNKNOWN_TYPE TODO

섹션 제목: “6.2 페이지별 반환 — overflow_delete_internal과 FILE_UNKNOWN_TYPE TODO”

페이지별 작업 함수는 간결하다 — unfix 후 file_dealloc:

// overflow_delete_internal -- src/storage/overflow_file.c
pgbuf_unfix_and_init (thread_p, pgptr); /* <- dealloc 전에 latch+pin 해제 */
/* TODO: clarify file_type */
ret = file_dealloc (thread_p, ovf_vfid, vpid, FILE_UNKNOWN_TYPE); /* <- hint = UNKNOWN */
if (ret != NO_ERROR)
{ goto exit_on_error; }
return ret;
exit_on_error:
return (ret == NO_ERROR && (ret = er_errid ()) == NO_ERROR) ? ER_FAILED : ret;

페이지는 file_dealloc 전에 unfix된다. next 링크는 이미 수집되어 있으므로 write-latch 경합이 없다 — 체인은 레코드에 대한 상위 레벨 락이 독점적이므로 페이지별 락은 필요하지 않다(companion 문서 참조).

FILE_UNKNOWN_TYPE 힌트와 그 TODO. 세 번째 인자는 힌트다. FILE_UNKNOWN_TYPE을 넘기면 file_dealloc이 파일 헤더를 fix해서 실제 타입을 읽어온다(file_type_hint = fhead->type). #if defined (NDEBUG)if 조건만을 감싸기 때문에, 헤더-fix 분기의 도달 가능성은 빌드 모드에 따라 달라진다:

// file_dealloc -- src/storage/file_manager.c
#if defined (NDEBUG)
if (file_type_hint == FILE_UNKNOWN_TYPE
|| file_type_hint == FILE_EXTENDIBLE_HASH || file_type_hint == FILE_EXTENDIBLE_HASH_DIRECTORY)
#endif /* NDEBUG */
{
/* this should be avoided in release */
page_fhead = pgbuf_fix (thread_p, &vpid_fhead, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH);
fhead = (FILE_HEADER *) page_fhead;
assert (file_type_hint == FILE_UNKNOWN_TYPE || file_type_hint == fhead->type /* ... */);
file_type_hint = fhead->type; /* <- hint가 실제 타입으로 교체됨 */
}

릴리스 빌드(NDEBUG)에서는 이 분기가 조건부FILE_UNKNOWN_TYPE과 extendible-hash 두 타입에 대해서만 실행된다. 디버그 빌드에서는 #if가 조건 자체를 제거하므로 분기가 무조건 실행된다 — 모든 file_dealloc이 헤더를 읽어 힌트를 검증한다. 어느 경우든 overflow dealloc은 항상 FILE_UNKNOWN_TYPE을 넘기므로 헤더 fix는 항상 여기서 발생한다. 따라서 이 TODO는 정확성과 무관한 정리 작업이다. 호출자는 타입을 알고 있지만(heap은 FILE_MULTIPAGE_OBJECT_HEAP, B+Tree 키는 FILE_BTREE_OVERFLOW_KEY) 그것을 아래로 넘기지 않는다. companion 문서 Open Questions §2를 참조하라.

페이지별 RVOVF_* undo/redo 레코드가 생성되지 않는 이유. 삭제 시 overflow 전용 로그 레코드는 전혀 생성되지 않으며, 복구는 file_dealloc에 위임된다 — 모든 파일에 대한 RVFL_DEALLOC postpone, 그리고 numerable/user-page-table 분기에서만 RVFL_USER_PAGE_MARK_DELETE. RVFL_DEALLOCpostpone 레코드이므로, 영구 파일의 페이지는 run postpone(커밋 이후)에서만 해제된다. 따라서 abort 시 되돌릴 필요가 없으며, §6.1의 오류 경로에서 롤백 없이 NULL을 반환하는 것이 안전하다. RVOVF_* 레코드는 중복이 된다.

6.3 heap MVCC 비대칭 — MVCC 삭제는 체인을 해제하지 않는다

섹션 제목: “6.3 heap MVCC 비대칭 — MVCC 삭제는 체인을 해제하지 않는다”

heap_delete_bigoneis_mvcc_op를 기준으로 분기한다. MVCC 경로에서는 heap_ovf_delete절대 호출하지 않는다 — overflow 페이지에서 MVCC 헤더를 읽어 delete-MVCCID를 기록하고, home 슬롯은 살아있는 REC_BIGONE 상태로 유지한다:

// heap_delete_bigone -- src/storage/heap_file.c (is_mvcc_op branch, condensed)
overflow_oid = *((OID *) context->home_recdes.data); /* home 슬롯에는 OID만 있음 */
/* ... fix overflow page ... */
if (heap_get_mvcc_rec_header_from_overflow (..., &overflow_header, NULL) != NO_ERROR)
{ return ER_FAILED; }
heap_mvcc_log_delete (thread_p, &log_addr, RVHF_MVCC_DELETE_OVERFLOW);
heap_delete_adjust_header (&overflow_header, mvcc_id, false); /* <- DELID 기록 */
heap_set_mvcc_rec_header_on_overflow (..., &overflow_header); /* overflow 페이지에 씀 */
heap_mvcc_log_home_no_change (thread_p, &log_addr); /* <- home 슬롯 변경 없음 */

heap_delete_adjust_header가 실제 기록을 수행한다(MVCC_SET_FLAG_BITS(..., OR_MVCC_FLAG_VALID_DELID)MVCC_SET_DELID). 세 번째 인자 need_mvcc_header_max_sizefalse인 이유는 헤더가 이미 최대 크기이기 때문이다(§6.4). 대칭 함수 heap_get_mvcc_rec_header_from_overflow는 head 페이지 data[]의 첫 OR_MVCC_MAX_HEADER_SIZE 바이트를 읽는다.

불변 조건 — “큰 레코드에 대한 MVCC 삭제는 head overflow 페이지만 변경하며, home heap 슬롯은 살아있는 REC_BIGONE으로 유지된다.” is_mvcc_op 분기에 의해 강제된다. 유일한 쓰기는 overflow 페이지에 대한 heap_set_mvcc_rec_header_on_overflow와, home 페이지의 max-MVCCID/vacuum-status를 갱신해 vacuum이 재방문하도록 하는 no-change 레코드(heap_mvcc_log_home_no_change)뿐이다. 이 시점에 체인을 해제하면, mvcc_id보다 이전 스냅샷을 가진 reader가 해제된 overflow 페이지를 fix하게 되어 — 가시성 오류가 발생한다.

6.4 강제 최대 헤더 불변 조건 — heap_set_mvcc_rec_header_on_overflow

섹션 제목: “6.4 강제 최대 헤더 불변 조건 — heap_set_mvcc_rec_header_on_overflow”

인-플레이스 MVCC 삭제가 가능하려면, DELID를 기록할 때 헤더의 바이트 크기가 절대 변하지 않아야 한다. 이를 위해 CUBRID는 head overflow 페이지의 MVCC 헤더를 항상 최대 크기로 강제한다:

// heap_set_mvcc_rec_header_on_overflow -- src/storage/heap_file.c
ovf_recdes.data = overflow_get_first_page_data (ovf_page);
ovf_recdes.area_size = ovf_recdes.length = OR_HEADER_SIZE (ovf_recdes.data);
assert (ovf_recdes.length == OR_MVCC_MAX_HEADER_SIZE); /* <- 크기 고정 */
if (!MVCC_IS_FLAG_SET (mvcc_header, OR_MVCC_FLAG_VALID_INSID)) /* 누락된 INSID 패딩 */
{ MVCC_SET_FLAG_BITS (mvcc_header, OR_MVCC_FLAG_VALID_INSID); MVCC_SET_INSID (mvcc_header, MVCCID_ALL_VISIBLE); }
if (!MVCC_IS_FLAG_SET (mvcc_header, OR_MVCC_FLAG_VALID_DELID)) /* 누락된 DELID 패딩 */
{ MVCC_SET_FLAG_BITS (mvcc_header, OR_MVCC_FLAG_VALID_DELID); MVCC_SET_DELID (mvcc_header, MVCCID_NULL); }
return or_mvcc_set_header (&ovf_recdes, mvcc_header);

불변 조건 — “head overflow 페이지의 MVCC 헤더는 항상 OR_MVCC_MAX_HEADER_SIZE다.” 누락된 INSID는 MVCCID_ALL_VISIBLE로, 누락된 DELID는 MVCCID_NULL로 패딩하고 크기를 단언함으로써 강제된다. 이 덕분에 §6.3에서 DELID를 인-플레이스로 기록할 때 row 바디가 밀리지 않는다. 헤더가 짧으면 바디를 오염시키거나 읽어들일 때 잘못 파싱된다.

6.5 실제 해제 — 비-MVCC 삭제와 vacuum

섹션 제목: “6.5 실제 해제 — 비-MVCC 삭제와 vacuum”

else 분기(비-MVCC 경로) — 비-MVCC 클래스 삭제 또는 §6.3의 delete-MVCCID가 모든 스냅샷을 넘어선 후 vacuum이 도달했을 때 실행된다 — 는 home 슬롯을 먼저 회수한 다음 체인을 해제한다:

// heap_delete_bigone -- src/storage/heap_file.c (non-MVCC branch, condensed)
bool is_reusable = heap_is_reusable_oid (context->file_type);
heap_log_delete_physical (..., &context->home_recdes, is_reusable, NULL); /* home-slot 로그 */
rc = heap_delete_physical (thread_p, &context->hfid, ..., &context->oid); /* home 슬롯 해제 */
if (rc != NO_ERROR)
{ return rc; }
if (heap_ovf_delete (thread_p, &context->hfid, &overflow_oid, NULL) == NULL) /* <- 체인 해제 */
{ return ER_FAILED; }

heap_log_delete_physicalRVHF_DELETE undoredo 레코드를 기록하고 — is_reusable에 따라 — RVHF_MARK_REUSABLE_SLOT postpone를 추가한다(home 슬롯의 운명 분기; 핵심 정리 6). heap_ovf_deleteovf_vfid_p == NULL로 호출되어 overflow 파일을 먼저 resolve한 뒤 위임한다:

// heap_ovf_delete -- src/storage/heap_file.c
if (ovf_vfid_p == NULL || VFID_ISNULL (ovf_vfid_p)) /* <- 호출자가 NULL 전달 */
{
ovf_vfid_p = (ovf_vfid_p != NULL) ? ovf_vfid_p : &ovf_vfid;
if (heap_ovf_find_vfid (thread_p, hfid, ovf_vfid_p, false /*docreate*/, ...) == NULL)
{ return NULL; } /* <- ovf 파일을 찾지 못함 */
}
ovf_vpid.pageid = ovf_oid->pageid; ovf_vpid.volid = ovf_oid->volid; /* OID 페이지 = head VPID */
if (overflow_delete (thread_p, ovf_vfid_p, &ovf_vpid) == NULL)
{ return NULL; }
return ovf_oid;

docreate = false — 삭제 시에는 파일을 생성하지 않는다. ovf_oid(volid, pageid)가 곧 체인 head VPID다. 분기: (1) ovf_vfid_p가 non-NULL이면 lookup 생략(vacuum의 캐시된 VFID 경로); (2) NULL 또는 null 값이면 heap_ovf_find_vfid로 resolve, 실패 시 NULL; (3)/(4) overflow_delete의 NULL/성공을 전파한다.

stateDiagram-v2
  [*] --> Live: REC_BIGONE 삽입, 체인 할당
  Live --> DelStamped: MVCC 삭제\nhead 페이지에 DELID 기록\nhome 슬롯 변경 없음
  DelStamped --> Reclaimed: vacuum 또는 비-MVCC 삭제\nheap_ovf_delete -> overflow_delete\nhome 슬롯 -> REC_MARKDELETED 또는 REC_DELETED_WILL_REUSE
  Live --> Reclaimed: 비-MVCC 클래스 삭제\n직접 heap_ovf_delete 호출
  Reclaimed --> [*]: run-postpone 시 페이지 file_dealloc

Figure 6-2 — 삭제 시점의 생명주기. MVCC 삭제는 행을 DelStamped로만 이동시킨다(체인 유지). 체인은 Reclaimed에서만 해제된다.

6.6 B+Tree overflow-key 삭제 — btree_delete_overflow_key

섹션 제목: “6.6 B+Tree overflow-key 삭제 — btree_delete_overflow_key”

B+Tree overflow-key 경로(긴 키가 FILE_BTREE_OVERFLOW_KEY 체인으로 spill된 경우)는 MVCC 지연 없이 overflow_delete를 재사용한다(키 바이트는 MVCC-불변이다). btree_delete_overflow_key는 내장된 head VPID를 추출하고 체인을 해제하지만, 슬롯 자체는 건드리지 않는다(“btree 슬롯은 삭제하지 않는다”):

// btree_delete_overflow_key -- src/storage/btree.c (condensed)
if (spage_get_record (thread_p, page_ptr, slot_id, &rec, PEEK) != S_SUCCESS) /* rec peek */
{ goto exit_on_error; }
if (node_type == BTREE_LEAF_NODE)
{
int mvccids_size = 0; /* +OR_MVCCID_SIZE per INSID/DELID flag */
if (btree_record_object_is_flagged (rec.data, BTREE_OID_HAS_MVCC_INSID)) mvccids_size += OR_MVCCID_SIZE;
if (btree_record_object_is_flagged (rec.data, BTREE_OID_HAS_MVCC_DELID)) mvccids_size += OR_MVCCID_SIZE;
if (btree_leaf_is_flaged (&rec, BTREE_LEAF_RECORD_CLASS_OID))
{ start_ptr = rec.data + (2 * OR_OID_SIZE) + mvccids_size; } /* OID + class OID */
else
{ start_ptr = rec.data + OR_OID_SIZE + mvccids_size; } /* OID만 */
}
else
{ start_ptr = rec.data + NON_LEAF_RECORD_SIZE; } /* 비-리프: 고정 prefix */
or_init (&buf, start_ptr, DISK_VPID_SIZE);
page_vpid.pageid = or_get_int (&buf, &rc);
if (rc == NO_ERROR) { page_vpid.volid = or_get_short (&buf, &rc); }
if (rc != NO_ERROR) { goto exit_on_error; }
if (overflow_delete (thread_p, &(btid->ovfid), &page_vpid) == NULL) /* <- 체인 해제 */
{ goto exit_on_error; }
return NO_ERROR;

리프/비-리프 분기(인라인 주석)는 가변 형태의 레코드 안에서 head VPID를 찾는 역할만 한다. 세 가지 실패 지점(spage_get_record, or_get_int/or_get_short, overflow_delete)은 모두 exit_on_error로 이동하며 rc(또는 ER_FAILED)를 반환한다. PAGE_OVERFLOW 페이지 위의 FILE_BTREE_OVERFLOW_KEY 체인으로서(VFID btid->ovfid), 파괴의 핵심 로직은 heap 경우(§6.1/§6.2)와 동일하다.

6.7 대조 — OID-list 반환은 이 경로를 사용하지 않는다

섹션 제목: “6.7 대조 — OID-list 반환은 이 경로를 사용하지 않는다”

두 종류의 B+Tree overflow 형태는 btid->ovfid 파일을 공유하지만, 삭제 시점에서 갈린다. overflow-key 체인(이 챕터)은 overflow_delete로 일괄 해제된다. overflow-OID-list 체인(Chapter 7)은 overflow_delete로 해제되지 않는다PAGE_BTREE slotted 페이지 위에서 OID 단위로 회수(btree_overflow_remove_object)하며, 비워진 페이지는 개별적으로 언링크되고 file_dealloc된다. 키 체인 페이지만이 이 챕터의 경로를 흐른다.

  1. 반환은 얇은 순회다: overflow_deleteoverflow_traverse(OVERFLOW_DO_DELETE)overflow_delete_internal(pgbuf_unfix_and_initfile_dealloc), 각 페이지를 해제하기 전에 next_vpid를 수집한다. 모든 오류 분기는 NULL을 반환한다.
  2. 삭제 시 페이지별 RVOVF_* 레코드는 생성되지 않는다 — 복구는 file_dealloc에 위임된다(RVFL_DEALLOC postpone, numerable-file 분기에서는 추가로 RVFL_USER_PAGE_MARK_DELETE). 따라서 순회 중 실패해도 롤백이 불필요하다.
  3. FILE_UNKNOWN_TYPE 힌트는 정확성과 무관한 TODO다file_dealloc이 헤더에서 실제 타입을 읽도록 강제하며, 호출자는 타입을 알고 있으나 넘기지 않는다.
  4. REC_BIGONE에 대한 MVCC 삭제는 체인을 해제하지 않는다: is_mvcc_op 분기는 head overflow 페이지에 DELID만 기록하고(heap_get_mvcc_rec_header_from_overflowheap_delete_adjust_headerheap_set_mvcc_rec_header_on_overflow), home 슬롯에는 no-change 레코드를 남긴다 — 체인은 논리적 삭제보다 오래 살아있어야 한다.
  5. overflow head의 MVCC 헤더는 항상 OR_MVCC_MAX_HEADER_SIZE로 강제된다 (누락된 INSID/DELID 패딩 포함). 덕분에 DELID를 인-플레이스로 기록해도 row 바디가 이동하지 않는다 — 핵심 정리 4의 전제 조건.
  6. 체인은 vacuum 또는 비-MVCC 삭제에 의해서만 해제된다: 해당 분기는 home 슬롯을 먼저 회수하고(heap_delete_physicalREC_MARKDELETED, 또는 heap_is_reusable_oidRVHF_MARK_REUSABLE_SLOT postpone로 REC_DELETED_WILL_REUSE) 그 다음 heap_ovf_delete를 호출한다.
  7. B+Tree overflow-key 삭제에는 MVCC 지연이 없다: btree_delete_overflow_key는 head VPID를 추출하고 즉시 overflow_delete를 호출하며, 슬롯 처리는 호출자에게 맡긴다 — overflow_delete를 전혀 사용하지 않는 OID-list 반환(Chapter 7)과는 대조적이다.

Chapter 7: B+Tree OID 리스트 변형 구현

섹션 제목: “Chapter 7: B+Tree OID 리스트 변형 구현”

B+tree 인덱스는 overflow_file.c와 동일한 개념이 필요하다 — 하나의 논리적 레코드가 슬롯에 들어가기에 너무 크면 페이지 체인으로 흘러넘치게 된다 — 그러나 여기서는 하나의 인덱스 키를 공유하는 OID들의 정렬된 리스트를 위한 것이다. CUBRID는 overflow_file.c를 재사용하지 않는다; 대신 일반적인 PAGE_BTREE 슬롯 페이지 위에서 btree.c / btree_load.c 안에 체인을 독자적으로 구현한다. 디스크 상의 OID 인코딩은 cubrid-overflow-file.md의 “B+tree OID overflow” 절을 참조하고, 변형이 존재하는 이유는 §7.10에서 정리한다.

불변 조건 (페이지 타입 도메인). 모든 overflow OID 페이지는 pgbuf_check_page_ptype(...) == PAGE_BTREE를 만족하며, 이는 각 진입점에서 디버그 빌드 시 (#if !defined(NDEBUG) 내부에서) 단언된다. NDEBUG 빌드에서는 이 검사가 컴파일 아웃된다: 디버그에서는 잘못된 타입을 단언하지만, 릴리즈에서는 날(raw) 바이트를 슬롯 OID 레코드로 오독하여 리스트를 손상시킬 수 있다.

7.1 페이지 상의 레이아웃과 BTREE_OVERFLOW_HEADER

섹션 제목: “7.1 페이지 상의 레이아웃과 BTREE_OVERFLOW_HEADER”

슬롯 0 / HEADERBTREE_OVERFLOW_HEADER이고, 슬롯 1은 하나의 OID 레코드로 BTREE_OBJECT_FIXED_SIZE 보폭(stride)에 따라 오름차순으로 정렬되어 있다.

graph LR
  leaf["leaf record<br/>OVERFLOW_OIDS<br/>link=first vpid"]
  o1["page A HEAD<br/>slot0 next=B"]
  o2["page B<br/>slot0 next=C"]
  o3["page C TAIL<br/>slot0 next=NULL"]
  leaf --> o1 --> o2 --> o3

Figure 7-1. 하나의 키에 대한 overflow 체인. leaf는 HEAD를 가리키며, 각 페이지의 slot-0 헤더가 단방향 연결 next_vpid를 보유한다.

// btree_overflow_header -- src/storage/btree_load.h
struct btree_overflow_header
{
VPID next_vpid;
};
필드역할존재 이유
next_vpid이 키의 체인에서 다음 페이지의 VPID; 꼬리(tail)에서는 VPID_ISNULL.체인은 HEAD→tail로 단방향 연결된다. leaf는 HEAD만 보관하고, 이후의 모든 hop에서 이 필드를 읽는다. 역방향 포인터도 없고, 길이 카운트도 없다 — 순회와 접합(splice)에 필요한 최솟값은 VPID 하나뿐이다.

불변 조건 (슬롯 1에 단일 OID 레코드). 오브젝트 데이터는 슬롯 1에만 존재하고, 슬롯 0은 헤더다. 읽기 경로는 spage_get_record(..., 1, ...)HEADER를 하드코딩한다. 세 번째 슬롯이 생기면 이진 탐색의 stride 계산이 깨진다.

불변 조건 (고정 stride, 필수 전체 MVCC 헤더). 각 오브젝트는 BTREE_OBJECT_FIXED_SIZE(btid) 바이트이며 OID, 선택적 클래스 OID, 전체 OR_MVCC_MAX_HEADER_SIZE 헤더를 포함한다 (leaf 레코드는 이를 생략할 수 있다). 이진 탐색이 data + size * mid로 인덱싱하므로, 오브젝트가 짧으면 레코드 중간에 착지한다. 삽입 시 BTREE_MVCC_INFO_SET_FIXED_SIZE(...)로 stride를 강제한다.

슬롯 0에 대한 유일하게 허가된 접근자들이다.

// btree_get_overflow_header -- src/storage/btree_load.c
#if !defined(NDEBUG)
(void) pgbuf_check_page_ptype (thread_p, page_ptr, PAGE_BTREE); /* <- 디버그 전용 도메인 검사 */
#endif
if (spage_get_record (thread_p, page_ptr, HEADER, &header_record, PEEK) != S_SUCCESS)
{ assert_release (false); return NULL; } /* <- HEADER 슬롯이 반드시 존재해야 함 */
return (BTREE_OVERFLOW_HEADER *) header_record.data; /* <- PEEK: 페이지 버퍼를 별칭(alias) */

PEEK은 페이지 버퍼를 별칭(alias)하므로, btree_get_next_overflow_vpidbtree_get_overflow_header를 호출하여 (NULL 헤더면 ER_FAILED 반환) ovf_header->next_vpid를 값으로 호출자의 *vpid복사한다 — 덕분에 호출자는 이후 페이지를 안전하게 unfix할 수 있다.

btree_init_overflow_header는 슬롯 0을 설치하지만 자체적으로는 아무것도 로그하지 않는다 — 호출자가 첫 번째 오브젝트와 함께 하나의 redo 레코드로 묶는다. 유일한 실패 분기:

// btree_init_overflow_header -- src/storage/btree_load.c
// ... condensed: memcpy ovf_header into rec, rec.length = sizeof(...), rec.type = REC_HOME ...
if (spage_insert_at (thread_p, page_ptr, HEADER, &rec) != SP_SUCCESS)
{ return ER_FAILED; } /* <- 유일한 오류 경로 */

7.3 체인 헤드 생성: btree_start_overflow_page

섹션 제목: “7.3 체인 헤드 생성: btree_start_overflow_page”

새 페이지가 HEAD가 된다: next_vpid가 현재의 첫 번째 overflow VPID로 설정되고, 호출자(§7.4)가 leaf 링크를 다시 쓴다. 아래의 모든 분기가 인라인으로 주석 처리되어 있다 — btree_get_new_page NULL, init != NO_ERROR (unfix 없이 반환), spage_insert_at 실패, 그 다음 redo 전용 로그 + dirty:

// btree_start_overflow_page -- src/storage/btree.c
*new_page_ptr = btree_get_new_page (thread_p, btid_int, new_vpid, near_vpid);
if (*new_page_ptr == NULL) { ASSERT_ERROR_AND_SET (error_code); return error_code; }
VPID_COPY (&ovf_header_info.next_vpid, first_overflow_vpid); /* <- HEAD는 기존 첫 번째 페이지를 가리킴 */
error_code = btree_init_overflow_header (thread_p, *new_page_ptr, &ovf_header_info);
// ... condensed: build rec, BTREE_OVERFLOW_NODE; init!=NO_ERROR returns WITHOUT unfixing ...
if (spage_insert_at (thread_p, *new_page_ptr, 1, &rec) != SP_SUCCESS) /* <- slot 1 = 오브젝트들 */
{ assert_release (false); return ER_FAILED; }
// ... condensed: redo buffer = first_overflow_vpid + rec.data (link restored by redo too) ...
log_append_redo_data (thread_p, RVBT_RECORD_MODIFY_NO_UNDO, &addr, rv_redo_data_length, rv_redo_data);
pgbuf_set_dirty (thread_p, *new_page_ptr, DONT_FREE);

Redo 전용: 롤백 시 주변 sysop이 할당 전체를 되돌리므로(§7.4), 물리적으로 따로 되돌릴 것이 없다.

7.4 헤드 연결과 페이지 누수 주석

섹션 제목: “7.4 헤드 연결과 페이지 누수 주석”

호출자 btree_key_append_object_as_new_overflow하나의 시스템 연산(system operation) 하에서 leaf 링크를 다시 쓴다. 핵심 세 줄은 sysop 시작, 소스 자체의 누수 페이지 주석, leaf 재작성 로그 호출이다:

// btree_key_append_object_as_new_overflow -- src/storage/btree.c
if (!insert_helper->is_system_op_started) { log_sysop_start (thread_p); /* ... set flag ... */ }
/* Note that this page may be leaked if server crashes before changing the link in leaf page. */
// ... condensed: btree_start_overflow_page, btree_leaf_record_change_overflow_link, spage_update leaf ...
log_append_undoredo_data (thread_p, RVBT_RECORD_MODIFY_UNDOREDO, &insert_helper->leaf_addr, ...);

불변 조건 (체인 도달 가능성 vs. 누수 페이지 윈도우). 페이지는 선행 페이지가 자신을 링크한 후에야 라이브 체인에 합류한다: HEAD의 next_vpid가 먼저 설정되고, leaf 링크가 두 번째로 설정되며, sysop이 할당과 leaf 링크 재작성을 하나의 원자 단위로 묶는다. 따라서 복구는 둘 다 갖거나 둘 다 갖지 않는다. 남는 유일한 누수는 소스 주석이 명시한 sysop-end 이전 크래시 경우 — 타입이 올바르고, 참조되지 않은 무해한 페이지다. error: 레이블은 sysop을 중단하고 ovfl_page를 unfix한다.

7.5 여유 공간 탐색: btree_find_free_overflow_oids_page

섹션 제목: “7.5 여유 공간 탐색: btree_find_free_overflow_oids_page”

세 가지 분기가 있는 선형 WRITE-latch 최초 적합(first-fit) 스캔으로, 인라인 주석이 달려 있다:

// btree_find_free_overflow_oids_page -- src/storage/btree.c
int space_needed = BTREE_OBJECT_FIXED_SIZE (btid);
ovfl_vpid = *first_ovfl_vpid; /* 비어 있으면 루프 건너뜀, *overflow_page NULL */
while (!VPID_ISNULL (&ovfl_vpid))
{
*overflow_page = pgbuf_fix (...);
if (*overflow_page == NULL) { ASSERT_ERROR_AND_SET (error_code); return error_code; }
if (spage_max_space_for_new_record (thread_p, *overflow_page) > space_needed)
{ return NO_ERROR; } /* <- 적중: 페이지를 FIXED 상태로 호출자에게 반환 */
btree_get_next_overflow_vpid (thread_p, *overflow_page, &ovfl_vpid);
pgbuf_unfix_and_init (thread_p, *overflow_page); /* <- 공간 없음: 다음 hop 전에 해제 */
}
return NO_ERROR; /* <- 미적중: *overflow_page == NULL, 체인 전체 가득 참 */

적중 시 페이지가 fixed 상태로 반환되고, 미적중 시 *overflow_page == NULL을 반환하여 호출자에게 새 HEAD를 시작하도록 알린다.

7.6 탐색: btree_find_oid_and_its_pagebtree_find_oid_from_ovfl

섹션 제목: “7.6 탐색: btree_find_oid_and_its_page와 btree_find_oid_from_ovfl”

btree_find_oid_and_its_page는 먼저 leaf를 검사하고 (적중 시 *found_page = leaf), null 링크면 단락(short-circuit)한 뒤, 이전 페이지를 추적하면서 체인을 순회한다. 모든 분기가 아래에 인라인으로 주석 처리되어 있다; 루프 이후의 후처리 (적중 시 *found_page/*prev_page 할당, 미적중 시 unfix)는 생략되었다:

// btree_find_oid_and_its_page -- src/storage/btree.c
if (*offset_to_object != NOT_FOUND) { *found_page = leaf_page; return NO_ERROR; } /* leaf 적중 */
if (VPID_ISNULL (&leaf_rec_info->ovfl)) { return NO_ERROR; } /* <- 체인 없음 */
do {
overflow_page = pgbuf_fix (...); /* NULL -> goto error */
error_code = btree_find_oid_from_ovfl (...); /* err -> goto error */
if (*offset_to_object != NOT_FOUND) { break; } /* <- 적중: 페이지 fixed 유지 */
if (prev_overflow_page != NULL) { pgbuf_unfix_and_init (thread_p, prev_overflow_page); }
prev_overflow_page = overflow_page; overflow_page = NULL; /* <- prev 포인터 전진 */
error_code = btree_get_next_overflow_vpid (thread_p, prev_overflow_page, &overflow_vpid);
if (error_code != NO_ERROR) { ASSERT_ERROR (); goto error; }
} while (!VPID_ISNULL (&overflow_vpid));
// ... condensed: assign *found_page / *prev_page, or unfix on not-found ...

불변 조건 (prev_page 계약). 체인 적중 시 prev_page가 요청되면, 함수는 바로 앞 페이지를 반환한다 — 또 다른 overflow 페이지이거나, 첫 번째 overflow 페이지에서 적중했을 때는 leaf — 이는 unlink 경로(§7.8)가 필요로 하는 정확한 값이다. thread_p->read_ovfl_pages_count는 vacuum을 위해 증가된다.

btree_find_oid_from_ovfl은 정렬된 슬롯 1에 대한 페이지 단위 이진 탐색이다: 슬롯 1을 PEEK (실패 시 assert_release); OID_LT/OID_GT를 이용한 첫/마지막 오브젝트 조기 탈출; 그 다음 [1 .. num_oids-2] 구간에서 while (min <= max) 탐색. 두 가지 핵심 세부사항은 stride 계산과 OID 일치 시의 위임이다:

// btree_find_oid_from_ovfl -- src/storage/btree.c
size = BTREE_OBJECT_FIXED_SIZE (btid_int); /* <- 고정 stride가 탐색 불변 조건 */
oid_ptr = ovf_record.data + (size * mid); /* <- stride 계산은 고정 크기에 의존 */
if (OID_EQ (oid, &inst_oid)) /* <- OID 일치: MVCC 런(run) 스캔, 아니면 +/- mid */
{ return btree_seq_find_oid_from_ovfl (thread_p, btid_int, oid, &ovf_record, oid_ptr, ...); }
// ... else OID_GT -> min = mid+1; OID_LT -> max = mid-1; fall through -> NO_ERROR (not found) ...
섹션 제목: “7.7 링크 재작성: btree_modify_overflow_link”

제거된 페이지의 선행자가 또 다른 overflow 페이지일 때, 그 next_vpid를 재작성한다 — 라이브 overflow 헤더가 변경되는 유일한 지점이다. 생존하는 페이지에 대한 제자리 변경이므로 RVBT_RECORD_MODIFY_UNDOREDO를 사용한다.

// btree_modify_overflow_link -- src/storage/btree.c
assert (delete_helper->is_system_op_started); /* <- 호출자가 sysop을 소유해야 함 */
overflow_header_record.data = rv_undo_data;
if (spage_get_record (thread_p, ovfl_page, HEADER, &overflow_header_record, COPY) != S_SUCCESS)
{ assert_release (false); return ER_FAILED; } /* <- 오류 분기 1: undo 이미지 읽기 */
VPID_COPY (&ovf_header_info.next_vpid, next_ovfl_vpid); /* <- 새 링크가 제거된 페이지를 건너뜀 */
overflow_header_record.data = (char *) &ovf_header_info; // ... condensed: length set ...
if (spage_update (thread_p, ovfl_page, HEADER, &overflow_header_record) != SP_SUCCESS)
{ assert_release (false); return ER_FAILED; } /* <- 오류 분기 2: update */
ovf_addr.offset = HEADER; /* <- 슬롯 0, 슬롯 1 아님: 헤더 재작성 */
BTREE_RV_SET_OVERFLOW_NODE (&ovf_addr);
LOG_RV_RECORD_SET_MODIFY_MODE (&ovf_addr, LOG_RV_RECORD_UPDATE_ALL); /* <- 전체 헤더 교체 */
log_append_undoredo_data (thread_p, RVBT_RECORD_MODIFY_UNDOREDO, &ovf_addr, ...);

Undo = 기존 헤더 (먼저 복사됨), redo = 새 헤더, 1-필드 레코드 전체(LOG_RV_RECORD_UPDATE_ALL), 슬롯 HEADER를 주소로 지정. 두 가지 오류 분기: undo 이미지 읽기(spage_get_record COPY)와 spage_update — 둘 다 assert_release(false)ER_FAILED 반환. leaf 선행자 변형은 btree_modify_leaf_ovfl_vpid이며, 마찬가지로 RVBT_RECORD_MODIFY_UNDOREDO를 사용한다.

페이지가 비게 되는지 여부에 따른 두 가지 최상위 분기.

flowchart TD
  R["spage_get_record slot1 COPY"] --> Q{length == FIXED_SIZE?}
  Q -->|yes: 마지막 오브젝트| GN{get_next 오류?}
  GN -->|yes| RET["return err; 아직 sysop 없음"]
  GN -->|no| U["unfix; sysop_start"]
  U --> DA["file_dealloc page"]
  DA --> DAE{dealloc 오류?}
  DAE -->|yes| ERR["goto error: 시작했으면 sysop_end"]
  DAE -->|no| LK{prev_page == leaf?}
  LK -->|yes| LM["btree_modify_leaf_ovfl_vpid -> next"]
  LK -->|no| OM["btree_modify_overflow_link -> next"]
  LM --> SE["시작했으면 btree_delete_sysop_end"]
  OM --> SE
  Q -->|no: 오브젝트 더 있음| RR["btree_record_remove_object slot1"]
  RR --> OK["NO_ERROR"]
  SE --> OK

Figure 7-2. btree_overflow_remove_object 분기 지도. get_next_overflow_vpid 실패 후의 조기 bare return을 주목하라: 이는 sysop이 열리기 전에 발생하므로, 이후의 dealloc/link 실패가 사용하는 error: 레이블을 거치지 않는다.

빈 분기의 핵심 줄은 조기 bare return과 dealloc 이전 unfix 순서이다:

// btree_overflow_remove_object -- src/storage/btree.c
if (overflow_record.length == BTREE_OBJECT_FIXED_SIZE (btid_int))
{
assert (offset_to_object == 0); /* <- 단일 오브젝트는 오프셋 0에 있음 */
error_code = btree_get_next_overflow_vpid (thread_p, *overflow_page, &next_overflow_vpid);
if (error_code != NO_ERROR) { ASSERT_ERROR (); return error_code; } /* <- bare return: 아직 sysop 없음 */
pgbuf_get_vpid (*overflow_page, &overflow_vpid);
pgbuf_unfix_and_init (thread_p, *overflow_page); /* <- dealloc 이전에 unfix */
// ... condensed: sysop_start if needed; file_dealloc (goto error on fail) ...
if (prev_page == leaf_page) /* <- §7.7: leaf vs overflow 선행자 */
{ /* btree_modify_leaf_ovfl_vpid */ } else { /* btree_modify_overflow_link */ }
// ... condensed: goto error on fail; btree_delete_sysop_end if !save_system_op_started ...
}
else // ... condensed: 비어 있지 않은 페이지는 slot 1에서 btree_record_remove_object로 오브젝트 하나를 접합 제거 ...

불변 조건 (sysop 하에서 해제, 먼저 unfix). 해제는 sysop 안에서 이루어져야 한다. 그렇지 않으면 롤백 시 페이지가 누수된다 (소스가 명시); 코드는 필요하면 sysop을 시작하고, file_dealloc 이전에 unfix하며, 동일한 sysop 안에서 unlink를 실행한다 — “오브젝트 제거 + 페이지 해제 + 선행자 재연결”을 원자적으로 만든다. error: 레이블은 이 함수가 sysop을 열었을 때만 (!save_system_op_started) 종료시켜 이중 종료를 방지한다; 조기 get_next 실패는 sysop이 존재하기 전에 반환하므로 error:를 거쳐서는 안 된다.

7.9 제자리 교체: btree_overflow_record_replace_object

섹션 제목: “7.9 제자리 교체: btree_overflow_record_replace_object”

고유 인덱스 “첫 번째 leaf 오브젝트를 마지막 overflow 오브젝트와 교환” 삭제 패턴의 overflow 측 교체 — 제자리에서, 호출자의 sysop 하에서, 단일 분기:

// btree_overflow_record_replace_object -- src/storage/btree.c
assert (delete_helper->is_system_op_started); /* <- 반드시 sysop 내부에서 실행해야 함 */
// ... condensed: overflow_addr -> slot 1; BTREE_RV_SET_OVERFLOW_NODE; btree_record_replace_object ...
if (spage_update (thread_p, overflow_page, 1, overflow_record) != SP_SUCCESS)
{ assert_release (false); return ER_FAILED; } /* <- 유일한 오류 분기 */
log_append_undoredo_data (thread_p, RVBT_RECORD_MODIFY_UNDOREDO, &overflow_addr, ...);

두 오브젝트 모두 고정 크기이므로 길이가 변하지 않고, 정렬 순서는 구조적으로 보존된다 (교환은 마지막 오브젝트의 슬롯을 상속한다). FI_TEST 포인트가 절반만 적용된 복구 테스트를 위해 update 전후를 감싼다.

7.10 RVBT_*를 쓰고 RVOVF_*를 쓰지 않는 이유

섹션 제목: “7.10 RVBT_*를 쓰고 RVOVF_*를 쓰지 않는 이유”

세 가지 속성이 이 변형을 강제한다: (1) 페이지가 PAGE_BTREE이고 FILE_OVERFLOW가 아닌 인덱스 자체 파일(btid_int->sys_btid->vfid)에서 할당된다; (2) 두 슬롯짜리 spage가 제자리 spage_update를 허용하는데, 연속적인 OVERFLOW_REST_PART 바이트 형식에서는 불가능하다; (3) 모든 변경은 RVBT_RECORD_MODIFY_NO_UNDO, ..._UNDOREDO, 또는 RVBT_GET_NEWPAGE를 로그하며 — 이 레코드들은 BTREE_RV_OVERFLOW_FLAG가 태그된 addr.offset을 가지고, 고정 크기 OID 인코딩과 B+tree 논리적 undo를 이해하는 B+tree 레코드 변경 적용 루틴이 재생한다. RVOVF_*FILE_OVERFLOW 파일의 불투명한 바이트만 이해하므로, 여기서 재사용하면 구조적으로 잘못된 페이지를 재생하게 된다 — 그리고 overflow 편집과 leaf-link 편집은 하나의 RVBT_* 계열 아래서 함께 재생된다.

  1. OID 리스트 체인은 인덱스 자체 파일의 PAGE_BTREE 슬롯 페이지 위에서 의도적으로 재구현된 것이며, overflow_file.c를 재사용하지 않는다.
  2. BTREE_OVERFLOW_HEADER는 단일 next_vpid이고, 체인은 HEAD→tail로 단방향 연결되며 leaf는 HEAD 링크만 보관한다.
  3. 새 페이지는 하나의 sysop 하에서 HEAD 앞에 삽입된다 — 페이지 next_vpid 먼저, leaf 링크 두 번째 — sysop-end 이전 크래시 시 무해한 누수가 문서화되어 있다.
  4. 여유 공간 탐색은 최초 적합(first-fit)이고, 오브젝트 탐색은 고정 stride 이진 탐색이다.
  5. btree_find_oid_and_its_page는 선행자 페이지를 반환하여, 삭제 시 btree_modify_leaf_ovfl_vpidbtree_modify_overflow_link 중 선택할 수 있게 한다.
  6. 축소에는 두 분기가 있다: 마지막 오브젝트일 때 sysop 하에서 dealloc-and-unlink, 또는 생존 레코드에서 btree_record_remove_object를 통한 접합 제거; 조기 get_next 실패는 sysop이 열리기 전에 반환한다.
  7. 모든 편집은 BTREE_RV_OVERFLOW_FLAG가 태그된 주소로 RVBT_*를 로그한다 — 절대 RVOVF_*가 아니다.

Chapter 8: 크래시 복구와 예외 경로

섹션 제목: “Chapter 8: 크래시 복구와 예외 경로”

라이프사이클 챕터(3, 5, 6)는 뮤테이터가 무엇을 기록하는지를 다뤘다. 이 챕터는 다음 질문에 답한다: 서버가 두 로그 어펜드 사이에 죽는다면, 어느 레코드가 체인을 일관된 상태로 재현하는가, 그리고 어느 경로는 정상 라이프사이클을 전혀 거치지 않는가?

네 개의 recovery index가 이 서브시스템 전체를 담당한다(핸들러 네 개, 덤프 헬퍼 두 개). 다섯 번째 레코드인 길이 0의 LOG_DUMMY_OVF_RECORD는 복구 동작 없이, HA 리플리케이션과 vacuum 헤드 식별의 앵커 역할만 한다. undo, redo, physical logging, sysop 브래킷에 대한 개요는 상위 컴패니언 문서 cubrid-overflow-file.md를 참고한다.

8.1 디스패치 테이블: 네 개의 인덱스, 네 개의 핸들러

섹션 제목: “8.1 디스패치 테이블: 네 개의 인덱스, 네 개의 핸들러”

복구는 테이블 구동 방식이다. 각 rcvindexrecovery.cRV_fun[]의 행 하나에 대응하며, 구조는 { rcvindex, name, undofun, redofun, undo_dump, redo_dump }이다:

// RV_fun[] -- src/transaction/recovery.c
{RVOVF_NEWPAGE_INSERT, "RVOVF_NEWPAGE_INSERT",
NULL, /* <- no undo: insert is redo-only */
overflow_rv_newpage_insert_redo, NULL, log_rv_dump_hexa},
{RVOVF_NEWPAGE_LINK, "RVOVF_NEWPAGE_LINK",
overflow_rv_newpage_link_undo, /* <- undo: cut the new link to NULL */
overflow_rv_link, /* <- redo: re-establish the link */
overflow_rv_link_dump, overflow_rv_link_dump},
{RVOVF_PAGE_UPDATE, "RVOVF_PAGE_UPDATE",
log_rv_copy_char, /* <- undo: restore the before-image */
overflow_rv_page_update_redo, /* <- redo: set ptype then after-image */
overflow_rv_page_dump, overflow_rv_page_dump},
{RVOVF_CHANGE_LINK, "RVOVF_CHANGE_LINK",
overflow_rv_link, overflow_rv_link, /* <- undo and redo are the SAME fn */
overflow_rv_link_dump, overflow_rv_link_dump},

테이블에서 두 가지 설계 사실이 바로 눈에 띈다:

  • RVOVF_NEWPAGE_INSERT의 undo 슬롯은 NULL이다. 삽입은 페이지 쓰기를 되돌리지 않는다. abort 시에는 sysop이 페이지를 해제하므로 un-write할 내용 자체가 없다.
  • RVOVF_CHANGE_LINKundo와 redo에 동일한 함수를 사용한다. 레코드 자체가 대칭적(undo용 old VPID, redo용 new VPID)이며, 핸들러는 복구 시스템이 건네주는 값을 next_vpid에 복사할 뿐이다.
flowchart TD
  subgraph idx["recovery 인덱스 (recovery.h 100..103, 값 54..57)"]
    A["RVOVF_NEWPAGE_INSERT"]
    B["RVOVF_NEWPAGE_LINK"]
    C["RVOVF_PAGE_UPDATE"]
    D["RVOVF_CHANGE_LINK"]
  end
  A -->|redo| H1["overflow_rv_newpage_insert_redo<br/>= log_rv_copy_char"]
  B -->|undo| H2["overflow_rv_newpage_link_undo<br/>next_vpid := NULL"]
  B -->|redo| H3["overflow_rv_link<br/>next_vpid := data"]
  C -->|undo| H4["log_rv_copy_char<br/>before-image"]
  C -->|redo| H5["overflow_rv_page_update_redo<br/>ptype 재설정 + after-image"]
  D -->|undo / redo| H3

Figure 8-1. 네 인덱스와 각 핸들러. overflow_rv_link는 네 슬롯 중 세 개에서 공유된다.

8.2 RVOVF_NEWPAGE_INSERT — redo 전용 전체 페이지 재현

섹션 제목: “8.2 RVOVF_NEWPAGE_INSERT — redo 전용 전체 페이지 재현”

핸들러는 제네릭 log_rv_copy_char 위에 한 줄짜리 래퍼다. log_rv_copy_charmemcpy(rcv->pgptr + rcv->offset, rcv->data, rcv->length)pgbuf_set_dirty를 수행한다. 여기서 rcv->offset은 항상 0이므로 레코드는 페이지 맨 앞에 기록된다:

// overflow_rv_newpage_insert_redo -- src/storage/overflow_file.c
int overflow_rv_newpage_insert_redo (THREAD_ENTRY * thread_p, LOG_RCV * rcv)
{ return log_rv_copy_char (thread_p, rcv); }

발행 지점 (챕터 3). overflow_insertnext_vpid, length, 페이로드를 스탬프한 뒤 페이지당 한 번씩 이 레코드를 로깅한다. log_append_redo_data(…, RVOVF_NEWPAGE_INSERT, &addr, copy_length + (copyto - addr.pgptr), addr.pgptr) — 소스 포인터는 copyto가 아니라 페이지 맨 앞(addr.pgptr)이다. 따라서 redo 이미지는 이미 설정된 next_vpid를 포함한 헤더 플러스 페이로드이며, 레코드 하나로 단일 memcpy에 앞부분 전체를 재구성한다. redo 전용인 이유: 콘텐츠와 링크가 하나의 원자적 레코드를 공유하므로 절반만 쓰여진 페이지를 undo할 상황이 없다. abort 시에는 sysop(§8.8)이 페이지를 회수한다.

섹션 제목: “8.3 RVOVF_NEWPAGE_LINK — undo는 링크를 끊고, redo는 링크를 복원한다”

이 인덱스는 overflow_update(챕터 5)에서만 나타난다. 재작성 중 페이지가 부족해 체인을 연장해야 할 때 사용된다. file_alloc으로 새 next_vpid를 확보한 뒤 log_append_undoredo_data(…, RVOVF_NEWPAGE_LINK, &addr, 0, sizeof(VPID), NULL, &next_vpid)를 로깅한다. 레코드는 비대칭이다 — undo 길이 0, redo 길이 sizeof(VPID) — 이전에 존재하지 않던 forward pointer를 새로 만들었다는 시그니처다.

// overflow_rv_link -- src/storage/overflow_file.c (redo: install the pointer)
{ ((OVERFLOW_REST_PART *) rcv->pgptr)->next_vpid = *(VPID *) rcv->data; /* <- splice in */
pgbuf_set_dirty (thread_p, rcv->pgptr, DONT_FREE); ... }
// overflow_rv_newpage_link_undo -- src/storage/overflow_file.c (undo: sever)
{ VPID_SET_NULL (&((OVERFLOW_REST_PART *) rcv->pgptr)->next_vpid); /* <- ignores rcv->data */
pgbuf_set_dirty (thread_p, rcv->pgptr, DONT_FREE); ... }

undo 핸들러는 rcv->data무시한다(따라서 undo 길이 0). undo는 항상 “이전에 forward link가 없었다”를 의미하기 때문이다. 두 핸들러 모두 첫 번째 페이지에도 OVERFLOW_REST_PART로 캐스팅한다. next_vpid가 두 구조체 모두에서 offset 0에 위치하기 때문이다(챕터 1).

불변 조건 — 도달 가능한 페이지는 절대 댕글링 소스가 아니다. 선행 페이지의 next_vpid는 완전히 복구 가능한 후속 페이지를 가리키거나 NULL이다. 회수된 페이지를 가리키는 경우는 없다. 순서 보장으로 강제된다: 페이지 할당(별도 로그)이 RVOVF_NEWPAGE_LINK보다 먼저 이루어지며, 둘 다 동일한 sysop 안에 있다. undo 시 링크는 NULL이 되고 파일 undo가 후속 페이지를 회수한다 — 어느 재현 순서에서도 선행 페이지가 회수된 페이지를 가리키는 일은 없다. 이 조건이 깨지면 overflow_traverse(챕터 4)가 해제된 페이지로 걸어들어가 쓰레기 값을 next_vpid로 읽게 된다.

섹션 제목: “8.4 RVOVF_CHANGE_LINK — 축소를 위한 대칭 링크 재작성”

overflow_update가 여분의 페이지를 남기며 복사를 마치면, 마지막으로 필요한 페이지의 링크를 NULL로 만들고 꼬리를 해제해 체인을 축소한다(챕터 5, 6). tmp_vpid를 NULL로 설정한 뒤 log_append_undoredo_data(…, RVOVF_CHANGE_LINK, &addr, sizeof(VPID), sizeof(VPID), &next_vpid, &tmp_vpid)를 로깅한다 — 양쪽 이미지가 모두 완전한 VPID다: undo용으로는 사라질 꼬리를 가리키던 old forward link, redo용으로는 새 값(NULL). 연산의 본질이 “VPID 하나를 다른 값으로 교체”이므로 undo와 redo가 overflow_rv_link를 공유한다 — undo는 old VPID를 복원하고, redo는 NULL을 기록한다(따라서 §8.1에서 양쪽 슬롯에 모두 등장한다).

stateDiagram-v2
  direction LR
  [*] --> Linked: 페이지가 꼬리를 가리킴
  Linked --> Unlinked: redo CHANGE_LINK \n next_vpid를 NULL로 설정
  Unlinked --> Linked: undo CHANGE_LINK \n next_vpid를 data에서 복원

Figure 8-2. RVOVF_CHANGE_LINK는 제자리에서 가역적이다. 레코드가 양 끝점을 모두 담고 있으므로 어느 방향이든 next_vpid에 복사하는 것으로 처리된다.

NEWPAGE_LINK와의 차이. 둘 다 next_vpid를 재작성하지만 서로 교환 불가능하다: NEWPAGE_LINK는 체인을 확장하고 undo 시 NULL로 되돌린다(레코드에 없음); CHANGE_LINK는 체인을 축소하고 undo 시 old 값으로 복원한다(레코드에 있음). 두 인덱스가 겹치는 지점은 redo뿐이다.

8.5 RVOVF_PAGE_UPDATE — ptype 가드를 포함한 before/after 이미지

섹션 제목: “8.5 RVOVF_PAGE_UPDATE — ptype 가드를 포함한 before/after 이미지”

인플레이스 콘텐츠 재작성(챕터 5)은 덮어쓰는 페이지마다 undo용 before-image와 redo용 after-image를 로깅한다:

// overflow_update -- src/storage/overflow_file.c (first-part branch)
if (hdr_length + old_length > DB_PAGESIZE) /* <- old payload spans full page */
log_append_undo_data (..., RVOVF_PAGE_UPDATE, &addr, DB_PAGESIZE, addr.pgptr);
else /* <- only the used prefix */
log_append_undo_data (..., RVOVF_PAGE_UPDATE, &addr, hdr_length + old_length, addr.pgptr);
// ... memcpy of new content, then ...
log_append_redo_data (..., RVOVF_PAGE_UPDATE, &addr, copy_length + hdr_length, addr.pgptr);

undo 길이는 old 페이로드가 페이지를 가득 채웠을 때 DB_PAGESIZE, 그렇지 않으면 사용된 접두부만 — 뒤쪽 쓰레기 바이트는 절대 포함하지 않는다. redo 길이 copy_length + hdr_length는 헤더와 새 슬랩의 합이다.

// overflow_rv_page_update_redo -- src/storage/overflow_file.c
{ (void) pgbuf_set_page_ptype (thread_p, rcv->pgptr, PAGE_OVERFLOW); /* <- re-stamp */
return log_rv_copy_char (thread_p, rcv); } /* <- then bytes */

undo 핸들러는 순수한 log_rv_copy_char(§8.1 undo 슬롯)다. before-image는 ptype 수정이 필요 없다. 이미지를 찍었을 때 페이지가 이미 해당 타입을 갖고 있었기 때문이다.

redo에서 PAGE_OVERFLOW를 재스탬프하는 이유. 복구 시 after-image가 적용되는 페이지는 재할당 과정에서 pgbuf의 페이지 타입이 초기화됐거나, 애초에 영속화된 적이 없을 수 있다. pgbuf_set_page_ptype 호출은 redo를 그 자체로 완결 가능하게 만든다.

덤프 헬퍼는 로그 덤프 시 복구 페이로드를 텍스트로 렌더링한다. 상태를 변경하지 않는다.

// overflow_rv_page_dump -- src/storage/overflow_file.c
void overflow_rv_page_dump (FILE * fp, int length, void *data)
{ OVERFLOW_REST_PART *rest_parts = (OVERFLOW_REST_PART *) data;
int hdr_length = offsetof (OVERFLOW_REST_PART, data); /* <- skip link header */
fprintf (fp, "Overflow Link to Volid = %d|Pageid = %d\n",
rest_parts->next_vpid.volid, rest_parts->next_vpid.pageid);
log_rv_dump_char (fp, length - hdr_length, (char *) data + hdr_length); } /* <- ascii payload */

overflow_rv_link_dump(fp, length_ignore, data)length를 무시하고(링크 레코드는 항상 VPID 하나) VPID를 fprintf한다. 두 링크 인덱스의 undo/redo 모두에 사용된다. overflow_rv_page_dump는 헤더 next_vpid를 출력한 뒤 나머지 length - hdr_length 바이트를 아스키 덤프한다 — RVOVF_PAGE_UPDATE redo 레이아웃에 대응한다. RVOVF_NEWPAGE_INSERT는 제네릭 log_rv_dump_hexa(§8.1)를 사용한다. 이미지가 페이지 맨 앞에서 시작하므로 16진수 덤프로 처리한다.

8.7 LOG_DUMMY_OVF_RECORD — 복구 동작 없는 길이 0의 앵커

섹션 제목: “8.7 LOG_DUMMY_OVF_RECORD — 복구 동작 없는 길이 0의 앵커”

LOG_DUMMY_OVF_RECORD(“overflow 레코드 첫 번째 파트의 지시자”)는 RV_fun[]에 없다 — 핸들러도, undo/redo도 없다. 두 뮤테이터 모두 log_append_empty_record(길이 0 본문)로 첫 번째 페이지에서만 발행한다: overflow_inserti == 0 브랜치는 file_type != FILE_TEMP 조건으로 가드되고, overflow_update는 first-part 브랜치에서 발행한다.

이 레코드는 페이지별 레코드가 갖지 못한 인식 가능한 LSN 랜드마크다. 모든 페이지는 거의 동일한 RVOVF_NEWPAGE_INSERT / RVOVF_PAGE_UPDATE를 발행하므로, 로그 컨슈머는 하나의 overflow 객체가 어디서 끝나고 다음이 어디서 시작하는지 알 수 없다. 첫 번째 페이지에서만 발행되므로 객체의 head VPID를 표시한다 — vacuum이나 HA/CDC 컨슈머가 필요로 하는 앵커다(CDC 워커 cdc_get_overflow_recdes가 이 구분자의 사용처 중 하나다).

8.8 sysop 브래킷: 반쯤 만들어지거나 고아가 된 체인이 살아남지 못하는 이유

섹션 제목: “8.8 sysop 브래킷: 반쯤 만들어지거나 고아가 된 체인이 살아남지 못하는 이유”

모든 뮤테이터는 할당과 쓰기를 sysop으로 감싼다: log_sysop_start, file_alloc_multiple(성공 시 file_init_page_type, FILE_TEMP의 경우 file_init_temp_page_type 전달), 페이지별 memcpy + RVOVF_NEWPAGE_INSERT, 그리고 성공 시 log_sysop_attach_to_outer — 오류 시에는 exit_on_error에서 log_sysop_abort. 이 브래킷은 개별 레코드로는 불가능한 전부-또는-전무 보장을 제공한다:

  • attach 이전 → undo가 log_sysop_abort의 보상을 실행해 모든 페이지를 해제한다. RVOVF_NEWPAGE_INSERT redo 이미지는 살아남지 못한다.
  • attach 이후, 부모 커밋 이전 → 쓰기는 부모 트랜잭션에 귀속된다. abort 시 파일 레이어 undo가 페이지를 회수한다.
  • 부모 커밋 이후 → redo가 각 자기 완결적 레코드를 순서대로 재현한다. 이미 플러시된 페이지가 어느 것이든 체인은 올바르게 조립된다.

overflow_updateRVOVF_NEWPAGE_LINK / RVOVF_CHANGE_LINK를 같은 브래킷에 추가한다. 해당 undo 핸들러(§8.3, §8.4)는 선행 페이지의 next_vpid를 복원하므로, 부분적으로 크기가 조정된 체인은 업데이트 이전 토폴로지로 롤백된다 — 물리적 이미지와 sysop 해제가 모든 크래시 윈도우를 닫는다.

8.9 예외 경로: 임시 파일, no_logging, MVCC 피크, 디버그 덤퍼

섹션 제목: “8.9 예외 경로: 임시 파일, no_logging, MVCC 피크, 디버그 덤퍼”

FILE_TEMP와 no_logging은 RVOVF 로깅을 억제한다. 임시 overflow 파일은 휘발성이므로 발행 지점이 가드된다: overflow_insertRVOVF_NEWPAGE_INSERT 어펜드를 if (file_type != FILE_TEMP && thread_p->no_logging != true) 조건으로 감싸고, overflow_update는 콘텐츠 redo를 no_logging != true만으로 가드한다. 로깅이 억제되면 해당 페이지에 대한 핸들러는 절대 호출되지 않는다 — 재현할 레코드 자체가 없다.

임시 페이지는 redo가 아닌 할당 시점에 타입을 받는다. redo 경로가 없으므로 타입은 처음부터 올바르게 설정되어야 한다: overflow_insertfile_alloc_multipleFILE_TEMP일 때 file_init_page_type 대신 file_init_temp_page_type을 받는다(file_type != FILE_TEMP ? file_init_page_type : file_init_temp_page_type, §8.8). 재스탬프하는 레코드가 없기 때문이다. 이 분기는 오직 여기서만 존재한다: overflow_updateassert (file_type == FILE_MULTIPAGE_OBJECT_HEAP)으로 시작하며 FILE_TEMP에 대해서는 절대 실행되지 않으므로, 확장 브랜치의 file_alloc은 임시 대안 없이 file_init_page_type에 하드와이어드다.

overflow_get_first_page_data를 이용한 MVCC 피크. 챕터 4의 순회 없이 인라인 페이로드만 필요한 독자는, 첫 번째 페이지를 fixed 상태로 보유하는 동안 이 접근자를 호출한다: assert (page_ptr != NULL)((OVERFLOW_FIRST_PART *) page_ptr)->data, 즉 link+length 헤더 너머의 주소를 반환한다. MVCC 코드는 이 접근자로 선두 바이트(MVCC id를 담은 레코드 헤더)를 인플레이스로 피크할 수 있다 — NULL이 아니라는 assert 이상의 fix, latch, 또는 검증 없이.

overflow_dump는 CUBRID_DEBUG 전용이다. exit에 영향을 주는 브랜치는 세 개다: pgbuf_fix 실패 조기 반환 두 개, 그리고 VPID_ISNULL 손상 검사 하나. head VPID에서의 초기 fix와 페이지 전진 후의 per-page fix는 모두 return ((ret = er_errid ()) == NO_ERROR) ? ER_FAILED : ret;로 bail out한다 — fix 실패는 NULL 페이지를 걷는 대신 덤프를 중단한다(페이지 버퍼 오류 또는 ER_FAILED 전파). 비직관적인 브랜치는 현재 페이지가 unfix된 , remain_length > 0 루프 내부의 VPID_ISNULL 검사다:

// overflow_dump -- src/storage/overflow_file.c (#if defined (CUBRID_DEBUG))
while (remain_length > 0)
{ remain_length -= dump_length;
if (remain_length > 0)
{ pgbuf_unfix_and_init (thread_p, pgptr); /* <- release current before checking */
if (VPID_ISNULL (&next_vpid)) /* <- chain ends early but bytes remain */
{ er_set (..., ER_HEAP_OVFADDRESS_CORRUPTED, ...); return ...; }
pgptr = pgbuf_fix (..., &next_vpid, ...); /* <- advance to next page */
if (pgptr == NULL) /* <- second fix-failure early return */
{ return ((ret = er_errid ()) == NO_ERROR) ? ER_FAILED : ret; }
next_vpid = ((OVERFLOW_REST_PART *) pgptr)->next_vpid; } }

이 코드는 선언된 length가 링크가 공급하는 바이트보다 더 많이 요구하는 손상 케이스를 포착한다. NULL next pointer를 역참조하는 대신 오류를 올린다 — 챕터 4가 의존하는 length-대-링크 일관성과 동일한 불변 조건이다.

  1. 네 인덱스, 네 핸들러: insert(redo 전용 전체 페이지 복사), NEWPAGE_LINK(undo는 NULL / redo는 링크 설치), PAGE_UPDATE(undo는 before-image / redo는 ptype 재스탬프 후 after-image), CHANGE_LINK(대칭 VPID 스왑, 양방향 동일 핸들러).
  2. overflow_rv_linkRVOVF_NEWPAGE_LINK redo와 RVOVF_CHANGE_LINK 양쪽 슬롯 모두에서 공유된다. 확장/축소의 비대칭성은 undo 쪽에만 존재한다(overflow_rv_newpage_link_undorcv->data를 무시하고 강제로 NULL). PAGE_UPDATE redo는 PAGE_OVERFLOW를 재확인하는 유일한 경로다. insert와 FILE_TEMP 페이지는 할당 시 file_init_page_type / file_init_temp_page_type에서 타입을 받는다. 이 temp/non-temp 분기는 overflow_insert에만 존재한다. overflow_updateFILE_MULTIPAGE_OBJECT_HEAP을 assert하며 임시 파일을 절대 처리하지 않는다.
  3. 크래시 후 반쯤 만들어지거나 고아가 된 체인은 살아남지 못한다: 콘텐츠와 링크는 레코드 단위로 원자적이고, 연산 전체는 abort 시 페이지를 해제하는 sysop으로 감싸져 있다. FILE_TEMP / no_logging은 RVOVF 어펜드 자체를 건너뛴다.
  4. LOG_DUMMY_OVF_RECORD는 객체 헤드의 길이 0 LSN 랜드마크다. overflow_get_first_page_data는 latch 없는 MVCC 피크다. overflow_dump(CUBRID_DEBUG 전용)는 pgbuf_fix 실패 시 중단하고 length-대-링크 불일치를 손상으로 플래그한다.

아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.

SymbolFileLine
CEIL_PTVDIVsrc/base/memory_alloc.h57
BTREE_LEAF_RECORD_OVERFLOW_OIDSsrc/storage/btree.c115
BTREE_LEAF_RECORD_OVERFLOW_KEYsrc/storage/btree.c117
BTREE_RV_SET_OVERFLOW_NODEsrc/storage/btree.c971
btree_create_overflow_key_filesrc/storage/btree.c1975
btree_store_overflow_keysrc/storage/btree.c2021
btree_load_overflow_keysrc/storage/btree.c2131
btree_delete_overflow_keysrc/storage/btree.c2202
btree_start_overflow_pagesrc/storage/btree.c3973
btree_get_new_pagesrc/storage/btree.c5135
btree_initialize_new_pagesrc/storage/btree.c5165
btree_modify_leaf_ovfl_vpidsrc/storage/btree.c9977
btree_modify_overflow_linksrc/storage/btree.c10054
btree_key_append_object_as_new_overflowsrc/storage/btree.c11330
btree_key_append_object_to_overflowsrc/storage/btree.c11452
btree_find_free_overflow_oids_pagesrc/storage/btree.c11579
btree_find_oid_and_its_pagesrc/storage/btree.c11646
btree_find_oid_from_ovflsrc/storage/btree.c12037
btree_seq_find_oid_from_ovflsrc/storage/btree.c12210
btree_overflow_remove_objectsrc/storage/btree.c32344
btree_overflow_record_replace_objectsrc/storage/btree.c33340
struct non_leaf_recsrc/storage/btree.h103
LEAF_RECsrc/storage/btree.h110
ovfidsrc/storage/btree.h129
btree_get_overflow_headersrc/storage/btree_load.c340
btree_init_overflow_headersrc/storage/btree_load.c576
btree_get_next_overflow_vpidsrc/storage/btree_load.c675
btree_overflow_headersrc/storage/btree_load.h245
file_init_page_typesrc/storage/file_manager.c5349
file_init_temp_page_typesrc/storage/file_manager.c5365
file_deallocsrc/storage/file_manager.c6116
FILE_MULTIPAGE_OBJECT_HEAPsrc/storage/file_manager.h43
FILE_BTREE_OVERFLOW_KEYsrc/storage/file_manager.h45
FILE_TEMPsrc/storage/file_manager.h52
struct file_ovf_heap_dessrc/storage/file_manager.h90
struct file_ovf_btree_dessrc/storage/file_manager.h106
file_create_with_npagessrc/storage/file_manager.h187
heap_ovf_find_vfidsrc/storage/heap_file.c6462
heap_ovf_insertsrc/storage/heap_file.c6569
heap_ovf_updatesrc/storage/heap_file.c6597
heap_ovf_deletesrc/storage/heap_file.c6632
heap_ovf_getsrc/storage/heap_file.c6717
heap_get_mvcc_rec_header_from_overflowsrc/storage/heap_file.c19541
heap_set_mvcc_rec_header_on_overflowsrc/storage/heap_file.c19567
heap_get_bigone_contentsrc/storage/heap_file.c19610
heap_delete_adjust_headersrc/storage/heap_file.c21290
heap_delete_bigonesrc/storage/heap_file.c21389
heap_delete_physicalsrc/storage/heap_file.c22388
heap_log_delete_physicalsrc/storage/heap_file.c22428
HEAP_HEADER_AND_CHAIN_SLOTIDsrc/storage/heap_file.h62
OVERFLOW_ALLOCVPID_ARRAY_SIZEsrc/storage/overflow_file.c43
OVERFLOW_DO_FUNCsrc/storage/overflow_file.c45
OVERFLOW_DO_DELETEsrc/storage/overflow_file.c47
overflow_insertsrc/storage/overflow_file.c81
overflow_insertsrc/storage/overflow_file.c82
overflow_next_vpidsrc/storage/overflow_file.c275
overflow_next_vpidsrc/storage/overflow_file.c276
overflow_traversesrc/storage/overflow_file.c296
overflow_traversesrc/storage/overflow_file.c297
overflow_updatesrc/storage/overflow_file.c374
log_sysop_startsrc/storage/overflow_file.c407
log_sysop_attach_to_outersrc/storage/overflow_file.c594
log_sysop_abortsrc/storage/overflow_file.c600
overflow_delete_internalsrc/storage/overflow_file.c613
overflow_delete_internalsrc/storage/overflow_file.c614
overflow_deletesrc/storage/overflow_file.c645
overflow_flush_internalsrc/storage/overflow_file.c656
overflow_flushsrc/storage/overflow_file.c678
overflow_get_lengthsrc/storage/overflow_file.c691
overflow_get_lengthsrc/storage/overflow_file.c692
overflow_get_nbytessrc/storage/overflow_file.c738
overflow_get_nbytessrc/storage/overflow_file.c739
overflow_getsrc/storage/overflow_file.c917
overflow_get_capacitysrc/storage/overflow_file.c934
overflow_get_capacitysrc/storage/overflow_file.c935
overflow_dumpsrc/storage/overflow_file.c1039
overflow_rv_newpage_insert_redosrc/storage/overflow_file.c1113
overflow_rv_newpage_link_undosrc/storage/overflow_file.c1125
overflow_rv_linksrc/storage/overflow_file.c1145
overflow_rv_link_dumpsrc/storage/overflow_file.c1165
overflow_rv_page_update_redosrc/storage/overflow_file.c1178
overflow_rv_page_update_redosrc/storage/overflow_file.c1179
overflow_rv_page_dumpsrc/storage/overflow_file.c1193
overflow_get_first_page_datasrc/storage/overflow_file.c1217
overflow_get_first_page_datasrc/storage/overflow_file.c1218
OVERFLOW_FIRST_PARTsrc/storage/overflow_file.h37
overflow_first_partsrc/storage/overflow_file.h38
struct overflow_first_partsrc/storage/overflow_file.h38
OVERFLOW_REST_PARTsrc/storage/overflow_file.h45
overflow_rest_partsrc/storage/overflow_file.h46
struct overflow_rest_partsrc/storage/overflow_file.h46
PAGE_OVERFLOWsrc/storage/storage_common.h158
log_append_empty_recordsrc/transaction/log_manager.c3138
log_rv_copy_charsrc/transaction/log_manager.c9087
LOG_DUMMY_OVF_RECORDsrc/transaction/log_record.hpp124
RV_funsrc/transaction/recovery.c54
RVFL_DEALLOCsrc/transaction/recovery.h57
RVFL_USER_PAGE_MARK_DELETEsrc/transaction/recovery.h63
RVHF_STATSsrc/transaction/recovery.h80
RVOVF_NEWPAGE_INSERTsrc/transaction/recovery.h100
RVOVF_NEWPAGE_LINKsrc/transaction/recovery.h101
RVOVF_PAGE_UPDATEsrc/transaction/recovery.h102
RVOVF_CHANGE_LINKsrc/transaction/recovery.h103
RVBT_UPDATE_OVFIDsrc/transaction/recovery.h123
  • cubrid-overflow-file.md — 상위 개요 문서. 함께 볼 문서: cubrid-heap-manager-detail.md (REC_BIGONE) 및 cubrid-btree-detail.md (overflow-OID 페이지).
  • 코드: src/storage/overflow_file.{c,h}; 호출부는 src/storage/heap_file.csrc/storage/btree.c.
  • 방법론: knowledge/methodology/code-analysis-detail-doc.md.