Skip to content

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 the connect function, 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 the Cursor object, 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/Sequence collections and LOB handles, with no SQL rendering and no I/O. It imports nothing from protocol.py; protocol.py re-exports every name, so pycubrid.protocol._PreparedScalar and 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 connection charset codec (#86): the connection sets it on every packet before write(), 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 the Lob class 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.
  • qmark paramstyle 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-in executemany(..., prepared=True) (#839), which sends typed host variables with one PREPARE and one EXECUTE_ARRAY request. 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_READERS dictionary 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 bytes avoids the performance overhead and complexity of recursive parsing for features that are rarely used in standard applications.
  • Identify as JDBC client: Sending CLIENT_JDBC=3 during 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).

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.