Architecture¶
For a reader-facing introduction to the Broker, CAS process, CAS wire protocol and CCI, see CAS Architecture and Protocol Concepts.
Design Objectives¶
- Pure Python: Zero system dependencies, no C extensions or CCI library required, runs anywhere Python runs.
- Full PEP 249 Compliance: Implements the Python Database API Specification v2.0 for maximum compatibility.
- CAS Binary Protocol v8: Targets the current CUBRID CAS protocol version used by pycubrid.
- Single-connection Synchronous Model: Reliable, blocking I/O model suitable for standard application integration.
- PEP 561 Typed: Fully type-hinted for modern IDE support and static analysis.
High-Level Flow¶
Phase 1: Connection Handshake¶
sequenceDiagram
participant App
participant pycubrid as pycubrid.connect()
participant Broker as Broker (port 33000)
participant CAS as CAS Process
participant DB as CUBRID Database
App->>pycubrid: connect(host, port, database, user, password)
rect rgb(230, 245, 255)
note over pycubrid, CAS: Phase 1 — Broker Handshake
pycubrid->>Broker: TCP connect to port 33000
alt ssl truthy
pycubrid->>Broker: ClientInfoExchange ("CUBRS" + CLIENT_JDBC=3 + v8)
else plaintext
pycubrid->>Broker: ClientInfoExchange ("CUBRK" + CLIENT_JDBC=3 + v8)
end
Broker-->>pycubrid: status int32 (0 ok / >0 redirect port / <0 fail-fast)
opt status > 0 (redirect)
pycubrid->>CAS: TCP reconnect to redirected port (no rehandshake)
end
opt ssl truthy
pycubrid->>CAS: TLS upgrade (start_tls / wrap_socket)
end
end
rect rgb(230, 255, 230)
note over pycubrid, DB: Phase 2 — Database Session
pycubrid->>CAS: OpenDatabase (db, user, password — 628B raw)
CAS->>DB: Authenticate + open session
DB-->>CAS: Session established
CAS-->>pycubrid: CAS Info (4B) + response_code (4B) + Broker Info (8B) + Session ID (4B)
end
pycubrid-->>App: Connection object
Phase 2: Query Lifecycle¶
sequenceDiagram
participant App
participant Cursor
participant Connection
participant CAS
App->>Cursor: execute("SELECT ...", params)
rect rgb(255, 245, 230)
note over Cursor, CAS: SQL Execution
Cursor->>Connection: _send_and_receive(PrepareAndExecutePacket)
Connection->>CAS: [4B length][4B cas_info][FC=41 + SQL + params]
CAS-->>Connection: [4B length][4B cas_info][result metadata + inline rows]
Connection-->>Cursor: Parsed response (columns, rows)
end
Cursor-->>App: None (results buffered)
App->>Cursor: fetchall()
alt All rows in initial fetch
Cursor-->>App: Buffered rows
else More rows on server
Cursor->>Connection: _send_and_receive(FetchPacket)
Connection->>CAS: [4B length][4B cas_info][FC=8 + handle + offset]
CAS-->>Connection: [4B length][4B cas_info][row data]
Connection-->>Cursor: Additional rows
Cursor-->>App: All rows
end
App->>Cursor: close()
Cursor->>Connection: _send_and_receive(CloseQueryPacket)
Connection->>CAS: [FC=6 + query_handle]
CAS Reconnection¶
CAS_INFO[0] is transaction state (0 = OUT_TRAN, 1 = IN_TRAN), not
connection liveness. A normal commit, rollback, or autocommit response with
OUT_TRAN keeps the same physical session. An ordinary SQL request is never
replayed on another connection after an uncertain transport failure.
The CAS may still close the socket right after an OUT_TRAN reply: a CAS
memory restart (APPL_SERVER_MAX_SIZE), cubrid broker reset, or CHANGE
CLIENT when more clients than CAS processes are waiting. Like JDBC
UClientSideConnection.checkReconnect, the driver therefore sends CHECK_CAS
before the next request when the last reply was OUT_TRAN (#485). A live CAS
keeps the session. Only a failed probe replaces it: once per request, with the
escape-mode probe and autocommit restored and no SQL replayed. A CLOSE_REQ
for a handle of the lost session is skipped, a FETCH of its result raises
OperationalError, and a failed reconnect raises OperationalError. Commit and rollback first send CLOSE_REQ for query
handles still held by unclosed cursors, so handles do not accumulate in a CAS
session that now outlives transactions.
sequenceDiagram
participant App
participant Connection
participant Broker
participant CAS
App->>Connection: commit()/rollback()
Connection->>CAS: CLOSE_REQ for each open cursor handle, then END_TRAN
CAS-->>Connection: CAS_INFO[0]=0 (OUT_TRAN)
App->>Connection: next request
Connection->>CAS: CHECK_CAS (FC=32)
alt CAS alive
CAS-->>Connection: Healthy
note over Connection,CAS: Same socket and session remain in use
else CAS closed the socket (restart, reset, CHANGE CLIENT)
Connection->>Broker: Reconnect once, probe escape mode, restore autocommit
note over Connection: Original request is sent once on the new session
end
App->>Connection: ping(reconnect=True)
opt Existing socket is connected
Connection->>CAS: CHECK_CAS (FC=32)
CAS-->>Connection: Healthy or negative (CAS-to-DB link broken)
end
opt Disconnected, negative CHECK_CAS, or transport/protocol failure
Connection->>Connection: Discard old transport and query handles
Connection->>Broker: ClientInfoExchange ("CUBRK"/"CUBRS")
Broker-->>Connection: status int32 (0 / >0 redirect / <0 fail)
opt status > 0 (redirect)
Connection->>CAS: TCP reconnect to redirected port (no rehandshake)
end
opt ssl truthy
Connection->>CAS: TLS upgrade (start_tls / wrap_socket)
end
Connection->>CAS: OpenDatabase
CAS-->>Connection: New session
note over Connection: Restore explicitly set autocommit only
end
ping(reconnect=False) still checks an open socket but never reconnects,
including when CHECK_CAS returns a negative code.
Recovery through ping(reconnect=True) is explicit and limited to one attempt;
the caller decides whether to retry interrupted SQL.
Module Boundaries¶
flowchart TD
init["__init__.py<br/>Public API & PEP 249 globals"]
conn["connection.py<br/>TCP socket, transactions, LOB"]
cursor["cursor.py<br/>execute, fetch, callproc"]
protocol["protocol.py<br/>18 CAS packet classes"]
binding["_prepared_binding.py<br/>Prepared bind encoders"]
packet["packet.py<br/>PacketReader / PacketWriter"]
constants["constants.py<br/>CASFunctionCode, CUBRIDDataType"]
types["types.py<br/>DBAPIType, STRING, NUMBER, ..."]
exceptions["exceptions.py<br/>PEP 249 exception hierarchy"]
lob["lob.py<br/>Lob class (BLOB/CLOB)"]
init --> conn
init --> types
init --> exceptions
init --> lob
conn --> protocol
conn --> packet
conn --> cursor
conn --> exceptions
cursor --> protocol
cursor --> packet
cursor --> exceptions
protocol --> binding
binding --> packet
protocol --> packet
protocol --> constants
protocol --> exceptions
lob --> conn
__init__.py— Public API & PEP 249 Globals: The entry point of the package. It exposes theconnectfunction, exception hierarchy, and DB-API type objects.connection.py— TCP Socket & Transaction Management: Manages the physical TCP connection to the CAS, handles transactions (commit/rollback), and acts as the owner for LOB operations.cursor.py— SQL Execution & Result Fetching: Implements theCursorobject, handling SQL preparation, execution, and the various fetch operations while maintaining state of results.protocol.py— CAS Packet Classes: Defines 18 specialized packet classes that map to CUBRID CAS function codes, handling the serialization and deserialization of specific requests and responses._prepared_binding.py— Prepared Bind Encoders (private): Validates Python values and encodes them as the typed bind arguments of the prepared FC3 (EXECUTE) and FC21 (EXECUTE_ARRAY) requests: scalars,Set/Multiset/Sequencecollections and LOB handles, with no SQL rendering and no I/O. It imports nothing fromprotocol.py;protocol.pyre-exports every name, sopycubrid.protocol._PreparedScalarand the other names are the same objects (#856).packet.py— PacketReader / PacketWriter: Provides low-level utilities for reading from and writing to the wire format, handling byte order and primitive type serialization. Both carry the connectioncharsetcodec (#86): the connection sets it on every packet beforewrite(), so SQL text, credentials, character values, column names, error text and LOB locators use the database charset, while fetched JSON, NUMERIC and timezone names stay UTF-8. See Character Encoding.constants.py— CAS Constants: Contains enumeration for CAS function codes, CUBRID data types, and other protocol-level constants.types.py— DB-API Types: Defines the type objects required by PEP 249 and manages the mapping between CUBRID types and Python types.exceptions.py— PEP 249 Exceptions: Implements the standard hierarchy of exceptions required by the DB-API 2.0 specification.lob.py— LOB Management: Implements theLobclass for handling Large Object data (BLOB/CLOB), providing an interface for reading and writing data in chunks.
Packet Format¶
┌─────────────────┬──────────────┬─────────────────────────┐
│ Data Length │ CAS Info │ Payload │
│ (4 bytes) │ (4 bytes) │ (variable length) │
│ big-endian int │ session │ [FC byte][arguments…] │
└─────────────────┴──────────────┴─────────────────────────┘
The handshake packet (ClientInfoExchange) does NOT use this framing; it uses a specialized 10-byte fixed header for the initial broker negotiation.
Type Dispatch¶
flowchart TD
wire["Wire Data<br/>[4B size][raw bytes]"]
dispatch{"_TYPE_READERS<br/>dict dispatch<br/>O(1) lookup"}
wire --> dispatch
dispatch -->|"1-4, 25"| str["str<br/>(connection charset,<br/>default UTF-8)"]
dispatch -->|"5, 6"| bytes["bytes<br/>(raw binary)"]
dispatch -->|"7"| decimal["Decimal<br/>(string-parsed)"]
dispatch -->|"8"| int32["int<br/>(4B signed)"]
dispatch -->|"9"| int16["int<br/>(2B signed)"]
dispatch -->|"21"| int64["int<br/>(8B signed)"]
dispatch -->|"11"| float32["float<br/>(IEEE 754 single)"]
dispatch -->|"12, 10"| float64["float<br/>(IEEE 754 double)"]
dispatch -->|"13"| date["datetime.date"]
dispatch -->|"14"| time["datetime.time"]
dispatch -->|"15, 22, 29-32"| datetime["datetime.datetime"]
dispatch -->|"19"| oid["str (OID)"]
dispatch -->|"23, 24"| lob["dict (LOB handle)"]
dispatch -->|"16-18"| collection["bytes (opaque)"]
Key Design Decisions¶
- Pure Python over C extension: Zero system dependencies ensure the driver runs anywhere Python is available, simplifying deployment and avoiding cross-compilation issues.
- CAS protocol v8: The driver targets the current broker protocol, including JSON-aware parsing paths and modern feature support, without carrying compatibility code for legacy protocol revisions.
qmarkparamstyle with driver-side binding: Parameters use?placeholders. The driver escapes and interpolates values locally (type-aware escaping for strings, bytes, dates, decimals, None → NULL) before sending the final SQL to the CAS broker. This is not server-side prepared statement binding — the broker receives a complete SQL string. The exception is the opt-inexecutemany(..., prepared=True)(#839), which sends typed host variables with onePREPAREand oneEXECUTE_ARRAYrequest. This design avoids a protocol round-trip for PREPARE and simplifies the implementation while maintaining injection safety through strict type-dispatch escaping.- Dict-based type dispatch: Utilizing the
_TYPE_READERSdictionary provides O(1) lookup performance, ensuring high-speed result parsing compared to iterative conditional checks. - Opaque collection types: Returning SET, MULTISET, and SEQUENCE types as raw
bytesavoids the performance overhead and complexity of recursive parsing for features that are rarely used in standard applications. - Identify as JDBC client: Sending
CLIENT_JDBC=3during handshake ensures the CAS treats pycubrid with the same stability and feature set as the official JDBC driver.
Public API Boundary¶
# Module-level attributes (PEP 249)
apilevel = "2.0"
threadsafety = 1
paramstyle = "qmark"
# Constructor
connect(host, port, database, user, password, **kwargs) -> Connection
# Exceptions
Warning, Error, InterfaceError, DatabaseError, DataError,
OperationalError, IntegrityError, InternalError,
ProgrammingError, NotSupportedError
# Type Objects
STRING, BINARY, NUMBER, DATETIME, ROWID
# Constructors
Date, Time, Timestamp, DateFromTicks, TimeFromTicks, TimestampFromTicks, Binary
# Extensions
Lob, get_error_description
What This Package Owns / Does Not Own¶
- Owns: CAS wire protocol implementation, PEP 249 interface, type conversion, connection lifecycle, LOB support.
- Does not own: Connection pooling (use SQLAlchemy), ORM (use sqlalchemy-cubrid), schema migration (use Alembic), query building (use SQLAlchemy Core).
Related Documents¶
CUBRID Server License Relationship¶
pycubrid speaks the CAS wire protocol and is an independent, pure-Python client implementation. It contains no CUBRID server source and links no server libraries, so it is not a derivative work of the server under any license regime.
The CUBRID server engine is distributed under Apache License 2.0, with official
APIs/connectors under BSD (upstream COPYING, http://www.cubrid.org/cubrid).
An earlier GPL v2+ regime no longer applies — and even under it, a wire-protocol
client over TCP was unaffected. No copyleft obligations reach this codebase.
The cubrid/cubrid Docker images (10.2-11.4) are used in CI strictly to run
integration tests against a live server; they are not distributed with pycubrid.