Skip to content

CAS Protocol Reference

Technical documentation for the CUBRID CAS (Common Application Server) wire protocol as implemented by pycubrid.

New to CAS? CAS Architecture and Protocol Concepts introduces the Broker, CAS process and CCI and maps DB-API calls to function codes before this reference.


Table of Contents


Overview

CUBRID uses a proprietary binary protocol called CAS (Common Application Server) for client–server communication. The protocol operates over TCP sockets with big-endian byte ordering.

Key characteristics:

  • Transport: TCP (default port 33000 for the broker)
  • Byte order: Big-endian (network byte order)
  • Framing: 4-byte data length + 4-byte CAS info + payload
  • Handshake: Magic string CUBRK (plaintext) or CUBRS (STARTTLS) + client type + protocol version
  • TLS (optional): STARTTLS-style upgrade after the handshake using start_tls() / wrap_socket() before OPEN_DATABASE
  • Session: Stateful — session ID maintained after OpenDatabase

Architecture

graph LR
    client["Client (pycubrid)"] -- TCP port 33000 --> broker[Broker]
    broker --> cas[CAS Process]
    cas --> db[CUBRID Database]
  1. Client connects to the Broker on port 33000
  2. Broker performs handshake and may redirect to a CAS process on a different port
  3. Client opens a database session on the CAS process
  4. All subsequent requests go through the CAS session

Packet Framing

All packets after the initial handshake use the following frame format:

graph LR
    data["Data Length (4B) big-endian int32"] --> casinfo["CAS Info (4B) session state"]
    casinfo --> payload["Payload (variable) function-specific"]
Field Size Description
Data Length 4 bytes Length of CAS Info + Payload (signed int32, big-endian)
CAS Info 4 bytes Session state bytes maintained by the server
Payload variable Function code + arguments (requests) or response data

The first CAS Info byte is transaction state: 0 means OUT_TRAN and 1 means IN_TRAN. OUT_TRAN after END_TRAN or an auto-committing request does not mean the socket or CAS session was released; send the next request on the same transport. The CAS may still close the socket after an OUT_TRAN reply (memory restart, cubrid broker reset, CHANGE CLIENT), so before the next request the driver sends CHECK_CAS (FC=32), like JDBC UClientSideConnection.checkReconnect. Only a failed probe replaces the session, once per request, before that request is first sent (#485). A failed CHECK_CAS may also justify explicit ping(reconnect=True) recovery, but an uncertain application request is never replayed automatically. Commit and rollback send CLOSE_REQ (FC=6) for open cursor query handles before END_TRAN.

Deferred close (#488): when a direct CUBRID CAS reports statement pooling in OPEN_DATABASE (broker_info[0] == 1 and broker_info[2] == 1), query handles survive END_TRAN, so in autocommit mode a handle released by cursor.close() or by re-executing a cursor, and in either mode the handle of a cursor collected without close(), is not closed with its own CLOSE_REQ. Its id is appended to the next FC41 request as extra prepare arguments after the auto-commit flag (the prepare argument count grows by one per id), and CAS frees those handles before preparing the statement. This is the wire mechanism of JDBC's deferred close, but the policy differs: JDBC defers only statements without a result set and closes SELECT/CALL/EVALUATE handles at once (CLOSE_USTATEMENT), while pycubrid defers result-set handles too. A native error in that statement still frees them. One statement carries at most 256 ids; an explicit release while 256 are queued sends CLOSE_REQ at once, and collected cursors are always queued and ride on later statements. FC20 batch requests do not carry queued IDs: a handle released by executemany() waits for a later FC41 or transaction boundary. Empty executemany() sends no batch SQL and does not flush the queue; when its previous handle can be deferred, it also sends no CLOSE_REQ. Existing immediate-close rules still apply when deferral is unavailable (including a full queue). commit() and rollback() close every id still queued for the session with CLOSE_REQ before END_TRAN, as they closed unreferenced cursors before. Queued ids belong to one physical session: they are dropped when it is retired or replaced and never sent to another. Without statement pooling CAS frees handles at every commit, so CLOSE_REQ is sent at once as before and a collected cursor's handle is left to the next commit. A shard proxy (broker_info[0] other than 1, CUBRID) ignores the extra arguments, so explicit releases still send CLOSE_REQ at once. Collected proxy cursor handles rely on proxy transaction/session cleanup. Explicit releases during session setup (the escape-mode probe and restored settings of a replacement session) are never deferred; a cursor collected on an eligible direct, pooling-enabled session may queue its handle during setup, since it cannot send anything. Each handle keeps the generation of the session that opened it, so a cursor collected after a reconnect cannot release a handle id on the new session.

Pooling-off ownership (#584): a direct CUBRID CAS with statement pooling explicitly disabled frees all ordinary cursor and schema handles at a transaction boundary, and can immediately reuse their IDs on the same physical session. The driver retires ownership when the actual reply is OUT_TRAN for END_TRAN, an autocommitting FC41/FC3 or version request, a failed autocommit FC2 which auto-rolls back, or the final ordinary autocommit FETCH. Successful FC2 prepare does not establish a boundary. This runs before parsing the reply and before an INSERT's identity RPC: that subsequent RPC can return IN_TRAN although the preceding commit freed the handles. The current FC41 result is not adopted when its reply already freed it, including a complete-reply DataError. Buffered rows remain readable; a required FETCH from an unfinished invalidated result raises InterfaceError, while a completed result retains normal EOF. Physical generation and session verification are unchanged. OUT_TRAN echoes from CHECK_CAS, CLOSE_REQ, parameters, schema requests/fetches and batch replies do not independently prove this boundary; manual FETCH and pooling-enabled/proxy sessions keep their existing behavior.

Automatic no_backslash_escapes detection is scoped to a physical session: new sessions are probed before parameter binding resumes, while a healthy same-session CHECK_CAS does not probe. Explicit mode remains pinned. If recovery probing fails, the replacement is retired and ping returns False; async SQL bound under a prior session generation is rejected before send.

Building a header:

from pycubrid.packet import build_protocol_header

header = build_protocol_header(data_length=42, cas_info=b"\x00\x00\x00\x00")
# Returns 8 bytes: 4-byte length + 4-byte cas_info

Exceptions

The handshake packet (ClientInfoExchangePacket) does NOT use this framing — it sends raw bytes without a length/cas_info header.

The open database packet (OpenDatabasePacket) sends raw bytes (628 bytes) without a header, but its response uses standard framing.


Connection Handshake

The connection flow has up to four phases (the TLS phase is optional):

Phase 1: Client Info Exchange

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)
  • Magic string: "CUBRK" (plaintext) or "CUBRS" (STARTTLS-requested) — 5 ASCII bytes
  • Client type: CLIENT_JDBC = 3 (pycubrid identifies as a JDBC-compatible client)
  • CAS version: PROTO_INDICATOR(0x40) | VERSION(8) = 0x48
  • Status int32: signed big-endian
    • > 0 — redirect port: close socket, reconnect to (host, status), do not repeat the handshake (mirrors the JDBC BrokerHandler.connectBroker behavior). A status above 65535 is not a TCP port and raises OperationalError in both drivers before any reconnect (#896)
    • < 0 — fail-fast: raise OperationalError(status)
    • == 0 — reuse the current socket (direct mode)

Phase 1.5: TLS Upgrade (optional)

If ssl was truthy on the connect call, the live transport is upgraded before any OPEN_DATABASE bytes are written:

  • Sync driver: ssl.SSLContext.wrap_socket(sock, server_hostname=host)
  • Async driver: loop.start_tls(transport, protocol, context, server_hostname=host, ssl_handshake_timeout=...)

The TLS handshake uses read_timeout when configured, otherwise a 10-second default. That default does not bound subsequent requests; connect_timeout still bounds only the TCP connect.

Failed handshakes abort the transport.

Phase 2: Open Database

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)
  • Database, user, and password are fixed-length null-padded strings
  • Broker Info (8 bytes) contains:
  • Byte 0: db_type
  • Byte 2: statement_pooling
  • Byte 4: protocol_version (lower 6 bits)
  • Session ID: Used for subsequent CAS info tracking

Phase 3: Ready

After successful OpenDatabase, the connection is ready for SQL operations.


Request/Response Lifecycle

Every operation after connection follows this pattern:

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: Success path (code may carry additional info)
    else response_code < 0
        Client->>Client: Error path (error_code + error_message)
    end

For the explicit sync pycubrid.compat.native prepared cursor, the synchronous transport accepts a private physical-session generation expectation (#478). Only calls that pass this opt-in value are fenced; the #439 owner stamps each FC2/FC3/FC6/FC8 request. The transport checks generation and socket identity before serialization, immediately before send and again around the response, under the same reentrant lock that covers connect, ping recovery, close and a whole request/response. A captured request socket is used for both send and receive, so a reentrant replacement cannot redirect old FC3/FC6 bytes or read the new session's reply as success. A definitely pre-byte local validation failure keeps the session; an interrupted/partial prepared send or uncertain reply retires it without replay or a new-session FC6. A fully received broker SQL error retains the existing code-based DB-API mapping and a usable transport; the explicit compatibility cursor redacts broker-controlled error text while preserving its class, code, errno and SQLSTATE. The fence does not change ordinary FC41 calls or raise threadsafety above 1; a long blocking request can delay ping on the same connection, and other ordinary multi-step cursor/schema lifecycles are not made generally thread-safe by this lock. Calling DB-API methods reentrantly from an arbitrary Python signal handler is not a supported safety contract.


Function Codes

All 41 CAS function codes, defined in CASFunctionCode:

Code Name Description
1 END_TRAN Commit or rollback a transaction
2 PREPARE Prepare a SQL statement
3 EXECUTE Execute a prepared statement
4 GET_DB_PARAMETER Get a database parameter value
5 SET_DB_PARAMETER Set a database parameter value
6 CLOSE_REQ_HANDLE Close a query/request handle
7 CURSOR Position the cursor
8 FETCH Fetch result rows
9 SCHEMA_INFO Get schema information
10 OID_GET Get object by OID
11 OID_PUT Update object by OID
15 GET_DB_VERSION Get the database engine version
16 GET_CLASS_NUM_OBJS Get class object count
17 OID_CMD OID operations (drop, lock, etc.)
18 COLLECTION Collection operations
19 NEXT_RESULT Get next result set
20 EXECUTE_BATCH Batch execute multiple SQL statements
21 EXECUTE_ARRAY Execute with array binding
22 CURSOR_UPDATE Update via cursor
23 GET_ATTR_TYPE_STR Get attribute type string
24 GET_QUERY_INFO Get query plan information
26 SAVEPOINT Set or rollback to savepoint
27 PARAMETER_INFO Get parameter metadata
28–30 XA_* XA distributed transaction operations
31 CON_CLOSE Close the CAS connection
32 CHECK_CAS Ping the CAS process
33 MAKE_OUT_RS Create output result set
34 GET_GENERATED_KEYS Get auto-generated keys
35 LOB_NEW Create a new LOB handle
36 LOB_WRITE Write data to a LOB
37 LOB_READ Read data from a LOB
38 END_SESSION End the CAS session
39 GET_ROW_COUNT Get affected row count
40 GET_LAST_INSERT_ID Get last auto-increment ID
41 PREPARE_AND_EXECUTE Combined prepare + execute

Packet Classes

pycubrid implements 20 packet classes in pycubrid.protocol. Each class provides:

  • write() — Serialize the request (some take cas_info parameter)
  • parse(data) — Deserialize the response

ClientInfoExchangePacket

Handshake packet — no standard framing.

packet = ClientInfoExchangePacket()
raw = packet.write()          # 10 bytes, no cas_info needed
packet.parse(response_4bytes) # Parses new_connection_port
Attribute Type Description
new_connection_port int Port for the CAS process (0 = reuse current)

OpenDatabasePacket

Open a database session — sends 628 raw bytes, receives framed response.

packet = OpenDatabasePacket(database="testdb", user="dba", password="")
raw = packet.write()        # 628 bytes, no cas_info
packet.parse(response_data) # Framed response with CAS info prefix
Attribute Type Description
cas_info bytes CAS session info (4 bytes)
response_code int 0 on success, negative on error
broker_info dict {db_type, protocol_version, statement_pooling}
session_id int Server session identifier

Database, user and password are encoded with the connection charset (default UTF-8) and cut to their 32-byte fields on a character boundary, so a multibyte character is never split (#86).

Connection charset

The CAS protocol carries no client charset and the broker converts nothing: the server interprets request text in the database charset and returns each value in its column's charset. charset (default "utf-8", #86) is therefore a client-side codec. Every packet built by a connection carries it (packet.encoding, set before write()), and PacketWriter / PacketReader use it for SQL text, FC9 arguments, credentials, character values, collection elements, column metadata names and defaults, and error text (with errors="replace", as are LOB file locators, which embed the table name). Fetched JSON values are always UTF-8. NUMERIC text, timezone names and the engine version string stay UTF-8, and LOB contents are raw bytes. With euc_kr, Hangul outside KS X 1001, which Python encodes as an 8-byte makeup sequence starting A4 D4, is treated as unencodable. An encode failure raises DataError inside write(), before the request is sent; a strict decode failure raises DataError after the whole reply was read, so the session stays usable.


PrepareAndExecutePacket

Combined prepare + execute (FC=41) — the primary packet for SQL operations.

Attribute Type Description
query_handle int Server-assigned query handle
statement_type int CUBRIDStatementType code
bind_count int Number of bind parameters
column_count int Number of result columns
columns list[ColumnMetaData] Column metadata
total_tuple_count int Total rows in result set
result_count int Number of result info entries
result_infos list[ResultInfo] Per-statement result info
tuple_count int Rows in the initial fetch
rows list[tuple[Any, ...]] Fetched row data

PreparePacket

Prepare a statement (FC=2) — separate prepare step.

The internal packet accepts prepare_flag=NORMAL (legacy default) or HOLDABLE=0x08, plus effective autocommit. SQL with an embedded NUL or text the connection charset cannot encode is rejected before FC2. This is internal wire groundwork for #439, not a public prepared cursor. executemany(..., prepared=True) (#839) sets carries_deferred_closes: its FC2 then appends queued deferred-close handle ids after the autocommit byte, which CAS fn_prepare_internal frees before preparing, as for PREPARE_AND_EXECUTE (#488).

Attribute Type Description
query_handle int Server-assigned query handle
statement_type int Statement type code
bind_count int Number of bind parameters
column_count int Number of result columns
columns list[ColumnMetaData] Column metadata

ExecutePacket

Execute a prepared statement (FC=3).

After ten fixed arguments, the packet writes two length-prefixed arguments per validated scalar binding: type byte and value bytes. The supported internal subset is signed INT32 (8, four big-endian bytes), CHAR in the connection charset (1, bytes plus NUL), and SQL NULL (0, zero bytes). Empty CHAR is a single NUL byte, not NULL. Since #840 it also covers BIGINT (21, eight big-endian bytes), DOUBLE (12, eight bytes, finite only), NUMERIC (7, ASCII decimal text plus NUL), DATE (13), TIME (14) and DATETIME (22) as seven big-endian int16 fields (14 bytes; the broker reads TIME at offset 6), DATETIMETZ (31, those 14 bytes plus the zone text without NUL) and VARBIT (6, at least one byte; a zero-length value reads as NULL). See Prepared host-variable mapping. The optional bind_count must match the number of bindings. The forward-only byte follows effective autocommit: 1 in auto mode, 0 in manual mode. No FC41 fallback or SQL literal rendering occurs.

The typed collection binding (#482) uses the same pair. The type argument is the collection kind: SET (16), MULTISET (17) or SEQUENCE (18). The value argument is one element-type byte, INT (8) or STRING (2, the byte the official driver sends), followed by one int32 length + payload per element. There is no element count in the request. An INT element is four big-endian bytes, a string element is the connection-charset bytes plus NUL, and a NULL element has length 0. An empty collection is the element-type byte alone. Whole SQL NULL is the scalar NULL pair, not a collection. The broker stops parsing silently and keeps the partial collection when an element length overruns the argument (cas_execute.c), so elements are validated before any bytes are built: input must be a flat tuple; INT elements are int (not bool) or canonical decimal strings, not both in one collection; string elements are str without NUL. Mixed, nested, bool, float and bytes elements are rejected. On CUBRID 10.2 and 11.4 the broker rejects the MULTISET kind with error -454 (it wraps the multiset with db_make_set()), and a SET value stored into a MULTISET column loses duplicates; a SEQUENCE value keeps them.

The public pycubrid.compat.native set.imports() (#440) builds this pair the way the official driver does: every element is a STRING (2) element, whatever element type is requested (INT elements are their decimal text), and the default kind is SET (16), so its bytes equal the official request. kind=MULTISET is sent as SEQUENCE (18), never as 17.

A LOB-handle binding (#441; public only through pycubrid.compat.native fetch_lob()/bind_lob()) uses the same pair. The type argument is BLOB (23) or CLOB (24) and the value argument is the packed handle [int32 db_type][int64 size][int32 locator length][locator NUL]. This is byte for byte what the official driver sends for bind_lob() when its lob type matches the column; the official driver takes the type byte from result column 1, and the broker ignores that byte in favor of the handle's own db_type. The db_type must match the type argument (BLOB 33, CLOB 34), and the locator length must cover the rest of the handle exactly. The broker builds the stored value from the handle's own size field (caslob_to_dblob in cas_execute.c), so a handle with a stale size stores the wrong length. CCI raises that field to the end of each LOB_WRITE, and Lob.write() now does the same. A binding is sent only on the physical session it was made for, identified by its connection and that connection's session generation (generation numbers alone repeat across connections). On CUBRID 10.2 and 11.4 a fetched handle is copied into the new row and can be bound again, while a handle from LOB_NEW names a temporary file that its first autocommit statement takes over, even when that statement fails.

For protocol version >1, an include_column_info=1 response carries the full FC2 prepare-info tail before the shard ID and inline FETCH. The parser updates statement/bind/column metadata and rejects truncated tails. A negative per-result record is surfaced through the existing code-to-DB-API exception mapping rather than treated as success; its broker text is not copied into the exception. These packet primitives remain internal until #439 supplies physical-session ownership, cursor lifecycle and full live support gates.

Attribute Type Description
total_tuple_count int Total result rows
result_count int Number of result infos
result_infos list[ResultInfo] Per-statement results
tuple_count int Inline fetch row count
rows list[tuple[Any, ...]] Inline fetched rows

FetchPacket

Fetch result rows (FC=8) — used for paginated row retrieval.

Attribute Type Description
tuple_count int Number of fetched rows
rows list[tuple[Any, ...]] Row data

CommitPacket

Commit transaction (FC=1 with CCITransactionType.COMMIT).

No result attributes — raises an exception on error.


RollbackPacket

Rollback transaction (FC=1 with CCITransactionType.ROLLBACK).

No result attributes — raises an exception on error.


CloseDatabasePacket

Close the CAS session (FC=31).


CloseQueryPacket

Release a query handle (FC=6).


GetEngineVersionPacket

Get server version (FC=15).

Attribute Type Description
engine_version str Version string (e.g., "11.2.0.0378")

GetSchemaPacket

Schema introspection (FC=9).

Attribute Type Description
query_handle int Handle for fetching schema rows
tuple_count int Number of schema entries
columns list Condensed four-field column metadata

Owned schema requests (#455, #456): the verified FC9 request carries length-prefixed arguments in this order: schema type (int), first name/pattern (string or NULL), second name/pattern (string or NULL), flags (byte), then shard ID (int, protocol V5 and newer). NULL is a zero-length argument; an empty string contains its NUL terminator and is a different argument. Strings use the connection charset (default UTF-8).

After the response handle and tuple count, the column count precedes condensed columns: type (one or two bytes), scale (int16), precision (int32), name length (int32) and name. Unlike ordinary SELECT metadata, this layout has no separate attribute/table names, nullability, default or constraint flags. The private decoder therefore exposes only type, scale, precision and name; it preserves the existing collection-kind normalization rather than inventing absent fields.

GetSchemaPacket.write/parse now uses this layout. Connection getters register the original packet identity and immutable handle/count/columns. Consume via fetch_schema_info(packet) (FC8 until all rows arrive, followed by FC6 even for zero rows) or abandon via close_schema_info(packet). Both remain on the original CAS session, without reconnect/replay. The unchanged handle-only FC6 omits the optional auto-commit argument, whose server default is false. Commit/rollback close active schema handles before END_TRAN; physical teardown retires ownership. Async ownership registration and fetch/close are atomic under the connection lock; in-flight cancellation discards the transport rather than sending FC6 over an unread reply. The #457 live matrix covers CLASS/VCLASS/ATTRIBUTE/CONSTRAINT/ PRIMARY_KEY/IMPORTED_KEYS/EXPORTED_KEYS on 10.2/11.4, not all schema codes or native-driver parity. Source references: CAS FC9 arguments, CCI condensed columns, and JDBC schema request.


BatchExecutePacket

Batch execute (FC=20) — multiple SQL statements in one request.

Reply (CAS ux_execute_batch): int result code, int executed statement count, then per statement a statement-type byte and either int count >= 0 and an 8-byte OID, or a negative int (DBMS_ERROR_INDICATOR, -2, for V3+, otherwise the error code itself), the error code (V3+) and a length-prefixed message; a shard id follows for V5+. A failed statement does not stop the later ones; as in ux_execute_array, the CAS stops early only after an ER_INTERRUPTED (-4) statement, which it reports, rewriting the executed count. An executed count that is negative or larger than the number of statements sent, or a short reply that does not end with an interrupted statement, is malformed.

Attribute Type Description
results list[tuple[int, int]] (stmt_type, result_count) per successful statement
errors list[dict] {code, message, index} for failed statements
row_results tuple[tuple[int, int, str], ...] (count, 0, "") or (-1, code, message) per executed statement, in order (#841)

ExecuteArrayPacket

Execute array (FC=21, #839) — one prepared statement executed once per row of bound values. Sent by executemany(..., prepared=True) after a PREPARE (FC=2) on the same session.

Request (CAS fn_execute_array): int query handle, int query timeout (0, protocol V4+ only), byte autocommit, then one (type byte, value) argument pair per marker per row, in row order: the same pair ExecutePacket writes for one binding. Every row must have exactly the server's bind count; the CAS would silently ignore extra values.

Reply (CAS ux_execute_array): int result code, int executed row count, then per row either int count >= 0 and an 8-byte OID, or a negative int (DBMS_ERROR_INDICATOR, -2, for V3+, otherwise the error code itself), the error code (V3+) and a length-prefixed message; a shard id follows for V5+. The CAS stops early only after an ER_INTERRUPTED (-4) row, which it reports; any other short reply is malformed.

Attribute Type Description
row_results tuple[tuple[int, int, str], ...] (count, 0, "") or (-1, code, message) per executed row, in row order
errors list[dict] {code, message, index} for failed rows

With autocommit on, the CAS commits each successful row and aborts each failing one; with autocommit off, a deadlock or a server shutdown fails the whole request.


LOBNewPacket

Create LOB handle (FC=35).

Attribute Type Description
lob_handle bytes Server-generated LOB handle

LOBWritePacket

Write LOB data (FC=36).

Attribute Type Description
bytes_written int Bytes the server wrote

The request carries the packed handle, an int64 offset and the data. The server only appends: on CUBRID 10.2 and 11.4 a write at any offset other than the current size, inside the value or past its end, fails with -1016. After a successful write, Lob.write() raises the handle's size field to offset + bytes_written (never lowers it), as CCI does.


LOBReadPacket

Read LOB data (FC=37).

Attribute Type Description
bytes_read int Actual bytes read
lob_data bytes The read data

GetLastInsertIdPacket

Get last insert ID (FC=40).

Attribute Type Description
last_insert_id str Last auto-increment value as string

GetDbParameterPacket / SetDbParameterPacket

Get/Set database parameters (FC=4 / FC=5).

Attribute Type Description
value int Parameter value (get result / set input)

Available parameters (CCIDbParam):

Code Name Description
1 ISOLATION_LEVEL Transaction isolation level
2 LOCK_TIMEOUT Lock wait timeout
3 MAX_STRING_LENGTH Maximum string length
4 AUTO_COMMIT Auto-commit mode (0/1)

PacketWriter

pycubrid.packet.PacketWriter — serializes data in CAS wire format.

Public Methods

Method Description
add_byte(value) Write length-prefixed byte (1B length + 1B value)
add_short(value) Write length-prefixed short (4B length + 2B value)
add_int(value) Write length-prefixed int (4B length + 4B value)
add_long(value) Write length-prefixed long (4B length + 8B value)
add_float(value) Write length-prefixed float (4B length + 4B value)
add_double(value) Write length-prefixed double (4B length + 8B value)
add_bytes(value) Write length-prefixed raw bytes
add_null() Write null marker (zero-length)
add_date(y, m, d) Write length-prefixed date
add_time(h, m, s) Write length-prefixed time
add_timestamp(y,m,d,h,m,s) Write length-prefixed timestamp
add_datetime(y,m,d,h,m,s,ms) Write length-prefixed datetime
add_cache_time() Write cache time (two zero ints)
to_bytes() Return all written bytes

Internal Methods

Method Description
_write_byte(value) Raw byte, no length prefix
_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 bytes, no prefix
_write_filler(count, value) Fill N bytes with value
_write_null_terminated_string(value) Length-prefixed string in the connection charset (default UTF-8) + null terminator
_write_fixed_length_string(value, length) Fixed-width null-padded string, cut on a character boundary

PacketReader

pycubrid.packet.PacketReader — deserializes CAS wire data.

Primitive Parsers

Method Returns Bytes Consumed
_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

Every read stays inside the reply (#383). A length-prefixed read (bytes, text, NUMERIC, JSON, a raw collection or LOB handle, _skip_bytes()) checks 0 <= length <= bytes_remaining() before it moves and raises ValueError otherwise; a fixed-width read past the end raises struct.error or IndexError. A failed read leaves the offset unchanged. Text readers return "" for a non-positive length without moving. A decoded collection's elements must fill its declared size exactly, and a LOB_READ byte count must fit the reply (a count below the requested length is a valid short read). A fetched BLOB/CLOB cell must be exactly the packed handle the CAS writes (cas_execute.c add_res_data_lob_handle, 10.2 to 11.4; #933): a 16-byte header [int32 db_type][int64 size][int32 locator size], then a locator that fills the rest of the cell. The embedded db_type must be the server DB_TYPE of the cell's type, BLOB 33 or CLOB 34 (not the CCI type 23/24). A shorter header, a negative locator size, a locator longer or shorter than the rest of the cell, or another embedded type raises ValueError, also when the row parser re-walks a reply before raising DataError for an earlier cell. Bytes after the declared cell belong to the next field. A size of -1 with an empty locator (a value without an external file) stays valid, and the decoder does not check the locator's trailing NUL. Each row cell of a FETCH or inline execute reply must use exactly the bytes its size word declares: fixed-width values (INT, DATE, OBJECT, ...) do not read the size themselves, so the row parser checks it against the type's width before reading the value (#523), also when it re-walks a reply before raising DataError. The CAS writes SQL NULL as size -1; a non-positive size is NULL, except that a BIT/VARBIT cell of size 0 is an empty bit string, b"" (#994). A negative FETCH tuple count is malformed too, and so is a negative column count or column name, real-name, table-name or default length in FC2, FC3 or FC41 column metadata (#555); a zero length is an empty string. FC41 also rejects a negative bind count, total_tuple_count or inline tuple count, and a column count the rest of the reply cannot hold (31 bytes per column at least), as FC2 does; FC3 rejects a negative inline tuple count (#581). Column metadata text that the connection codec cannot decode is deferred after the remaining metadata is walked by its declared lengths (#581). FC41 and FC3 with refreshed columns then validate their remaining tail, counts and inline rows before re-raising the first metadata DataError; later structural damage still wins (#591). This error path does not call application JSON deserializers, and validates subsequent cells even if an earlier row value is unrepresentable. Known collection framing is checked even when normal results use opaque bytes; negative element counts are malformed. A partially present inline-fetch header is malformed; an absent optional header remains supported. A per-row error message length in an FC20 (EXECUTE_BATCH) or FC21 (EXECUTE_ARRAY) reply must be 0 <= length <= bytes_remaining(), and the type header of the FC40 (GET_LAST_INSERT_ID) value counts in its size like a row cell's, so a header longer than the value is malformed (#897). The connection turns these exceptions into OperationalError("malformed response from broker") and closes; DataError stays reserved for a complete reply whose value Python cannot represent (#492, #512). Unread bytes after the last value a reply declares are not checked.

For known decoded collection member types, each length word and payload must fit the collection and the value decoder must consume exactly its declared bytes. After a complete element raises DataError, later elements still run their real decoders: a malformed representation wins over the saved error (#595). A complete collection retains the first conversion error and its cause. NULL-only, opaque/disabled decoding and unsupported nested member layouts retain their existing contracts; no recursive decoding capability is added.

Internal bounds re-walks use PacketReader.mark() and seek(position). Seeking outside the reply or to a non-integer position raises ValueError without moving the reader; zero and the end of the reply are valid positions.

Composite Parsers

Method Returns Description
_parse_date() datetime.date 3 shorts (year, month, day)
_parse_time() datetime.time 3 shorts (hour, minute, second)
_parse_datetime() datetime.datetime 7 shorts (y,m,d,h,m,s,ms)
_parse_timestamp() datetime.datetime 6 shorts (y,m,d,h,m,s)
_parse_timestamptz() datetime.datetime 6 shorts (y,m,d,h,m,s) + timezone string; second precision (TIMESTAMPTZ/TIMESTAMPLTZ)
_parse_datetimetz() datetime.datetime 7 shorts (y,m,d,h,m,s,ms) + timezone string; millisecond precision (DATETIMETZ/DATETIMELTZ)
_parse_numeric(size) Decimal Null-terminated string → Decimal
_parse_object() str OID string ("OID:@page|slot|volume")
read_blob(size) dict BLOB handle info
read_clob(size) dict CLOB handle info
read_error(length) (int, str) Error code and message
bytes_remaining() int Unread bytes in buffer

Data Types on the Wire

Column metadata preserves the first type byte's 0x60 collection-kind bits: 0x20 is SET, 0x40 MULTISET and 0x60 SEQUENCE. With 0x80 set, a second byte carries the full scalar/element type (including codes above 31); otherwise the low five bits carry it. Collection row dispatch uses the collection kind, not that element type.

Cells of a CALL / EVALUATE result and of a column whose metadata type is NULL (SELECT NULL, ...) carry their own type header before the value, and the cell size counts it. Protocol 7+ brokers (CUBRID 10.2+) write it like column metadata: 0x80 | collection bits | charset, then the type byte, for example 00000006 83 08 0000002a for CALL of a function returning INT 42 and 0000000a 83 13 <8-byte OID> for CALL find_user('dba') ON CLASS db_user. Older brokers write one type byte. The driver reads either layout (#542); a header longer than its cell is a malformed reply.

A collection value is one element-type byte, a 4-byte element count, then a 4-byte length and payload per element; a NULL element has length -1 and no payload. An empty BIT/VARBIT element (X'') has length 0 and decodes as b""; a length-0 element of any other type decodes as None (#998). When every element is NULL, CUBRID 10.2/11.4 send element type 0 (NULL): {} is 00 00000000 and {NULL, NULL} is 00 00000002 ffffffff ffffffff.

Column data is transmitted as a 4-byte size prefix followed by the raw data. The type determines how the data bytes are interpreted:

Type Code Name Wire Format
0 NULL size ≤ 0 → None
1 CHAR Null-terminated string in the connection charset (default UTF-8)
2 STRING Null-terminated string in the connection charset (default UTF-8)
3 NCHAR Null-terminated string in the connection charset (default UTF-8)
4 VARNCHAR Null-terminated string in the connection charset (default UTF-8)
5 BIT Raw bytes; size 0 → b"" (#994)
6 VARBIT Raw bytes; size 0 → b"" (#994)
7 NUMERIC Null-terminated string → Decimal
8 INT 4-byte big-endian signed int
9 SHORT 2-byte big-endian signed short
10 MONETARY 8-byte big-endian double
11 FLOAT 4-byte big-endian float
12 DOUBLE 8-byte big-endian double
13 DATE 3 shorts: year, month, day
14 TIME 3 shorts: hour, minute, second
15 TIMESTAMP 6 shorts: y, m, d, h, m, s
16–18 SET/MULTISET/SEQUENCE Raw bytes
19 OBJECT 4B page + 2B slot + 2B volume
21 BIGINT 8-byte big-endian signed long
22 DATETIME 7 shorts: y, m, d, h, m, s, ms
23 BLOB Packed LOB handle → dict
24 CLOB Packed LOB handle → dict
25 ENUM Null-terminated string in the connection charset (default UTF-8)

Column Metadata

Each column in a result set carries metadata parsed from the wire:

@dataclass
class ColumnMetaData:
    column_type: int         # CUBRIDDataType code
    scale: int               # Decimal scale (-1 if not applicable)
    precision: int           # Precision (-1 if not applicable)
    name: str                # Column alias
    real_name: str           # Actual column name
    table_name: str          # Source table name
    is_nullable: bool        # Allows NULL (normalized from the inverse wire flag)
    default_value: str       # Default value expression
    is_auto_increment: bool  # AUTO_INCREMENT column
    is_unique_key: bool      # Part of unique index
    is_primary_key: bool     # Part of primary key
    is_reverse_index: bool   # Has reverse index
    is_reverse_unique: bool  # Has reverse unique index
    is_foreign_key: bool     # Part of foreign key
    is_shared: bool          # Shared attribute

The raw column-metadata byte is is_non_null: zero permits NULL and nonzero means NOT NULL (the broker normalizes it to 0/1). PacketReader supplies that byte to the metadata parser, which derives is_nullable and the public cursor.description null_ok field using raw == 0.


Error Handling

When response_code < 0, the response contains an error:

graph LR
    code["Error Code (4B int)"] --> message["Error Message (null-terminated string)"]

The negative outer response indicator identifies the error domain (CAS -1 or DBMS -2); the following inner error code is a separate number. _raise_error() first looks up that inner numeric code in the existing error mapping. Only an unmapped inner code exactly -1 uses the text fallback below; an outer CAS indicator of -1 does not select that fallback. Other unmapped codes raise DatabaseError, regardless of their message text. Exceptions retain the received inner number unchanged in code and errno; the outer domain is exposed as DatabaseError.error_source: "cas" for -1, "server" for -2, and None for any other value (#903). The driver does not set BROKER_RENEWED_ERROR_CODE in the handshake, so a CAS error arrives in the old numbering (CAS_ER_* plus 9000, -1000 to -1200), which overlaps server codes; the indicator is what tells them apart.

Text fallback for inner code -1 only:

Error pattern Exception
unique, duplicate, foreign key, constraint violation IntegrityError
syntax, unknown class, does not exist, not found ProgrammingError
Other fallback text DatabaseError

Renewed CAS numbers: evaluation decision (#505)

Keep the current handshake function byte (zero-based index 7) at 0, for both plaintext and STARTTLS requests, and preserve raw public code/errno. Renewed-code bit 0x80 is supported by the researched brokers, but enabling it or adding an opt-in requires a separate domain-aware consumer/migration contract. This deferral does not resolve existing numeric ambiguity. Legacy CAS communication -1003 overlaps engine ER_SM_CONSTRAINT_HAS_DIFFERENT_TYPE=-1003; do not infer a domain or normalize numbers by adding/subtracting 9000 alone. The pinned broker serializer and CCI conversion use domain/protocol/capability conditions, not just numeric ranges; the engine collision is source-proven, not reproduced live. The separate holdable bit 0x40 in the function byte is outside this evaluation and remains disabled.

On 2026-10-03, frozen driver 984d3a2c9e30d613936d69195719a3439e1c08e1 with Python 3.12.13 compared flag 0 versus 0x80 on owned builds 10.2.18.9024, 11.0.16.0419, 11.2.9.0866 and 11.4.6.1963. A validly framed FC4 request with unsupported parameter ID 255 returned outer CAS domain -1 and inner -1011 versus -10011; each surfaced unchanged as DatabaseError.code and errno. The DBMS syntax control retained outer -2, inner -493 and ProgrammingError under both flags. Same-session health queries succeeded without replay or reconnect. This proves only that producer on those builds, not every patch or auth/embedded/batch/TLS/redirect/reconnect path, the live numeric collision or downstream migration. No negotiation/API behavior changed.


Constants Reference

CAS Protocol Constants

class CASProtocol:
    MAGIC_STRING = "CUBRK"      # Handshake magic (plaintext)
    MAGIC_STRING_SSL = "CUBRS"  # Handshake magic (STARTTLS-requested)
    CLIENT_JDBC = 3             # Client type identifier
    PROTO_INDICATOR = 0x40      # Protocol indicator bit
    VERSION = 8                 # Protocol version
    CAS_VERSION = 0x48          # Combined version byte

Wire Data Sizes

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

Statement Types

Code Name Code Name
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

Transaction Types

Code Name
1 COMMIT
2 ROLLBACK

Isolation Levels

Code Name
0x01 COMMIT_CLASS_UNCOMMIT_INSTANCE (legacy pre-MVCC)
0x02 COMMIT_CLASS_COMMIT_INSTANCE
0x03 REP_CLASS_UNCOMMIT_INSTANCE (legacy pre-MVCC)
0x04 REP_CLASS_COMMIT_INSTANCE (READ COMMITTED, server default)
0x05 REP_CLASS_REP_INSTANCE
0x06 SERIALIZABLE

On CUBRID's MVCC engine (10.0+) only levels 0x04/0x05/0x06 are accepted; the lower codes 0x01-0x03 are legacy pre-MVCC levels. CUBRID's server default isolation level is READ COMMITTED (0x04).