CAS 프로토콜 참조 (한국어)¶
🌐 PROTOCOL.md의 번역입니다. 영어 원문이 표준이며, CI가 영어 원문과의 구조 일치를 검사합니다.
pycubrid가 구현하는 CUBRID CAS(Common Application Server) 와이어 프로토콜의 기술 문서.
CAS가 처음이라면 이 참조 전에 CAS 아키텍처와 프로토콜 개념에서 브로커, CAS 프로세스, CCI를 소개하고 DB-API 호출을 함수 코드에 대응시킨 설명을 먼저 읽으세요.
목차¶
- 개요
- 아키텍처
- 패킷 프레이밍
- 연결 핸드셰이크
- 요청/응답 수명 주기
- 함수 코드
- 패킷 클래스
- ClientInfoExchangePacket
- OpenDatabasePacket
- 연결 문자셋
- PrepareAndExecutePacket
- PreparePacket
- ExecutePacket
- FetchPacket
- CommitPacket
- RollbackPacket
- CloseDatabasePacket
- CloseQueryPacket
- GetEngineVersionPacket
- GetSchemaPacket
- BatchExecutePacket
- ExecuteArrayPacket
- LOBNewPacket
- LOBWritePacket
- LOBReadPacket
- GetLastInsertIdPacket
- GetDbParameterPacket / SetDbParameterPacket
- PacketWriter
- PacketReader
- 와이어 데이터 타입
- 에러 처리
- 상수 참조
개요¶
CUBRID는 클라이언트-서버 통신에 CAS(Common Application Server)라는 독자 바이너리 프로토콜을 사용합니다. 프로토콜은 TCP 소켓 위에서 big-endian 바이트 순서로 동작합니다.
주요 특성:
- 전송: TCP (브로커 기본 포트 33000)
- 바이트 순서: Big-endian (네트워크 바이트 순서)
- 프레이밍: 4바이트 데이터 길이 + 4바이트 CAS 정보 + 페이로드
- 핸드셰이크: 매직 문자열
CUBRK(평문) 또는CUBRS(STARTTLS) + 클라이언트 타입 + 프로토콜 버전 - TLS (선택): 핸드셰이크 후
OPEN_DATABASE이전에start_tls()/wrap_socket()으로 STARTTLS 방식 업그레이드 - 세션: 상태 저장 —
OpenDatabase이후 세션 ID 유지
아키텍처¶
graph LR
client["Client (pycubrid)"] -- TCP port 33000 --> broker[Broker]
broker --> cas[CAS Process]
cas --> db[CUBRID Database]
- 클라이언트가 33000 포트의 브로커에 연결
- 브로커가 핸드셰이크를 수행하고 다른 포트의 CAS 프로세스로 리다이렉트할 수 있음
- 클라이언트가 CAS 프로세스에서 데이터베이스 세션을 엶
- 이후 모든 요청은 CAS 세션을 통해 진행
패킷 프레이밍¶
초기 핸드셰이크 이후의 모든 패킷은 다음 프레임 형식을 사용합니다:
graph LR
data["Data Length (4B) big-endian int32"] --> casinfo["CAS Info (4B) session state"]
casinfo --> payload["Payload (variable) function-specific"]
| 필드 | 크기 | 설명 |
|---|---|---|
| Data Length | 4바이트 | CAS Info + Payload 길이 (부호 있는 int32, big-endian) |
| CAS Info | 4바이트 | 서버가 유지하는 세션 상태 바이트 |
| Payload | 가변 | 함수 코드 + 인자(요청) 또는 응답 데이터 |
CAS Info의 첫 바이트는 트랜잭션 상태입니다. 0은 OUT_TRAN, 1은
IN_TRAN입니다. END_TRAN이나 자동 커밋 요청 뒤 OUT_TRAN이 되어도
소켓 또는 CAS 세션이 해제된 것은 아니므로 다음 요청은 같은 전송 경로로
보냅니다. 그래도 CAS는 OUT_TRAN 응답 뒤 소켓을 닫을 수 있으므로(메모리
재시작, cubrid broker reset, CHANGE CLIENT) 드라이버는 JDBC
UClientSideConnection.checkReconnect처럼 다음 요청 전에 CHECK_CAS(FC=32)를
보냅니다. 검사가 실패할 때만 요청당 한 번, 그 요청을 처음 보내기 전에 세션을
교체합니다(#485). CHECK_CAS 실패 후에는 명시적 ping(reconnect=True) 복구도
시도할 수 있지만, 결과가 불확실한 일반 SQL 요청을 자동 재실행하지는 않습니다.
commit과 rollback은 END_TRAN 전에 열린 커서의 쿼리 핸들에 CLOSE_REQ(FC=6)를
보냅니다.
지연 닫기(#488): 직접 연결된 CUBRID CAS가 OPEN_DATABASE에서 statement pooling을 알리면
(broker_info[0] == 1 및 broker_info[2] == 1) 쿼리 핸들이 END_TRAN 뒤에도 남으므로, autocommit 모드에서
cursor.close()나 커서 재실행으로 해제되는 핸들과, 모드와 관계없이 close() 없이
수거된 커서의 핸들은 별도의 CLOSE_REQ로 닫지 않습니다. 그 id는 다음 FC41 요청의
auto-commit 플래그 뒤에 추가 prepare 인자로 붙고(id마다 prepare 인자 수가 하나씩
늘어남), CAS는 문장을 준비하기 전에 그 핸들을 해제합니다. 이는 JDBC 지연 닫기의
와이어 방식이지만 정책은 다릅니다: JDBC는 결과 집합이 없는 문장만 지연하고
SELECT/CALL/EVALUATE 핸들은 바로 닫지만(CLOSE_USTATEMENT), pycubrid는 결과 집합
핸들도 지연합니다. 그 문장이 네이티브 오류로 실패해도 핸들은 해제됩니다. 한 문장은
최대 256개 id를 싣습니다. 256개가 대기 중일 때의 명시적 해제는 CLOSE_REQ를 바로
보내고, 수거된 커서는 항상 대기열에 들어가 이후 문장들에 실려 갑니다. FC20 배치
요청은 대기 중인 id를 싣지 않으므로 executemany()로 해제한 핸들은 다음 FC41이나
트랜잭션 경계까지 기다립니다. 빈 executemany()는 배치 SQL을 보내거나 대기열을
비우지 않으며, 이전 핸들을 지연할 수 있으면 CLOSE_REQ도 보내지 않습니다.
대기열이 가득 찬 경우처럼 지연할 수 없으면 기존의 즉시 닫기 규칙을 적용합니다. commit()과
rollback()은 END_TRAN 전에 그 세션의 대기 중인 id를 모두 CLOSE_REQ로 닫으며,
이는 예전에 참조가 없는 커서를 닫던 것과 같습니다. 대기 중인 id는 하나의 물리 세션에 속하며, 그 세션이
폐기되거나 교체되면 버려지고 다른 세션으로는 절대 보내지 않습니다. statement
pooling이 꺼져 있으면 CAS가 커밋마다 핸들을 해제하므로 예전처럼 CLOSE_REQ를 바로
보내고, 수거된 커서의 핸들은 다음 커밋에 맡깁니다. 샤드 프록시(broker_info[0]이
CUBRID를 뜻하는 1이 아닌 경우)는 추가 인자를 무시하므로 명시적 해제는 CLOSE_REQ를 바로
보냅니다. 수거된 프록시 커서의 핸들은 프록시 트랜잭션·세션 정리에 맡깁니다.
세션 설정(교체 세션의 escape 모드 감지와 설정 복원) 중의 명시적 해제는 지연하지 않습니다.
직접 연결된 pooling 활성 세션의 수거된 커서는 아무것도 보낼 수 없으므로 설정 중에도 핸들을 대기열에 넣을 수 있습니다.
각 핸들은 자신을 연 세션의 세대를 기억하므로, 재접속 뒤에 수거된 커서가 새 세션의
핸들 id를 해제하는 일은 없습니다.
pooling 비활성화 시의 소유권(#584): 직접 연결된 CUBRID CAS에서 statement
pooling이 명시적으로 꺼져 있으면 트랜잭션 경계에서 일반 커서·스키마 핸들을
모두 해제하며, 같은 물리 세션에서 그 id를 즉시 재사용할 수 있습니다.
드라이버는 END_TRAN, 자동 커밋 FC41/FC3·버전 요청, 실패 후 자동 rollback하는
자동 커밋 FC2 또는 일반 자동 커밋 결과의 마지막 FETCH의 실제 응답이 OUT_TRAN이면
소유권을 폐기합니다. 성공한 FC2 prepare는 경계를 확정하지 않습니다. 응답 파싱과
INSERT의 identity 조회 전에 처리하므로, 후속 조회가 IN_TRAN을 반환해도 이미
해제된 핸들을 남기지 않습니다. 현재 FC41 응답에서 해제된 핸들은 완전한 응답의
DataError 경로에서도 채택하지 않습니다. 버퍼의 행은 계속 읽을 수 있지만,
미완료 결과에서 서버 FETCH가 필요하면 InterfaceError가 발생하고 완료된
결과는 정상 EOF를 유지합니다. 물리 세대와 세션 검증은 바뀌지 않습니다.
CHECK_CAS·CLOSE_REQ·파라미터·스키마 요청/FETCH·배치 응답의 OUT_TRAN만으로는
이 경계를 확정하지 않으며, 수동 트랜잭션 FETCH와 pooling 활성·프록시 세션의
기존 동작을 유지합니다.
자동 no_backslash_escapes 감지는 물리 세션 단위입니다. 새 세션은
파라미터 바인딩 재개 전에 감지하지만 정상적인 동일 세션 CHECK_CAS는
감지하지 않습니다. 명시적 모드는 유지됩니다. 복구 감지 실패 시 대체
세션을 폐기하고 ping은 False를 반환하며, 이전 세션 세대에서 바인딩한
비동기 SQL은 전송 전에 거부합니다.
헤더 생성:
from pycubrid.packet import build_protocol_header
header = build_protocol_header(data_length=42, cas_info=b"\x00\x00\x00\x00")
# 8바이트 반환: 4바이트 길이 + 4바이트 cas_info
예외¶
핸드셰이크 패킷(ClientInfoExchangePacket)은 이 프레이밍을 사용하지 않습니다 — 길이/cas_info 헤더 없이 raw 바이트를 보냅니다.
데이터베이스 열기 패킷(OpenDatabasePacket)은 헤더 없이 raw 바이트(628바이트)를 보내지만, 응답은 표준 프레이밍을 사용합니다.
연결 핸드셰이크¶
연결 흐름은 최대 4단계입니다 (TLS 단계는 선택):
1단계: 클라이언트 정보 교환¶
sequenceDiagram
participant Client
participant Broker
alt ssl requested
Client->>Broker: ClientInfoExchange raw 10B ("CUBRS", CLIENT_JDBC=3, CAS_VER=0x48, padding)
else plaintext
Client->>Broker: ClientInfoExchange raw 10B ("CUBRK", CLIENT_JDBC=3, CAS_VER=0x48, padding)
end
Broker-->>Client: status int32 (0 ok / >0 redirect port / <0 fail-fast)
- 매직 문자열:
"CUBRK"(평문) 또는"CUBRS"(STARTTLS 요청) — ASCII 5바이트 - 클라이언트 타입:
CLIENT_JDBC = 3(pycubrid는 JDBC 호환 클라이언트로 식별됨) - CAS 버전:
PROTO_INDICATOR(0x40) | VERSION(8) = 0x48 - 상태 int32: 부호 있는 big-endian
> 0— 리다이렉트 포트: 소켓을 닫고(host, status)로 재연결, 핸드셰이크는 반복하지 않음 (JDBCBrokerHandler.connectBroker동작 미러링).65535보다 큰 상태 값은 TCP 포트가 아니므로 두 드라이버 모두 재연결 전에OperationalError를 발생시킴 (#896)< 0— 즉시 실패:OperationalError(status)발생== 0— 현재 소켓 재사용 (다이렉트 모드)
1.5단계: TLS 업그레이드 (선택)¶
connect 호출에서 ssl이 참이었으면, 어떤 OPEN_DATABASE 바이트가 쓰이기 전에 라이브 전송을 업그레이드합니다:
- 동기 드라이버:
ssl.SSLContext.wrap_socket(sock, server_hostname=host) - 비동기 드라이버:
loop.start_tls(transport, protocol, context, server_hostname=host, ssl_handshake_timeout=...)
TLS 핸드셰이크에는 설정된 read_timeout 또는 기본 10초 제한을 적용합니다.
이 기본값은 이후 요청을 제한하지 않으며, connect_timeout은 TCP 연결만 제한합니다.
실패한 핸드셰이크는 전송을 중단시킵니다.
2단계: 데이터베이스 열기¶
sequenceDiagram
participant Client
participant CAS
Client->>CAS: OpenDatabase raw 628B (database 32B, user 32B, password 32B, extended 512B, reserved 20B)
CAS-->>Client: Framed response (CAS Info 4B, Response Code 4B, Broker Info 8B, Session ID 4B)
- 데이터베이스, 사용자, 비밀번호는 고정 길이 null 패딩 문자열
- Broker Info(8바이트) 포함 내용:
- 바이트 0:
db_type - 바이트 2:
statement_pooling - 바이트 4:
protocol_version(하위 6비트) - 세션 ID: 이후 CAS 정보 추적에 사용
3단계: 준비 완료¶
OpenDatabase 성공 후 연결이 SQL 연산에 사용할 수 있게 됩니다.
요청/응답 수명 주기¶
연결 이후의 모든 연산은 이 패턴을 따릅니다:
sequenceDiagram
participant Client
participant Server
Client->>Client: Build payload [function_code (1B)] [arguments...]
Client->>Server: Send [data_length (4B)] [cas_info (4B)] [payload]
Server-->>Client: Response [data_length (4B)] [cas_info (4B)] [response_code (4B)] [result_data...]
alt response_code >= 0
Client->>Client: 성공 경로 (코드가 추가 정보를 가질 수 있음)
else response_code < 0
Client->>Client: 에러 경로 (error_code + error_message)
end
명시적 동기 pycubrid.compat.native prepared 커서를 위해 동기 전송 계층은 내부 물리 세션 세대 기대값을 받습니다(#478).
이 opt-in 값을 전달한 호출에만 보호가 적용됩니다. #439의 소유자는 각
FC2/FC3/FC6/FC8 요청에 이 값을 전달합니다. 전송 계층은 connect·ping 복구·close와 요청/응답 전체를
보호하는 동일한 재진입 잠금 안에서 직렬화 전·송신 직전, 그리고 응답 전후에 세대와 소켓 정체성을
확인합니다. 한 요청은 포착한 소켓으로만 송수신하므로 재진입으로 연결이 바뀌어도
이전 FC3/FC6 바이트를 새 세션에 보내거나 새 세션의 응답을 성공으로 읽지 않습니다.
명확한 송신 전 로컬 검증 실패는
세션을 유지하고, 중단되었거나 부분적인 prepared 송신과 불확실한 응답은 재실행이나 새 세션 FC6 없이
세션을 폐기합니다. 완전히 받은 브로커 SQL 오류는 기존 오류코드 기반 DB-API
매핑과 사용 가능한 전송을 유지하며, 명시적 호환 커서는 브로커가 제어하는 오류 문구를
가리되 예외 클래스·code·errno·SQLSTATE는 보존합니다.
이 보호는 일반 FC41 호출을 바꾸거나 threadsafety를 1보다 높이지 않습니다.
긴 동기 요청은 같은 연결의 ping을 지연시킬 수 있고, 일반 커서·스키마의 여러 단계
수명주기가 이 잠금만으로 전면적인 스레드 안전성을 얻는 것도 아닙니다. 임의의 Python
신호 처리기 안에서 DB-API 메서드를 재진입 호출하는 동작도 지원 계약이 아닙니다.
함수 코드¶
CASFunctionCode에 정의된 41개 CAS 함수 코드 전체:
| 코드 | 이름 | 설명 |
|---|---|---|
| 1 | END_TRAN |
트랜잭션 커밋 또는 롤백 |
| 2 | PREPARE |
SQL 문 준비 |
| 3 | EXECUTE |
준비된 문장 실행 |
| 4 | GET_DB_PARAMETER |
데이터베이스 파라미터 값 조회 |
| 5 | SET_DB_PARAMETER |
데이터베이스 파라미터 값 설정 |
| 6 | CLOSE_REQ_HANDLE |
쿼리/요청 핸들 닫기 |
| 7 | CURSOR |
커서 위치 이동 |
| 8 | FETCH |
결과 행 가져오기 |
| 9 | SCHEMA_INFO |
스키마 정보 조회 |
| 10 | OID_GET |
OID로 객체 조회 |
| 11 | OID_PUT |
OID로 객체 갱신 |
| 15 | GET_DB_VERSION |
데이터베이스 엔진 버전 조회 |
| 16 | GET_CLASS_NUM_OBJS |
클래스 객체 수 조회 |
| 17 | OID_CMD |
OID 연산 (drop, lock 등) |
| 18 | COLLECTION |
컬렉션 연산 |
| 19 | NEXT_RESULT |
다음 결과 집합 조회 |
| 20 | EXECUTE_BATCH |
여러 SQL 문 배치 실행 |
| 21 | EXECUTE_ARRAY |
배열 바인딩으로 실행 |
| 22 | CURSOR_UPDATE |
커서를 통한 갱신 |
| 23 | GET_ATTR_TYPE_STR |
속성 타입 문자열 조회 |
| 24 | GET_QUERY_INFO |
쿼리 플랜 정보 조회 |
| 26 | SAVEPOINT |
세이브포인트 설정 또는 롤백 |
| 27 | PARAMETER_INFO |
파라미터 메타데이터 조회 |
| 28–30 | XA_* |
XA 분산 트랜잭션 연산 |
| 31 | CON_CLOSE |
CAS 연결 종료 |
| 32 | CHECK_CAS |
CAS 프로세스 핑 |
| 33 | MAKE_OUT_RS |
출력 결과 집합 생성 |
| 34 | GET_GENERATED_KEYS |
자동 생성 키 조회 |
| 35 | LOB_NEW |
새 LOB 핸들 생성 |
| 36 | LOB_WRITE |
LOB에 데이터 쓰기 |
| 37 | LOB_READ |
LOB에서 데이터 읽기 |
| 38 | END_SESSION |
CAS 세션 종료 |
| 39 | GET_ROW_COUNT |
영향받은 행 수 조회 |
| 40 | GET_LAST_INSERT_ID |
마지막 auto-increment ID 조회 |
| 41 | PREPARE_AND_EXECUTE |
준비 + 실행 결합 |
패킷 클래스¶
pycubrid는 pycubrid.protocol에 20개 패킷 클래스를 구현합니다. 각 클래스는 다음을 제공합니다:
write()— 요청 직렬화 (일부는cas_info파라미터를 받음)parse(data)— 응답 역직렬화
ClientInfoExchangePacket¶
핸드셰이크 패킷 — 표준 프레이밍 없음.
packet = ClientInfoExchangePacket()
raw = packet.write() # 10바이트, cas_info 불필요
packet.parse(response_4bytes) # new_connection_port 파싱
| 속성 | 타입 | 설명 |
|---|---|---|
new_connection_port |
int |
CAS 프로세스 포트 (0 = 현재 재사용) |
OpenDatabasePacket¶
데이터베이스 세션 열기 — 628 raw 바이트 전송, 프레임된 응답 수신.
packet = OpenDatabasePacket(database="testdb", user="dba", password="")
raw = packet.write() # 628바이트, cas_info 없음
packet.parse(response_data) # CAS 정보 접두사가 있는 프레임된 응답
| 속성 | 타입 | 설명 |
|---|---|---|
cas_info |
bytes |
CAS 세션 정보 (4바이트) |
response_code |
int |
성공 시 0, 오류 시 음수 |
broker_info |
dict |
{db_type, protocol_version, statement_pooling} |
session_id |
int |
서버 세션 식별자 |
database, user, password는 연결 charset(기본 UTF-8)으로 인코딩한 뒤 32바이트 필드에
맞게 문자 경계에서 잘라, 멀티바이트 문자가 쪼개지지 않습니다(#86).
연결 문자셋¶
CAS 프로토콜은 클라이언트 문자셋을 전달하지 않고 브로커는 변환하지 않습니다. 서버는
요청 텍스트를 데이터베이스 문자셋으로 해석하고 각 값을 해당 컬럼의 문자셋으로
반환합니다. 따라서 charset(기본값 "utf-8", #86)은 클라이언트 측 코덱입니다.
연결이 만드는 모든 패킷이 이를 가지며(write() 전에 설정되는 packet.encoding),
PacketWriter / PacketReader는 SQL 텍스트, FC9 인자, 자격 증명, 문자 값, 컬렉션 요소,
컬럼 메타데이터 이름과 기본값, 오류 텍스트와 테이블 이름을 담은 LOB 파일
로케이터(errors="replace")에 이를 사용합니다. 가져온 JSON 값은 항상 UTF-8입니다.
NUMERIC 텍스트, 타임존 이름, 엔진 버전 문자열은 UTF-8을 유지하고 LOB 내용은 원시
바이트입니다. euc_kr에서 KS X 1001 밖의 한글(Python이 A4 D4로 시작하는 8바이트 조합
시퀀스로 인코딩)은 인코딩할 수 없는 것으로 취급합니다. 인코딩 실패는 요청을
보내기 전 write() 안에서 DataError를 발생시키고, 엄격 디코딩 실패는 응답 전체를
읽은 뒤 DataError를 발생시키므로 세션은 계속 사용할 수 있습니다.
PrepareAndExecutePacket¶
준비 + 실행 결합 (FC=41) — SQL 연산의 주 패킷.
| 속성 | 타입 | 설명 |
|---|---|---|
query_handle |
int |
서버 할당 쿼리 핸들 |
statement_type |
int |
CUBRIDStatementType 코드 |
bind_count |
int |
바인드 파라미터 수 |
column_count |
int |
결과 컬럼 수 |
columns |
list[ColumnMetaData] |
컬럼 메타데이터 |
total_tuple_count |
int |
결과 집합의 총 행 수 |
result_count |
int |
결과 정보 항목 수 |
result_infos |
list[ResultInfo] |
문장별 결과 정보 |
tuple_count |
int |
초기 fetch의 행 수 |
rows |
list[tuple[Any, ...]] |
가져온 행 데이터 |
PreparePacket¶
문장 준비 (FC=2) — 별도 준비 단계.
내부 패킷은 prepare_flag=NORMAL(기존 기본값) 또는 HOLDABLE=0x08과 실제 autocommit
값을 받습니다. SQL에 NUL이나 연결 문자셋으로 인코딩할 수 없는 문자가 있으면 FC2를 보내기 전에
거부합니다. 이는 #439 설계를 위한 내부 와이어
기반이며, 공개 prepared 커서가 아닙니다.
executemany(..., prepared=True)(#839)는 carries_deferred_closes를 설정합니다. 그러면
FC2가 autocommit 바이트 뒤에 대기 중인 지연 닫기 핸들 id를 붙이며, CAS
fn_prepare_internal은 PREPARE_AND_EXECUTE처럼 준비 전에 이를 해제합니다(#488).
| 속성 | 타입 | 설명 |
|---|---|---|
query_handle |
int |
서버 할당 쿼리 핸들 |
statement_type |
int |
문장 타입 코드 |
bind_count |
int |
바인드 파라미터 수 |
column_count |
int |
결과 컬럼 수 |
columns |
list[ColumnMetaData] |
컬럼 메타데이터 |
ExecutePacket¶
준비된 문장 실행 (FC=3).
고정 인자 10개 뒤에 검증된 스칼라 바인딩마다 타입 바이트와 값 바이트를 길이 접두 인자
2개로 보냅니다. 지원하는 내부 범위는 부호 있는 INT32(8, 빅엔디언 4바이트), 연결 문자셋의 CHAR(1,
바이트와 끝 NUL), SQL NULL(0, 0바이트)입니다. 빈 CHAR는 NUL 1바이트이며 NULL이
아닙니다. #840부터 BIGINT(21, 빅엔디언 8바이트), DOUBLE(12, 8바이트, 유한값만),
NUMERIC(7, ASCII 10진 텍스트와 NUL), 빅엔디언 int16 필드 7개(14바이트, 브로커는
TIME을 오프셋 6에서 읽음)인 DATE(13), TIME(14), DATETIME(22), 그 14바이트 뒤에
NUL 없는 시간대 텍스트를 붙인 DATETIMETZ(31), VARBIT(6, 1바이트 이상; 길이 0이면
NULL로 읽힘)도 포함합니다.
Prepared 호스트 변수 매핑을 참고하세요. 선택적 bind_count는 전달된 바인딩 수와 일치해야 하며 forward-only
바이트는 실제 autocommit을 따라 auto 모드일 때 1, 수동 모드일 때 0입니다. FC41 폴백이나 SQL 리터럴
변환은 하지 않습니다.
타입 컬렉션 바인딩(#482)도 같은 인자 쌍을
씁니다. 타입 인자는 컬렉션 종류인 SET(16), MULTISET(17), SEQUENCE(18)입니다.
값 인자는 원소 타입 바이트 하나(INT 8 또는 공식 드라이버와 같은 STRING 2)
뒤에 원소마다 int32 length + payload가 이어지며, 요청에는 원소 개수가 없습니다.
INT 원소는 빅엔디언 4바이트, 문자열 원소는 연결 문자셋 바이트와 NUL, NULL 원소는
길이 0입니다. 빈 컬렉션은 원소 타입 바이트만 보냅니다. 값 전체 SQL NULL은 컬렉션이
아니라 스칼라 NULL 쌍입니다. 원소 길이가 인자를 넘으면 브로커는 조용히 파싱을 멈추고
일부만 담긴 컬렉션을 저장하므로(cas_execute.c), 바이트를 만들기 전에 원소를
검증합니다. 입력은 중첩 없는 tuple이어야 하고, INT 원소는 int(bool 제외) 또는
정규 10진 문자열이며 한 컬렉션에 둘을 섞을 수 없습니다. 문자열 원소는 NUL 없는
str입니다. 혼합, 중첩, bool, float, bytes 원소는 거부합니다.
CUBRID 10.2와 11.4의 브로커는 MULTISET 종류를 오류 -454로 거부하며(멀티셋을
db_make_set()으로 감쌈), SET 값을 MULTISET 컬럼에 저장하면 중복이 사라지고
SEQUENCE 값은 중복을 유지합니다.
공개 pycubrid.compat.native의 set.imports()(#440)는 공식 드라이버와 같은
방식으로 이 쌍을 만듭니다. 요청한 원소 타입과 관계없이 모든 원소는 STRING(2)
원소이고(INT 원소는 10진 텍스트), 기본 종류는 SET(16)이므로 바이트가 공식 요청과
같습니다. kind=MULTISET은 17이 아니라 SEQUENCE(18)로 보냅니다.
LOB 핸들 바인딩(#441, 공개 API는 pycubrid.compat.native의 fetch_lob()/bind_lob()뿐)도
같은 인자 쌍을 씁니다. 타입
인자는 BLOB(23) 또는 CLOB(24)이고 값 인자는 패킹된 핸들
[int32 db_type][int64 size][int32 locator length][locator NUL]입니다. 공식
드라이버의 lob 타입이 컬럼과 맞을 때 bind_lob()이 보내는 바이트와 같습니다. 공식
드라이버는 타입 바이트를 결과의 1번 컬럼에서 정하지만, 브로커는 그 바이트 대신
핸들의 db_type을 씁니다. db_type은 타입 인자와
맞아야 하고(BLOB 33, CLOB 34), 로케이터 길이는 핸들의 나머지 부분과 정확히
일치해야 합니다. 브로커는 핸들의 크기 필드로 저장 값을 만들기 때문에
(cas_execute.c의 caslob_to_dblob) 크기 필드가 낡은 핸들은 잘못된 길이로
저장됩니다. CCI는 LOB_WRITE마다 이 필드를 쓴 위치의 끝까지 올리며, 이제
Lob.write()도 그렇게 합니다. 바인딩은 만들어진 물리 세션에서만 보내며, 그 세션은
연결과 그 연결의 세션 세대로 식별합니다(세대 번호만으로는 연결 간에 겹침).
CUBRID 10.2와 11.4에서 조회한 핸들은 새 행에 복사되므로 다시 바인딩할 수 있지만,
LOB_NEW로 만든 핸들은 임시 파일을 가리키며 첫 autocommit 문장이 실패하더라도 그
파일을 가져갑니다.
프로토콜 버전이 1보다 크고 응답의 include_column_info=1이면 전체 FC2 메타데이터
본문이 shard ID와 인라인 FETCH 앞에 옵니다. 파서는 문장·바인드·컬럼 정보를
갱신하고 잘린 메타데이터를 거부합니다. 결과 레코드가 음수 오류를 나타내면 성공으로
처리하지 않고, 기존 CAS 코드→DB-API 예외 클래스 매핑을 적용합니다. 브로커 오류
문구는 그대로 노출하지 않습니다.
이 패킷 기본 요소는 #439가 물리 세션 소유권, 커서 수명주기와 전체 라이브 지원 게이트를
제공할 때까지 내부용으로 남습니다.
| 속성 | 타입 | 설명 |
|---|---|---|
total_tuple_count |
int |
총 결과 행 수 |
result_count |
int |
결과 정보 수 |
result_infos |
list[ResultInfo] |
문장별 결과 |
tuple_count |
int |
인라인 fetch 행 수 |
rows |
list[tuple[Any, ...]] |
인라인으로 가져온 행 |
FetchPacket¶
결과 행 가져오기 (FC=8) — 페이지 단위 행 조회에 사용.
| 속성 | 타입 | 설명 |
|---|---|---|
tuple_count |
int |
가져온 행 수 |
rows |
list[tuple[Any, ...]] |
행 데이터 |
CommitPacket¶
트랜잭션 커밋 (CCITransactionType.COMMIT과 함께 FC=1).
결과 속성 없음 — 오류 시 예외 발생.
RollbackPacket¶
트랜잭션 롤백 (CCITransactionType.ROLLBACK과 함께 FC=1).
결과 속성 없음 — 오류 시 예외 발생.
CloseDatabasePacket¶
CAS 세션 종료 (FC=31).
CloseQueryPacket¶
쿼리 핸들 해제 (FC=6).
GetEngineVersionPacket¶
서버 버전 조회 (FC=15).
| 속성 | 타입 | 설명 |
|---|---|---|
engine_version |
str |
버전 문자열 (예: "11.2.0.0378") |
GetSchemaPacket¶
스키마 내성 (FC=9).
| 속성 | 타입 | 설명 |
|---|---|---|
query_handle |
int |
스키마 행을 가져올 핸들 |
tuple_count |
int |
스키마 항목 수 |
columns |
list |
네 필드만 있는 축약 컬럼 메타데이터 |
소유권이 있는 스키마 요청 (#455, #456): 확인된 FC9 요청은
길이 접두가 있는 인자를 스키마 타입(int), 첫 이름/패턴(string 또는 NULL),
두 번째 이름/패턴(string 또는 NULL), 플래그(byte), 샤드 ID(int, 프로토콜
V5 이상) 순서로 보냅니다. NULL은 길이 0인 인자이며, 빈 문자열은 NUL 종료자를
포함하므로 서로 다른 인자입니다. 문자열은 연결 문자셋(기본 UTF-8)을 사용합니다.
응답 핸들과 튜플 수 다음에는 컬럼 수와 축약 컬럼 정보가 옵니다. 각 컬럼은
타입(1 또는 2바이트), scale(int16), precision(int32), 이름 길이(int32),
이름만 포함합니다. 일반 SELECT 메타데이터와 달리 별도 속성/테이블 이름,
NULL 허용 여부, 기본값, 제약 플래그는 없습니다. 따라서 비공개 디코더는
타입·scale·precision·이름만 제공하고, 없는 필드를 만들어 내지 않으며 기존
컬렉션 종류 정규화를 유지합니다.
GetSchemaPacket.write/parse는 이제 이 레이아웃을 사용합니다. 연결 getter는
원래 패킷의 식별자와 불변 핸들·개수·컬럼을 등록합니다. fetch_schema_info(packet)은
FC8로 모든 행을 읽고 0행도 FC6으로 닫으며, close_schema_info(packet)은 명시적
폐기입니다. 두 작업은 원래 CAS 세션에서만 실행하고 자동 재접속·재실행하지 않습니다.
기존 핸들 전용 FC6은 서버 기본값이 false인 선택적 auto-commit 인자를 생략합니다.
commit/rollback은 END_TRAN 전에 활성 핸들을 닫고 물리 연결 폐기는 소유권을 종료합니다.
비동기 등록·fetch/close는 연결 락 안에서 원자적으로 처리하며 I/O 취소 시 읽지 않은
응답 위로 FC6을 보내지 않고 세션을 폐기합니다. #457 실서버 행렬은 10.2/11.4의
CLASS/VCLASS/ATTRIBUTE/CONSTRAINT/PRIMARY_KEY/IMPORTED_KEYS/EXPORTED_KEYS를
검증하며 모든 스키마 코드나 네이티브 동등성을 인증하지는 않습니다.
참조 소스: CAS FC9 인자,
CCI 축약 컬럼,
JDBC 스키마 요청.
BatchExecutePacket¶
배치 실행 (FC=20) — 하나의 요청에 여러 SQL 문.
응답(CAS ux_execute_batch): int 결과 코드, int 실행된 문장 수, 그 뒤 문장마다 문장 타입
바이트와 int count >= 0 및 8바이트 OID, 또는 음수 int(V3 이상은 DBMS_ERROR_INDICATOR -2, 그 전은
오류 코드 자체), 오류 코드(V3 이상), 길이 접두 메시지. V5 이상은 마지막에 shard id가 옵니다. 실패한 문장이 뒤의 문장을
멈추지 않습니다. ux_execute_array처럼 CAS는 ER_INTERRUPTED(-4) 문장을 보고하고 실행된
문장 수를 고쳐 쓴 뒤에만 일찍 멈춥니다. 실행된 문장 수가 음수이거나 보낸 문장 수보다 크거나,
중단된 문장으로 끝나지 않는 짧은 응답은 잘못된 응답입니다.
| 속성 | 타입 | 설명 |
|---|---|---|
results |
list[tuple[int, int]] |
성공한 문장별 (stmt_type, result_count) |
errors |
list[dict] |
실패한 문장의 {code, message, index} |
row_results |
tuple[tuple[int, int, str], ...] |
실행된 문장마다 순서대로 (count, 0, "") 또는 (-1, code, message) (#841) |
ExecuteArrayPacket¶
배열 실행 (FC=21, #839) — 준비된 문 하나를 바인딩 값의 행마다 한 번씩 실행합니다.
executemany(..., prepared=True)가 같은 세션의 PREPARE(FC=2) 뒤에 보냅니다.
요청(CAS fn_execute_array): int 쿼리 핸들, int 쿼리 타임아웃(0, 프로토콜 V4 이상만),
byte autocommit, 그 뒤 행 순서대로 행마다 표시마다 (타입 바이트, 값) 인자 쌍 하나씩.
이 쌍은 ExecutePacket이 바인딩 하나에 쓰는 것과 같습니다. 모든 행은 서버의 바인딩
개수와 정확히 같아야 합니다. CAS는 남는 값을 조용히 무시합니다.
응답(CAS ux_execute_array): int 결과 코드, int 실행된 행 수, 그 뒤 행마다
int count >= 0과 8바이트 OID, 또는 음수 int(V3 이상은 DBMS_ERROR_INDICATOR -2, 그
전은 오류 코드 자체), 오류 코드(V3 이상), 길이 접두 메시지. V5 이상은 마지막에 shard id가
옵니다. CAS는 ER_INTERRUPTED(-4) 행을 보고한 뒤에만 일찍 멈추며, 그 밖의 짧은 응답은
잘못된 응답입니다.
| 속성 | 타입 | 설명 |
|---|---|---|
row_results |
tuple[tuple[int, int, str], ...] |
실행된 행마다 행 순서대로 (count, 0, "") 또는 (-1, code, message) |
errors |
list[dict] |
실패한 행의 {code, message, index} |
autocommit이 켜져 있으면 CAS는 성공한 행마다 커밋하고 실패한 행마다 중단합니다. 꺼져 있으면 교착 상태나 서버 종료가 요청 전체를 실패시킵니다.
LOBNewPacket¶
LOB 핸들 생성 (FC=35).
| 속성 | 타입 | 설명 |
|---|---|---|
lob_handle |
bytes |
서버 생성 LOB 핸들 |
LOBWritePacket¶
LOB 데이터 쓰기 (FC=36).
| 속성 | 타입 | 설명 |
|---|---|---|
bytes_written |
int |
서버가 쓴 바이트 수 |
요청은 패킹된 핸들, int64 오프셋, 데이터로 구성됩니다. 서버는 덧붙이기만 하므로
CUBRID 10.2와 11.4에서 현재 크기가 아닌 오프셋(값 안쪽이든 끝 너머든)의 쓰기는
-1016으로 실패합니다. 쓰기가 성공하면 Lob.write()는
핸들의 크기 필드를 offset + bytes_written까지 올리며(낮추지는 않음), CCI와
같습니다.
LOBReadPacket¶
LOB 데이터 읽기 (FC=37).
| 속성 | 타입 | 설명 |
|---|---|---|
bytes_read |
int |
실제 읽은 바이트 수 |
lob_data |
bytes |
읽은 데이터 |
GetLastInsertIdPacket¶
마지막 insert ID 조회 (FC=40).
| 속성 | 타입 | 설명 |
|---|---|---|
last_insert_id |
str |
문자열로 표현된 마지막 auto-increment 값 |
GetDbParameterPacket / SetDbParameterPacket¶
데이터베이스 파라미터 조회/설정 (FC=4 / FC=5).
| 속성 | 타입 | 설명 |
|---|---|---|
value |
int |
파라미터 값 (조회 결과 / 설정 입력) |
사용 가능한 파라미터(CCIDbParam):
| 코드 | 이름 | 설명 |
|---|---|---|
| 1 | ISOLATION_LEVEL |
트랜잭션 격리 수준 |
| 2 | LOCK_TIMEOUT |
잠금 대기 타임아웃 |
| 3 | MAX_STRING_LENGTH |
최대 문자열 길이 |
| 4 | AUTO_COMMIT |
자동 커밋 모드 (0/1) |
PacketWriter¶
pycubrid.packet.PacketWriter — CAS 와이어 형식으로 데이터 직렬화.
공개 메서드¶
| 메서드 | 설명 |
|---|---|
add_byte(value) |
길이 접두 바이트 쓰기 (1B 길이 + 1B 값) |
add_short(value) |
길이 접두 short 쓰기 (4B 길이 + 2B 값) |
add_int(value) |
길이 접두 int 쓰기 (4B 길이 + 4B 값) |
add_long(value) |
길이 접두 long 쓰기 (4B 길이 + 8B 값) |
add_float(value) |
길이 접두 float 쓰기 (4B 길이 + 4B 값) |
add_double(value) |
길이 접두 double 쓰기 (4B 길이 + 8B 값) |
add_bytes(value) |
길이 접두 raw 바이트 쓰기 |
add_null() |
null 마커 쓰기 (길이 0) |
add_date(y, m, d) |
길이 접두 날짜 쓰기 |
add_time(h, m, s) |
길이 접두 시간 쓰기 |
add_timestamp(y,m,d,h,m,s) |
길이 접두 타임스탬프 쓰기 |
add_datetime(y,m,d,h,m,s,ms) |
길이 접두 datetime 쓰기 |
add_cache_time() |
캐시 시간 쓰기 (int 0 두 개) |
to_bytes() |
쓴 모든 바이트 반환 |
내부 메서드¶
| 메서드 | 설명 |
|---|---|
_write_byte(value) |
raw 바이트, 길이 접두 없음 |
_write_short(value) |
raw short (2B) |
_write_int(value) |
raw int (4B) |
_write_long(value) |
raw long (8B) |
_write_float(value) |
raw float (4B) |
_write_double(value) |
raw double (8B) |
_write_bytes(value) |
raw 바이트, 접두 없음 |
_write_filler(count, value) |
N바이트를 값으로 채우기 |
_write_null_terminated_string(value) |
연결 문자셋(기본 UTF-8)의 길이 접두 문자열 + null 종단 |
_write_fixed_length_string(value, length) |
고정 폭 null 패딩 문자열, 문자 경계에서 자름 |
PacketReader¶
pycubrid.packet.PacketReader — CAS 와이어 데이터 역직렬화.
원시 파서¶
| 메서드 | 반환 | 소비 바이트 |
|---|---|---|
_parse_byte() |
int |
1 |
_parse_short() |
int |
2 |
_parse_int() |
int |
4 |
_parse_long() |
int |
8 |
_parse_float() |
float |
4 |
_parse_double() |
float |
8 |
_parse_bytes(count) |
bytes |
count |
_parse_null_terminated_string(length) |
str |
length |
모든 읽기는 응답 안에서만 이루어집니다(#383). 길이가 앞에 오는 읽기(바이트,
텍스트, NUMERIC, JSON, 디코딩하지 않은 컬렉션 또는 LOB 핸들,
_skip_bytes())는 이동하기 전에 0 <= length <= bytes_remaining()를 확인하고,
아니면 ValueError를 발생시킵니다. 고정 폭 읽기가 끝을 넘으면 struct.error
또는 IndexError가 발생합니다. 실패한 읽기는 오프셋을 바꾸지 않습니다. 텍스트
리더는 길이가 0 이하이면 이동하지 않고 ""를 반환합니다. 디코딩한 컬렉션의
원소는 선언된 크기를 정확히 채워야 하며, LOB_READ 바이트 수는 응답 안에
들어가야 합니다(요청한 길이보다 작은 값은 정상적인 짧은 읽기). 조회한
BLOB/CLOB 셀은 CAS가 쓰는 패킹된 핸들과 정확히 같아야 합니다(cas_execute.c
add_res_data_lob_handle, 10.2~11.4, #933). 16바이트 헤더
[int32 db_type][int64 size][int32 locator size] 뒤에 셀의 나머지를 채우는
로케이터가 옵니다. 내장 db_type은 셀 타입의 서버 DB_TYPE, 즉 BLOB 33 또는
CLOB 34여야 합니다(CCI 타입 23/24가 아님). 헤더가 짧거나, 로케이터 크기가
음수이거나, 로케이터가 셀의 나머지보다 길거나 짧거나, 내장 타입이 다르면
ValueError가 발생합니다. 앞선 셀 때문에 DataError를 발생시키기 전에 행 파서가
응답을 다시 훑을 때도 같습니다. 선언된 셀 뒤의 바이트는 다음 필드에 속합니다. 크기
-1과 빈 로케이터(외부 파일이 없는 값)는 계속 유효하며, 디코더는 로케이터 끝의
NUL을 확인하지 않습니다. FETCH 또는
실행 응답에 포함된 행의 각 셀 값은 크기 워드가 선언한 바이트를 정확히 사용해야
합니다. 고정 폭 값(INT, DATE, OBJECT 등)은 크기를 직접 읽지 않으므로 행
파서가 값을 읽기 전에 타입의 폭과 비교합니다(#523). DataError를 발생시키기 전에
응답을 다시 훑을 때도 같습니다. CAS는 SQL NULL을 크기 -1로 씁니다. 0 이하의
크기는 NULL이지만, 크기 0인 BIT/VARBIT 셀은 빈 비트 문자열 b""입니다(#994). 음수인 FETCH
튜플 수도 잘못된 형식이며, FC2, FC3, FC41 컬럼 메타데이터의 음수 컬럼 수와 음수
컬럼 이름, 실제 이름, 테이블 이름, 기본값 길이도 마찬가지입니다(#555). 길이 0은
빈 문자열입니다. FC41도 FC2처럼 음수인 바인드 수, total_tuple_count, 인라인
튜플 수와, 응답의 나머지 바이트에 들어갈 수 없는 컬럼 수(컬럼당 최소 31바이트)를
거부하며, FC3는 음수인 인라인 튜플 수를 거부합니다(#581). 연결 문자셋으로 디코딩할
수 없는 컬럼 메타데이터 텍스트는 남은 메타데이터를 선언된 길이대로 끝까지 훑은
뒤 오류를 보류합니다(#581). FC41과 컬럼을 갱신하는 FC3는 남은 꼬리 필드·카운트·
인라인 행을 검증한 뒤 최초 메타데이터 DataError를 다시 발생시킵니다.
뒤쪽 구조 손상이 있으면 해당 오류가 우선합니다(#591). 이 오류 경로에서는 사용자
JSON 변환기를 호출하지 않으며, 앞선 행 값을 표현할 수 없어도 이후 셀을 검증합니다.
정상 결과에서 컬렉션을 원시 바이트로 반환하는 경우에도 알려진 컬렉션 구조를 검증하고,
음수 원소 수는 거부합니다. 인라인 페치 헤더가 일부만 있으면 잘못된 형식이며,
선택적 헤더가 전혀 없는 경우는 계속 지원합니다. FC20(EXECUTE_BATCH)과
FC21(EXECUTE_ARRAY) 응답의 행별 오류 메시지 길이는
0 <= length <= bytes_remaining()이어야 하며, FC40(GET_LAST_INSERT_ID) 값의
타입 헤더는 행 셀처럼 값 크기에 포함되므로 값보다 긴 헤더는 잘못된
형식입니다(#897). 연결은 이
예외들을 OperationalError("malformed response from broker")로 바꾸고 연결을
닫습니다. DataError는 응답은 완전하지만 Python이 값을 표현할 수 없는 경우에만
사용합니다(#492, #512). 응답이 선언한 마지막 값 뒤에 남은 바이트는 검사하지
않습니다.
알려진 타입으로 디코딩하는 컬렉션은 각 길이 워드와 페이로드가 컬렉션 안에
들어가야 하며, 디코더가 선언된 바이트 수를 정확히 소비해야 합니다.
완전한 원소에서 DataError가 발생해도 후속 원소의 실제 디코더를 계속 실행하여,
뒤쪽의 잘못된 타입 표현이 보류한 오류보다 우선하도록 합니다(#595).
완전한 컬렉션은 최초 변환 오류와 원인을 유지합니다. NULL 전용 컬렉션, 원시
바이트 반환·디코딩 비활성화, 지원하지 않는 중첩 원소 형식의 기존 계약은 유지하며
새로운 재귀 디코딩 기능을 추가하지 않습니다.
내부 경계 재검증은 PacketReader.mark()와 seek(position)을 사용합니다.
응답 밖의 위치나 정수가 아닌 위치는 리더를 이동하지 않고 ValueError를
발생시킵니다. 0과 응답 끝은 유효한 위치입니다.
복합 파서¶
| 메서드 | 반환 | 설명 |
|---|---|---|
_parse_date() |
datetime.date |
short 3개 (연, 월, 일) |
_parse_time() |
datetime.time |
short 3개 (시, 분, 초) |
_parse_datetime() |
datetime.datetime |
short 7개 (y,m,d,h,m,s,ms) |
_parse_timestamp() |
datetime.datetime |
short 6개 (y,m,d,h,m,s) |
_parse_timestamptz() |
datetime.datetime |
short 6개 (y,m,d,h,m,s) + 타임존 문자열; 초 정밀도 (TIMESTAMPTZ/TIMESTAMPLTZ) |
_parse_datetimetz() |
datetime.datetime |
short 7개 (y,m,d,h,m,s,ms) + 타임존 문자열; 밀리초 정밀도 (DATETIMETZ/DATETIMELTZ) |
_parse_numeric(size) |
Decimal |
null 종단 문자열 → Decimal |
_parse_object() |
str |
OID 문자열 ("OID:@page\|slot\|volume") |
read_blob(size) |
dict |
BLOB 핸들 정보 |
read_clob(size) |
dict |
CLOB 핸들 정보 |
read_error(length) |
(int, str) |
오류 코드와 메시지 |
bytes_remaining() |
int |
버퍼의 읽지 않은 바이트 |
와이어 데이터 타입¶
컬럼 메타데이터는 첫 타입 바이트의 0x60 컬렉션 종류 비트를 보존합니다.
0x20은 SET, 0x40은 MULTISET, 0x60은 SEQUENCE입니다. 0x80이 설정되면
두 번째 바이트 전체가 스칼라/요소 타입(31을 넘는 코드 포함)을 나타내며,
그렇지 않으면 하위 5비트가 타입입니다. 컬렉션 행은 요소 타입이 아닌 컬렉션 종류로 디코딩합니다.
CALL / EVALUATE 결과의 셀과 메타데이터 타입이 NULL인 컬럼(SELECT NULL 등)의
셀은 값 앞에 자신의 타입 헤더를 가지며, 셀 크기는 이 헤더를 포함합니다. 프로토콜 7 이상
브로커(CUBRID 10.2+)는 컬럼 메타데이터와 같이 0x80 | collection bits | charset, 그다음
타입 바이트를 씁니다. 예를 들어 INT 42를 반환하는 함수의 CALL은
00000006 83 08 0000002a, CALL find_user('dba') ON CLASS db_user는
0000000a 83 13 <8-byte OID>입니다. 이전 브로커는 타입 바이트 하나를 씁니다.
드라이버는 두 형식을 모두 읽으며(#542), 셀보다 긴 헤더는 잘못된 응답입니다.
컬렉션 값은 요소 타입 1바이트, 4바이트 요소 개수, 그리고 요소마다 4바이트 길이와 페이로드로
구성됩니다. NULL 요소는 길이 -1이며 페이로드가 없습니다. 빈 BIT/VARBIT 요소(X'')는
길이 0이며 b""로 디코딩되고, 다른 타입의 길이 0 요소는 None으로 디코딩됩니다(#998). 모든 요소가 NULL이면 CUBRID
10.2/11.4는 요소 타입 0(NULL)을 보냅니다. {}는 00 00000000, {NULL, NULL}은
00 00000002 ffffffff ffffffff입니다.
컬럼 데이터는 4바이트 크기 접두 + raw 데이터로 전송됩니다. 타입이 데이터 바이트의 해석을 결정합니다:
| 타입 코드 | 이름 | 와이어 형식 |
|---|---|---|
| 0 | NULL |
size ≤ 0 → None |
| 1 | CHAR |
연결 문자셋(기본 UTF-8)의 null 종단 문자열 |
| 2 | STRING |
연결 문자셋(기본 UTF-8)의 null 종단 문자열 |
| 3 | NCHAR |
연결 문자셋(기본 UTF-8)의 null 종단 문자열 |
| 4 | VARNCHAR |
연결 문자셋(기본 UTF-8)의 null 종단 문자열 |
| 5 | BIT |
raw 바이트; 크기 0 → b"" (#994) |
| 6 | VARBIT |
raw 바이트; 크기 0 → b"" (#994) |
| 7 | NUMERIC |
null 종단 문자열 → Decimal |
| 8 | INT |
4바이트 big-endian 부호 있는 int |
| 9 | SHORT |
2바이트 big-endian 부호 있는 short |
| 10 | MONETARY |
8바이트 big-endian double |
| 11 | FLOAT |
4바이트 big-endian float |
| 12 | DOUBLE |
8바이트 big-endian double |
| 13 | DATE |
short 3개: 연, 월, 일 |
| 14 | TIME |
short 3개: 시, 분, 초 |
| 15 | TIMESTAMP |
short 6개: y, m, d, h, m, s |
| 16–18 | SET/MULTISET/SEQUENCE |
raw 바이트 |
| 19 | OBJECT |
4B 페이지 + 2B 슬롯 + 2B 볼륨 |
| 21 | BIGINT |
8바이트 big-endian 부호 있는 long |
| 22 | DATETIME |
short 7개: y, m, d, h, m, s, ms |
| 23 | BLOB |
패킹된 LOB 핸들 → dict |
| 24 | CLOB |
패킹된 LOB 핸들 → dict |
| 25 | ENUM |
연결 문자셋(기본 UTF-8)의 null 종단 문자열 |
컬럼 메타데이터¶
결과 집합의 각 컬럼은 와이어에서 파싱한 메타데이터를 가집니다:
@dataclass
class ColumnMetaData:
column_type: int # CUBRIDDataType 코드
scale: int # 십진 스케일 (해당 없으면 -1)
precision: int # 정밀도 (해당 없으면 -1)
name: str # 컬럼 별칭
real_name: str # 실제 컬럼 이름
table_name: str # 출처 테이블 이름
is_nullable: bool # 반대 의미의 wire 플래그에서 변환한 NULL 허용 여부
default_value: str # 기본값 표현식
is_auto_increment: bool # AUTO_INCREMENT 컬럼
is_unique_key: bool # 유니크 인덱스의 일부
is_primary_key: bool # 기본 키의 일부
is_reverse_index: bool # 리버스 인덱스 보유
is_reverse_unique: bool # 리버스 유니크 인덱스 보유
is_foreign_key: bool # 외래 키의 일부
is_shared: bool # 공유 속성
컬럼 메타데이터의 raw 바이트는 is_non_null입니다. 0은 NULL 허용, 0이 아닌 값은
NOT NULL이며 브로커는 이를 0/1로 정규화합니다. 메타데이터 파서는 PacketReader가
읽은 이 바이트에 raw == 0을 적용해 is_nullable과 공개
cursor.description의 null_ok 값을 만듭니다.
에러 처리¶
response_code < 0이면 응답에 오류가 담겨 있습니다:
graph LR
code["Error Code (4B int)"] --> message["Error Message (null-terminated string)"]
음수 외부 응답 인디케이터는 오류 도메인(CAS -1 또는 DBMS -2)을 나타내며,
그 뒤의 내부 오류 코드와는 다른 숫자입니다. _raise_error()는 먼저 내부
숫자 코드를 기존 오류 매핑에서 찾습니다. 매핑되지 않은 내부 코드가 정확히
-1일 때만 아래 텍스트 폴백을 쓰며, 외부 CAS 인디케이터 -1이 폴백을
선택하는 것은 아닙니다. 나머지 미매핑 코드는 메시지와 관계없이 DatabaseError를
냅니다. 받은 내부 숫자는 예외의 code와 errno에 그대로 남으며, 외부 도메인은
DatabaseError.error_source로 제공합니다: -1은 "cas", -2는 "server", 그 밖의 값은
None입니다(#903). 드라이버는 핸드셰이크에 BROKER_RENEWED_ERROR_CODE를 설정하지 않으므로
CAS 오류는 이전 번호 체계(CAS_ER_*에 9000을 더한 -1000부터 -1200)로 도착하며, 이
범위는 서버 코드와 겹칩니다. 둘을 구분하는 것은 인디케이터입니다.
내부 코드 -1 전용 텍스트 폴백:
| 오류 패턴 | 예외 |
|---|---|
unique, duplicate, foreign key, constraint violation |
IntegrityError |
syntax, unknown class, does not exist, not found |
ProgrammingError |
| 그 외 폴백 텍스트 | DatabaseError |
갱신 CAS 번호: 평가 결정 (#505)¶
평문과 STARTTLS 요청 모두 현재 핸드셰이크 함수 바이트(0 기준 인덱스 7)를
0으로 유지하고 공개 code/errno 원시 값을 보존합니다. 조사한 브로커는
갱신 코드 비트 0x80을 지원하지만, 활성화나 opt-in 추가에는 별도의 도메인
인식 소비자·이행 계약이 필요합니다. 보류가 기존 숫자 모호성을 해결하는 것은
아닙니다. 이전 CAS 통신 코드 -1003은 엔진의
ER_SM_CONSTRAINT_HAS_DIFFERENT_TYPE=-1003과 겹칩니다. 숫자만으로 도메인을
추정하거나 9000을 더하고 빼서 정규화하지 마세요. 고정된
브로커 직렬화와
CCI 변환은
숫자 범위만이 아닌 도메인·프로토콜·기능 조건을 사용합니다.
엔진 충돌은
소스로 확인했으며 실서버에서 재현한 것은 아닙니다. 함수 바이트의 별도
holdable 비트 0x40은 이 평가 범위 밖이며 활성화하지 않습니다.
2026-10-03에 불변 드라이버 984d3a2c9e30d613936d69195719a3439e1c08e1과
Python 3.12.13으로 소유한 10.2.18.9024, 11.0.16.0419, 11.2.9.0866,
11.4.6.1963 빌드에서 flag 0과 0x80을 비교했습니다. 지원하지 않는 파라미터
ID 255를 넣은 정상 프레임 FC4 요청은 외부 CAS 도메인 -1, 내부 -1011과
-10011을 반환했고 각각 DatabaseError.code와 errno에 그대로 남았습니다.
DBMS 구문 제어는 두 플래그 모두 외부 -2, 내부 -493과 ProgrammingError를
유지했습니다. 동일 세션 건강 확인은 재실행·재접속 없이 성공했습니다. 이는
해당 빌드의 그 생산자만 확인하며 모든 패치, 인증·내장·배치·TLS·리다이렉트·
재접속 경로, 실서버 숫자 충돌이나 소비자 이행을 입증하지 않습니다.
협상이나 API 동작은 바꾸지 않았습니다.
상수 참조¶
CAS 프로토콜 상수¶
class CASProtocol:
MAGIC_STRING = "CUBRK" # 핸드셰이크 매직 (평문)
MAGIC_STRING_SSL = "CUBRS" # 핸드셰이크 매직 (STARTTLS 요청)
CLIENT_JDBC = 3 # 클라이언트 타입 식별자
PROTO_INDICATOR = 0x40 # 프로토콜 인디케이터 비트
VERSION = 8 # 프로토콜 버전
CAS_VERSION = 0x48 # 결합된 버전 바이트
와이어 데이터 크기¶
class DataSize:
BYTE = 1 BOOL = 1
SHORT = 2 INT = 4
FLOAT = 4 LONG = 8
DOUBLE = 8 OBJECT = 8
OID = 8 BROKER_INFO = 8
DATE = 14 TIME = 14
DATETIME = 14 TIMESTAMP = 14
RESULTSET = 4 DATA_LENGTH = 4
CAS_INFO = 4
문장 타입¶
| 코드 | 이름 | 코드 | 이름 |
|---|---|---|---|
| 0 | ALTER_CLASS |
20 | INSERT |
| 4 | CREATE_CLASS |
21 | SELECT |
| 5 | CREATE_INDEX |
22 | UPDATE |
| 9 | DROP_CLASS |
23 | DELETE |
| 10 | DROP_INDEX |
24 | CALL |
| 16 | ROLLBACK_WORK |
126 | CALL_SP |
| 17 | GRANT |
127 | UNKNOWN |
트랜잭션 타입¶
| 코드 | 이름 |
|---|---|
| 1 | COMMIT |
| 2 | ROLLBACK |
격리 수준¶
| 코드 | 이름 |
|---|---|
| 0x01 | COMMIT_CLASS_UNCOMMIT_INSTANCE (레거시 pre-MVCC) |
| 0x02 | COMMIT_CLASS_COMMIT_INSTANCE |
| 0x03 | REP_CLASS_UNCOMMIT_INSTANCE (레거시 pre-MVCC) |
| 0x04 | REP_CLASS_COMMIT_INSTANCE (READ COMMITTED, 서버 기본값) |
| 0x05 | REP_CLASS_REP_INSTANCE |
| 0x06 | SERIALIZABLE |
CUBRID의 MVCC 엔진(10.0+)에서는
0x04/0x05/0x06만 받아들여집니다. 하위 코드0x01–0x03은 레거시 pre-MVCC 수준입니다. CUBRID 서버의 기본 격리 수준은 READ COMMITTED(0x04)입니다.