Connection Guide¶
This guide covers how to install pycubrid, connect to a CUBRID database, and understand the connection lifecycle.
Table of Contents¶
- Prerequisites
- Installation
- Connection Function
- Connection Examples
- SSL/TLS
- Context Manager Protocol
- Autocommit Mode
- Connection Methods
- Broker Handshake
- Server Version Detection
- Troubleshooting
- Docker Quick Start
- SQLAlchemy Integration
Prerequisites¶
| Requirement | Version |
|---|---|
| Python | 3.11+ |
| CUBRID Server | 10.2–11.4 |
No C compiler or native libraries required — pycubrid is pure Python.
Tip
For local development, start with host="localhost", port=33000, user="dba", and empty password unless your environment is hardened.
Installation¶
From PyPI¶
From Source¶
git clone https://github.com/cubrid-lab/pycubrid.git
cd pycubrid
pip install -e . --group dev # pip 25.1+; contributor tools are a dependency group
Connection Function¶
def connect(
host: str = "localhost",
port: int = 33000,
database: str = "",
user: str = "dba",
password: str = "",
decode_collections: bool = False,
json_deserializer: Any = None,
ssl: bool | ssl_module.SSLContext | None = None,
charset: str = "utf-8",
**kwargs: Any,
) -> Connection
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
host |
str |
"localhost" |
CUBRID server hostname or IP address |
port |
int |
33000 |
CUBRID broker port |
database |
str |
"" |
Database name (required) |
user |
str |
"dba" |
Database username |
password |
str |
"" |
Database password |
decode_collections |
bool |
False |
Decode SET/MULTISET/SEQUENCE columns into Python collections |
json_deserializer |
Any |
None |
Callable used to decode JSON columns on fetch; when unset JSON is returned as str |
charset |
str |
"utf-8" |
Python codec (or CUBRID utf8/euckr/iso88591) for SQL text, credentials, character values, names and error text; set it to the database charset. See Character Encoding |
ssl |
bool \| ssl_module.SSLContext \| None |
None |
Opt-in TLS for sync broker connections |
Keyword Arguments¶
| Kwarg | Type | Default | Description |
|---|---|---|---|
connect_timeout |
int \| float \| None |
None |
TCP connect timeout in seconds |
read_timeout |
int \| float \| None |
None |
Request I/O timeout in seconds: each socket operation (sync) or each whole request round trip (async); see What the timeouts bound |
fetch_size |
int |
100 |
Server-side fetch batch size |
enable_timing |
bool \| None |
None |
Enable driver timing stats, or fall back to PYCUBRID_ENABLE_TIMING |
no_backslash_escapes |
bool \| None |
None (auto) |
Probe each new physical session's string-escape mode; explicit True/False skips detection and remains pinned across recovery |
autocommit |
bool |
False |
Enable immediate commit per statement |
connect_timeout and read_timeout accept None or finite non-negative numbers. Negative values, NaN and infinity raise ValueError; incompatible types raise TypeError. Both sync and async connections validate these settings before acquiring transport resources, without wrapping configuration errors in OperationalError. Zero remains accepted and does not disable deadlines; None means no deadline. The sync driver's zero-timeout behavior is unchanged. The async driver bounds the TCP connect (connect_timeout), the connect handshake and each request round trip (read_timeout) with asyncio.wait_for(), so a zero deadline cancels the fresh, not-yet-started operation before it runs: connect_timeout=0 fails before the TCP connect is attempted and read_timeout=0 fails before any handshake bytes are sent, both with OperationalError. The driver keeps wait_for() rather than asyncio.timeout() because on CPython 3.11.0–3.11.2 an expired timeout() block entered by an already-cancelled task (for example cleanup after a caught CancelledError) re-raises CancelledError (python/cpython#102780); with wait_for() a driver deadline is reported as OperationalError there too, while a new cancellation of the calling task still raises asyncio.CancelledError (#744).
What the timeouts bound. Each setting bounds one connect attempt, socket call
or request round trip; none is a wall-clock deadline for a whole SQL statement or execute() call, and none
cancels a statement on the server
(#855):
| Setting | Sync driver | Async driver |
|---|---|---|
connect_timeout |
Each TCP connection attempt (socket.create_connection()), including the reconnect to a redirected CAS port |
Each asyncio.open_connection() call, including the reconnect to a redirected CAS port |
read_timeout (connect) |
Each socket send or receive call of the broker handshake and OPEN_DATABASE |
The whole connect handshake: broker handshake, any redirect reconnect, TLS handshake and OPEN_DATABASE together |
read_timeout (request) |
Each socket send or receive call of a request; a reply that keeps arriving in pieces, each within the timeout, is waited for however long it takes | The whole round trip of one request: sending it and reading its complete reply |
| TLS handshake | The whole TLS handshake (Python's ssl module applies the socket timeout to it as one deadline): read_timeout, or 10 seconds when it is unset |
The whole TLS handshake (ssl_handshake_timeout): read_timeout, or 10 seconds when it is unset |
So the same read_timeout can end differently on the two drivers: a broker that
sends a reply slowly in pieces, each piece within read_timeout but the whole
reply taking longer, completes on the sync driver and raises
OperationalError('read timeout: ...') on the async driver
(tests/test_slow_stream_timeout.py). A statement whose execution itself takes
long sends no reply bytes until it finishes, so both drivers raise once
read_timeout passes. When a timeout fires, the driver only stops waiting and
retires the session (see below): the CAS may still be running, committing or
rolling back the statement, and the driver never resends it. An operation that
sends several requests (for example a CHECK_CAS probe before the statement, or
a FETCH for each later page) applies the timeout to each request separately.
To bound a whole operation, wrap the call in your own deadline (for example
asyncio.timeout() around an async call) and treat its expiry as an unknown
outcome: it does not cancel the statement on the server either. read_timeout=0
is not a usable bound on either driver: it makes the sync socket non-blocking,
so the connect fails at once with OperationalError (caused by
BlockingIOError), and the async driver fails before the handshake starts.
Unknown Options¶
Any keyword outside the two tables above is not a supported connection
option. pycubrid ignores it, but reports it through the
pycubrid.UnknownConnectionOptionWarning category so that a typo is not
swallowed silently:
import pycubrid
pycubrid.connect(host="localhost", database="demodb", read_timout=30)
# UnknownConnectionOptionWarning: Unknown connection option ignored by pycubrid:
# 'read_timout' (did you mean 'read_timeout'?). Supported options: autocommit,
# connect_timeout, database, decode_collections, enable_timing, fetch_size,
# host, json_deserializer, no_backslash_escapes, password, port, read_timeout,
# ssl, user.
The warning is emitted before any socket work, so a mis-spelled option is
reported even when the connection itself then fails. It applies equally to
pycubrid.connect(), pycubrid.aio.connect(), and direct Connection(...) /
AsyncConnection(...) construction.
A warning — rather than a TypeError — is the default because wrapper layers
(connection pools, ORM dialects) legitimately forward extra keywords, and
rejecting them outright would break those callers. Use the standard warnings
machinery to pick the strictness you want:
import warnings
import pycubrid
# Strict: turn an unknown option into an error.
warnings.simplefilter("error", pycubrid.UnknownConnectionOptionWarning)
# Lenient: silence it entirely (e.g. inside a wrapper that forwards kwargs).
warnings.simplefilter("ignore", pycubrid.UnknownConnectionOptionWarning)
Common Connection Profiles¶
| Profile | host | port | user | password | autocommit | Use case |
|---|---|---|---|---|---|---|
| Local default | localhost |
33000 |
dba |
"" |
False |
Development and smoke tests |
| Remote app | db.example.com |
33000 |
app_user |
required | False |
Production service workloads |
| Script mode | any | any | any | any | True |
One-off migration/maintenance scripts |
Return Value¶
Returns a Connection object implementing PEP 249.
Connection Examples¶
Basic Connection¶
import pycubrid
conn = pycubrid.connect(
host="localhost",
port=33000,
database="testdb",
user="dba",
)
cur = conn.cursor()
cur.execute("SELECT 1 + 1")
print(cur.fetchone()) # (2,)
cur.close()
conn.close()
Warning
The database argument is required in real environments. Empty database names can fail at OpenDatabasePacket stage depending on server configuration.
With Password¶
conn = pycubrid.connect(
host="localhost",
port=33000,
database="demodb",
user="dba",
password="mypassword",
)
Custom Port and Timeout¶
conn = pycubrid.connect(
host="db-server.internal",
port=33100,
database="production",
user="app_user",
password="secret",
connect_timeout=10.0, # 10-second timeout
)
Note
If you run behind firewalls or load balancers, set connect_timeout explicitly and monitor for broker redirection failures.
SSL/TLS¶
import pycubrid
conn = pycubrid.connect(
host="db.example.com",
port=33000,
database="production",
user="app_user",
password="secret",
ssl=True,
)
ssl=Truecreates a verified defaultssl.SSLContextusing system trust roots.- When
ssl=True, pycubrid setsSSLContext.minimum_version = ssl.TLSVersion.TLSv1_2on the default context for both sync and async connections. ssl=your_ssl_contextuses your custom context directly, which is useful for self-signed or private CA certificates.ssl=Noneorssl=Falsedisables TLS and preserves the previous plaintext behavior.
Both pycubrid.connect() and pycubrid.aio.connect() accept the same ssl values:
ssl=Truefor the default verified context with a TLS 1.2 minimum.ssl=Falseorssl=Nonefor plaintext.ssl=your_ssl_contextfor a customssl.SSLContext.
Async TLS uses CUBRID's STARTTLS-style upgrade: the connection opens in plaintext, sends the
CUBRS handshake magic to negotiate TLS with the broker, then upgrades the live transport via
asyncio.AbstractEventLoop.start_tls() (bounded by ssl_handshake_timeout) before the
OPEN_DATABASE exchange. Async shutdown awaits writer.wait_closed() so TLS sessions close
cleanly. The sync driver performs the equivalent flow with ssl.SSLContext.wrap_socket().
connect_timeout bounds only the TCP connect. The broker handshake, the TLS handshake and
OPEN_DATABASE are bounded by read_timeout on both drivers; when read_timeout is unset the
TLS handshake still gives up after 10 seconds on both drivers (ssl_handshake_timeout on async,
a handshake-only socket timeout on sync,
#535), and the broker handshake and
OPEN_DATABASE wait without a limit. The 10-second default covers only the TLS handshake:
requests after it stay unbounded without read_timeout. A broker that stalls or resets the
connection during the TLS handshake raises OperationalError within that bound
(#513).
To allow a TLS handshake longer than the 10-second default, set a larger
read_timeout, for example read_timeout=30.0. This also sets later request
read timeouts to 30 seconds: sync uses a per-receive socket timeout, while async
bounds the complete network round trip. It does not change connect_timeout.
After the session is open, an uncertain transport failure on a request (a socket error, a
timeout, a malformed reply, or an interrupt or task cancellation while a reply is outstanding)
closes the connection and retires every cursor and schema result handle of that session in
both drivers (#556). Rows a cursor already
buffered stay readable; the next fetch that needs the server raises, and no handle of the dead
session is ever sent again. The request is not replayed: reconnect with connect() or
ping(reconnect=True) and re-execute. The async OperationalError message says
read timeout: no complete round trip within read_timeout=...s only when the read_timeout deadline expired;
a timeout raised by the transport itself (for example ETIMEDOUT) is reported as
socket communication timed out, and other socket errors as socket communication failed. The
original exception is always chained as __cause__, and a cancelled task still raises
asyncio.CancelledError. The sync read_timeout is a per-receive socket timeout and is reported
as socket communication failed. An OSError (including TimeoutError) raised by a
json_deserializer callback after the whole reply was read is not a transport failure: it
propagates unchanged and the connection stays open. A ValueError-family error from a custom
deserializer (for example an orjson or simplejson decode error) is still treated as a malformed
reply: OperationalError('malformed response from broker'), and the session is retired.
If CAS closes the transport before a complete reply arrives, sync and async
requests raise OperationalError and retire the physical session. This remains
the error contract for a dead CAS, including the CUBRID crash tracked by
#614. It does not establish
why CAS died: a server-reported SQL error in a complete reply can leave the
session usable. Explicit recovery opens a new session; it does not restore the
failed statement's transaction or result set, or replay that statement. Check
the operation's outcome before deciding whether to retry it. The upstream crash
report is tracked separately in
#675.
import pycubrid.aio
conn = await pycubrid.aio.connect(
host="db.example.com",
port=33000,
database="production",
user="app_user",
password="secret",
ssl=True,
)
Note
CUBRID broker TLS must be enabled on the server side (SSL=ON in cubrid_broker.conf) before TLS connections can succeed.
Async Health Checks¶
Async connections expose the same lightweight native health check as sync connections:
import pycubrid.aio
conn = await pycubrid.aio.connect(database="testdb")
alive = await conn.ping(reconnect=False)
if not alive:
await conn.ping(reconnect=True)
await conn.ping(reconnect=False)issues a nativeCHECK_CASround-trip on an open socket without reconnecting.CAS_INFO[0]=0means OUT_TRAN after a transaction boundary, not a released session; it does not change this behavior. A closed socket or failed check returnsFalse, which makes this suitable for SQLAlchemy'spool_pre_ping.await conn.ping(reconnect=True)probes the existing socket first and attempts one reconnect when already disconnected, after aCHECK_CAStransport/protocol error, or whenCHECK_CASreturns a negative code indicating a broken CAS-to-DB link. Failed recovery returnsFalse;reconnect=Falsereports the negative response asFalsewithout reconnecting and closes that broken session.- A healthy same-session ping does not re-probe escape mode. On a new physical session, an automatic mode is re-probed before use; an explicit
no_backslash_escapes=TrueorFalseremains pinned. Probe failure retires the replacement and makes ping returnFalse, without guessing an escape mode or replaying SQL. - The healthy-session check uses the same native
CHECK_CASfunction code (FC=32) as syncConnection.ping()and executes no SQL; recovery may run a read-only escape-mode probe.
Context Manager Protocol¶
pycubrid connections support the with statement for automatic resource management:
import pycubrid
with pycubrid.connect(
host="localhost",
port=33000,
database="testdb",
user="dba",
) as conn:
cur = conn.cursor()
cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
# Connection commits automatically on success
# Connection is closed automatically after exiting the block
Behavior¶
| Scenario | Action |
|---|---|
| No exception | conn.commit() then conn.close() |
| Exception raised | conn.rollback() then conn.close() |
| Exception raised after the session was retired | conn.close() only |
The __enter__ method returns the connection itself. The __exit__ method:
- Commits the transaction if no exception occurred; a commit failure propagates
- Rolls back the transaction if an exception was raised
- Always closes the connection
An exception raised in the block always propagates unchanged. When it already
retired the physical session (a transport failure, or a cancelled
AsyncConnection request), there is no transaction left: no rollback,
reconnect or statement replay is attempted, and the caller gets the original
OperationalError (or asyncio.CancelledError; the task finishes cancelled)
instead of InterfaceError('connection is closed'). If the rollback itself
fails, that failure is logged as a pycubrid warning and named in a note on the
original exception, which still propagates. AsyncConnection's async with
follows the same rules; a new cancellation during its cleanup propagates as
cancellation.
Manual Transaction Control¶
If you need explicit control, manage transactions directly:
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
try:
cur = conn.cursor()
cur.execute("INSERT INTO cookbook_logs (msg) VALUES (?)", ("event",))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
Autocommit Mode¶
The autocommit property controls whether each statement is committed automatically.
# Check current mode
print(conn.autocommit) # False (driver default)
# Disable autocommit for transaction grouping
conn.autocommit = False
# Re-enable autocommit
conn.autocommit = True
Details¶
| Property | Description |
|---|---|
| Default value | False |
| Getter | Returns current autocommit state |
Setter (= True) |
Sends SetDbParameterPacket + CommitPacket to server |
Setter (= False) |
Sends SetDbParameterPacket + CommitPacket to server |
| CAS recycled between the two | The new value is restored on the replacement session before its COMMIT; at most one reconnect per call (#551) |
COMMIT fails |
Connection closed, previous value kept, OperationalError raised |
Note: When using pycubrid with SQLAlchemy (
cubrid+pycubrid://), the dialect setsautocommit = Falseon each new connection so SQLAlchemy can manage transactions properly.pycubrid's
Connectiondefaults toautocommit=Falsefor explicit transaction control and sends this value as the per-statementauto_commitflag on everyPrepareAndExecutepacket, so the broker's ownCUBRID_AUTO_COMMITsetting is effectively overridden by what the driver reports. Passautocommit=Truetoconnect()(or setconnection.autocommit = Trueafter connecting) to enable.
CAS recycled at a transaction boundary¶
CAS_INFO[0]=0 denotes OUT_TRAN, not a released CAS worker. Normal commit,
rollback, and autocommit requests keep the same socket and CAS session, so
session variables and SET TRANSACTION ISOLATION LEVEL survive them (#468).
The CAS may still close the socket right after such a reply: when its memory
exceeds APPL_SERVER_MAX_SIZE it restarts at the next END_TRAN, cubrid broker
reset recycles idle workers, and with KEEP_CONNECTION=AUTO an idle worker is
handed to a waiting client (CHANGE CLIENT) when more clients than
MAX_NUM_APPL_SERVER are connected. Therefore, when the last reply was OUT_TRAN,
sync and async connections send one native CHECK_CAS before the next request,
as the CUBRID JDBC driver does (#485):
- The CAS answers: the same session is kept and the request is sent. This costs one extra round trip per request issued out of transaction, including every statement in autocommit mode (an autocommit INSERT also probes before its last-insert-id lookup).
- The probe fails: the connection is replaced once for that request. The
escape mode is re-probed (unless pinned) and an explicitly set
autocommitis restored, then the request is sent for the first time on the new session. No SQL is replayed; the CAS reported no open transaction, so no uncommitted work is lost. Requests that depend on the lost session are not sent there: aCLOSE_REQfor one of its handles is skipped; a FETCH of its unread rows, a last-insert-id lookup, or a LOB read/write on a LOB obtained from it raisesOperationalError; andpycubrid.compat.nativeprepared statements are rejected as belonging to an earlier session. If the CAS is recycled right after an autocommit INSERT, that INSERT is committed but itslastrowidisNone; a WARNING is logged. Because the replacement setup itself ends out of transaction, it is verified with one moreCHECK_CASbefore the request. Cursors run this check before rendering parameters, so parameterized SQL is bound for the session it is sent on. SQL already rendered for a session that was then replaced is never sent: it fails with the retryableOperationalError("... parameter binding; retry operation") and the new session stays open for the retry. - The replacement fails:
OperationalErroris raised and the connection is left disconnected (not closed);ping(reconnect=True)reconnects it, or open a new connection.
Only driver-owned state is restored. A real reconnect resets session state set
through SQL: session variables, SET TRANSACTION ISOLATION LEVEL, lock timeouts
and other server session state belong to the lost CAS session and are not
carried over. Layers that apply such settings with SQL must keep re-applying
them on a new physical session (for example sqlalchemy-cubrid's isolation-level
re-apply, sqlalchemy-cubrid#527).
commit() and rollback() first verify an out-of-transaction CAS, then send
CLOSE_REQ for every query handle still held by an unclosed cursor before
END_TRAN, so a long-lived session does not accumulate server handles. Rows
that were already received stay readable; an unfinished result still raises
InterfaceError at its next required FETCH. In autocommit mode no END_TRAN is
sent, so unclosed cursors still accumulate server handles until commit(),
rollback() or close(): close cursors (or use them as context managers).
For handles queued by closed or collected cursors, a boundary transfers each
current-session ID out of the queue only when its CLOSE_REQ is about to be
sent. If commit()/rollback() is interrupted before the next send, unsent
IDs remain queued in FIFO order on that same live physical session; IDs whose
send began are not replayed. An uncertain transport retires the session and
its queued IDs instead of carrying them to a replacement CAS. A cursor
collected during a flush is queued behind the batch already in progress.
Session-state restoration after explicit ping recovery¶
pycubrid never replays an arbitrary SQL request after a transport failure. If a connection is already
disconnected, a CHECK_CAS probe raises a transport/protocol error, or the
probe returns a negative response (broken CAS-to-DB link), explicit
ping(reconnect=True) can attempt one new connection. With reconnect=False,
the negative response returns False without reconnecting and closes that
broken session, in both drivers; later calls raise InterfaceError until
connect() or ping(reconnect=True). The caller must
decide whether interrupted SQL is safe to retry.
Automatic no_backslash_escapes detection runs again on the replacement
physical session before state restoration; an explicitly selected mode remains
unchanged. If this probe fails, the session is retired and ping() returns
False. No interrupted SQL is replayed. Sync and async parameterized SQL bound
before a session replacement, including one made by that request's own
CHECK_CAS, is rejected before send because its generation changed; the caller
decides whether to retry. A sync Lob of the earlier session is refused the
same way: its read() and write() raise before send (#932). A healthy same-session ping does not
probe. This does not claim a dynamic per-session setting toggle or verified
heterogeneous failover.
After successful recovery, including the automatic reconnect above, and when
connect() reopens a connection after close() (sync and async alike, #520),
pycubrid restores the session-level setting the caller has explicitly set:
| Setting | Restored after successful ping recovery? |
|---|---|
autocommit (set via connect(autocommit=True), connection.autocommit = ..., or await conn.set_autocommit(...)) |
Yes — the same value is re-emitted via SetDbParameterPacket |
autocommit left at the connect-time default |
No — the broker default is used |
Settings the caller has never touched are intentionally not
re-emitted on the new connection to avoid spurious round-trips. If the restore
itself fails, the connection is torn down and the underlying transport
error is preserved via PEP 3134 __cause__ so callers can diagnose
the failure.
Connection Methods¶
| Method | Return Type | Description |
|---|---|---|
cursor() |
Cursor |
Create a new cursor for executing SQL |
commit() |
None |
Commit the current transaction |
rollback() |
None |
Roll back the current transaction |
close() |
None |
Close the connection and free resources |
get_server_version() |
str |
Return the CUBRID server version string |
get_last_insert_id() |
str \| None |
Return the cached broker identity, or None |
create_lob(lob_type) |
Lob |
Create a new LOB object (CLOB=24, BLOB=23) |
get_schema_info(schema_type, ...) |
GetSchemaPacket |
Query schema metadata from the server |
fetch_schema_info(packet) |
list[tuple] |
Eagerly read rows and release the original schema handle |
close_schema_info(packet) |
None |
Abandon an owned result; repeat close is a no-op |
get_last_insert_id() reads the identity captured by a cursor after INSERT without
another network request. Successful values remain strings; unavailable values are
None rather than the previous ambiguous "". Use value is None to detect
unavailability and guard int(value) accordingly. The cursor's lastrowid remains
an independent int | None snapshot.
Commit/rollback and SELECT preserve the cached observation. A new INSERT attempt,
nonempty batch, or physical connection discard/reconnect clears it; failed, empty,
or malformed identity retrieval leaves it None. The broker may retain an earlier
identity after a non-auto-increment INSERT, so a reported ID does not prove the
latest statement generated it or that a row exists after rollback. Sync and async
connections follow the same contract. A normal transaction boundary retains the
same connection and cache. An explicit ping(reconnect=True) recovery after an
actual connection failure clears the connection cache; the earlier cursor's
lastrowid snapshot still remains available.
The cache only refreshes for cursor operations with an INSERT server response.
Unlike the previous live broker query, it does not observe CALL, stored-procedure
INSERTs, or out-of-band SQL. Return the procedure's identity explicitly or query it
using the procedure's server-side contract. An empty batch clears the cursor's
lastrowid while retaining the connection cache; a failed prior-query close before
a nonempty batch starts preserves both previous identity values.
LOB Creation¶
# Create a CLOB (Character Large Object)
clob = conn.create_lob(24) # 24 = CLOB
clob.write(b"Large text content...")
# Create a BLOB (Binary Large Object)
blob = conn.create_lob(23) # 23 = BLOB
blob.write(b"\x89PNG\r\n...")
Tip:
Lob.write()acceptsbytesonly. For ordinary CLOB inserts, prefer direct SQL parameter binding withstr; for BLOB inserts, passbytesdirectly. See Examples for details.
Schema Information¶
# Get schema information (schema_type constants from CUBRID docs)
packet = conn.get_schema_info(1, "my_table", 0) # Exact CLASS filter
try:
print(conn.fetch_schema_info(packet))
finally:
conn.close_schema_info(packet)
Schema packets belong to the original connection/session; fetch before a
transaction boundary (including cursor work and version lookup when autocommit
is enabled) or explicitly abandon them. Async uses the same methods with
await. See API Reference
for the second filter, four-field columns and cleanup/error contract.
Broker Handshake¶
For what the Broker and the CAS process are, and how later execute(), fetch and
commit calls map to the protocol, see CAS Architecture and Protocol Concepts.
When pycubrid.connect() is called, the following protocol handshake occurs:
sequenceDiagram
participant Client
participant Broker as Broker (port 33000)
participant CAS
Client->>Broker: TCP connect
alt ssl requested
Client->>Broker: ClientInfoExchangePacket (CUBRS)
else plaintext
Client->>Broker: ClientInfoExchangePacket (CUBRK)
end
Broker-->>Client: status int32 (0 ok / >0 redirect / <0 error)
opt redirected (status > 0)
Client->>CAS: Reconnect to new port (no second handshake)
end
opt ssl requested
Client->>CAS: TLS upgrade (start_tls / wrap_socket)
end
Client->>CAS: OpenDatabasePacket
CAS-->>Client: Session ID
sequenceDiagram
autonumber
participant App as Python App
participant Driver as pycubrid.Connection
participant Broker as Broker:33000
participant CAS as CAS Worker
App->>Driver: pycubrid.connect(..., ssl=...)
Driver->>Broker: TCP connect
alt ssl truthy
Driver->>Broker: ClientInfoExchangePacket("CUBRS")
else ssl falsy
Driver->>Broker: ClientInfoExchangePacket("CUBRK")
end
Broker-->>Driver: status int32
alt status < 0
Driver-->>App: OperationalError (fail-fast)
else status > 0 (redirected)
Driver->>CAS: TCP reconnect to redirected port (skip rehandshake)
else status == 0 (direct mode)
Driver->>Broker: reuse existing socket
end
opt ssl truthy
Driver->>CAS: TLS upgrade via start_tls() / wrap_socket()
end
Driver->>CAS: OpenDatabasePacket(database, user, password)
CAS-->>Driver: cas_info + response_code + broker_info + session_id
Driver-->>App: connected Connection object
Danger
If broker redirection returns a CAS port not reachable from your client network, connection succeeds at step 1 but fails before session establishment.
Step-by-Step¶
- TCP Connect — Open a socket to the broker (default port 33000).
- Client Info Exchange — Send a 10-byte handshake: magic string
b"CUBRS"whensslis requested (STARTTLS) orb"CUBRK"for plaintext, plus client typeCLIENT_JDBC=3and protocol-version bytes. - Broker Status — The broker responds with a 4-byte big-endian signed integer:
status < 0— fail-fast: driver raisesOperationalErrorstatus > 0— redirect: close socket, reconnect to the new CAS port on the same host, without repeating the handshake (mirrors the official JDBCBrokerHandlerbehavior)status == 0— direct mode: reuse the existing socket
- TLS Upgrade (optional) — If
sslwas truthy, upgrade the live transport to TLS before anyOPEN_DATABASEbytes are written. Async usesasyncio.AbstractEventLoop.start_tls(..., ssl_handshake_timeout=...); sync usesssl.SSLContext.wrap_socket(). Failed handshakes abort the transport rather than leaking it. - Open Database — Send database name, username, and password via
OpenDatabasePacket. - Session Established — Server returns a session ID; the connection is ready.
Server Version Detection¶
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
version = conn.get_server_version()
print(version) # e.g., "11.2.0.0374"
conn.close()
The get_server_version() method sends a GetEngineVersionPacket to the server and returns the version as a string.
Troubleshooting¶
Common Connection Errors¶
ConnectionRefusedError on port 33000¶
The CUBRID broker is not running or not listening on the expected port.
- Verify the broker is running:
- Check the broker port in
cubrid_broker.conf(default: 33000) - If using Docker:
Wrong password or unknown user¶
A wrong password raises OperationalError with errno=-171 ("Incorrect or missing
password.") and SQLSTATE 28000, and an unknown user errno=-165 with the same
class and SQLSTATE (#903; DatabaseError / HY000 before). error_source is
"server". CUBRID's default dba user has no password. If you set one, ensure it matches:
# If dba has no password
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
# If dba has a password
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba", password="mypassword")
TimeoutError or socket.timeout¶
The server did not respond within the timeout period:
# Increase timeout
conn = pycubrid.connect(
host="slow-server.example.com",
port=33000,
database="testdb",
user="dba",
connect_timeout=30.0,
)
OperationalError: Connection is closed¶
The connection was closed by the server (session timeout, network interruption, or broker restart). Create a new connection:
Docker Quick Start¶
For local development, use the provided docker-compose.yml:
# Start CUBRID 11.2 (default)
docker compose up -d
# Start a specific version
CUBRID_VERSION=11.4 docker compose up -d
# Verify it's running
docker compose ps
# Connect with pycubrid
python3 -c "
import pycubrid
with pycubrid.connect(host='localhost', port=33000, database='testdb', user='dba') as conn:
cur = conn.cursor()
cur.execute('SELECT 1 + 1')
print(cur.fetchone())
"
# Stop and clean up
docker compose down -v
SQLAlchemy Integration¶
pycubrid works as a driver for sqlalchemy-cubrid:
from sqlalchemy import create_engine, text
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")
with engine.connect() as conn:
result = conn.execute(text("SELECT 1"))
print(result.scalar())
SQLAlchemy features — ORM, Core, Alembic migrations, schema reflection — are accessible through the pycubrid driver when used with sqlalchemy-cubrid.
Connection Pooling¶
pycubrid does not include a built-in connection pool. Each pycubrid.connect() call creates a new TCP connection to the CUBRID broker.
For connection pooling, use one of:
-
SQLAlchemy's built-in pool (recommended):
-
External pooling libraries (e.g.,
sqlalchemy.pool,DBUtils)
See Troubleshooting for pool tuning guidance.
Character Encoding¶
The charset connection option (default "utf-8") selects the Python codec
for the text pycubrid exchanges with the broker. Set it to the charset the
database was created with (#86):
import pycubrid
import pycubrid.aio
conn = pycubrid.connect(database="kodb", charset="euckr")
aconn = await pycubrid.aio.connect(database="kodb", charset="euckr")
Accepted values. Any Python codec name, plus the CUBRID spellings utf8,
euckr and iso88591, plus ksc5601, which pycubrid always maps to EUC-KR (Python
3.15 maps its own ksc5601 alias and the other KS C 5601 aliases, such as
ks_c_5601 and korean, to CP949, which is rejected below), and
a CUBRID locale as given to createdb, such as "ko_KR.euckr", whose part
after the dot is used. None means the default "utf-8". The name is
normalized to the Python codec name ("euckr" becomes "euc_kr"). The option
is validated before any socket work:
- a non-string raises
TypeError; - an unknown codec, CUBRID's
binarycharset (it has no text codec) and any codec that is not ASCII-transparent raiseValueError. Rejected codecs include UTF-16/32, UTF-7,utf-8-sig, Shift_JIS, Big5, GBK, GB18030, CP949, Johab and the ISO-2022 family: SQL quoting and escaping run onstrbefore encoding, so a codec that can emit an ASCII byte such as'or\inside a multibyte character is unsafe; - a
database,userorpasswordthat the codec cannot encode raisesDataError.
The codec is kept for the life of the connection, including the reconnect done
by ping(reconnect=True) and the CHECK_CAS recovery.
What uses the connection charset:
| Direction | Text | Behavior |
|---|---|---|
| Sent | SQL text, including parameters rendered into it (JSON parameters too); executemany batch SQL; schema-info arguments; compat.native prepared SQL and string bindings |
Encoded before any byte of the request is sent. An unencodable character raises DataError naming the codec and character position (the text itself is not echoed); nothing of that request is sent and the session stays usable. With euc_kr, Hangul syllables outside KS X 1001 (such as 똠 or 뷁) are unencodable: Python would send them as 8-byte makeup sequences that CUBRID stores as separate jamo. On read, the Hangul filler U+3164 and following jamo decode as separate characters, as CUBRID stores them. |
| Sent | OPEN_DATABASE database, user and password |
Encoded, then cut to the 32-byte field on a character boundary. |
| Received | CHAR, VARCHAR, STRING, NCHAR, NCHAR VARYING and ENUM values, collection elements (decode_collections=True) |
Strict decode. Undecodable bytes raise DataError (for example column value is not valid euc_kr (invalid byte at offset 0)); the session stays usable. |
| Received | Column, table and alias names; column default values | Strict decode; DataError (column metadata is not valid ...). Ordinary cursors keep the session and release the server handle; get_schema_info() and the compat.native prepared cursor still retire the session, as for any unparsable reply there. |
| Received | Server error messages, including per-statement batch errors | Decoded with errors="replace", so the native error always surfaces. |
| Received | LOB file locators (file_locator; the server path embeds the table name) |
Decoded with errors="replace": informational only, the packed handle is what goes back to the server. |
What does not use it: fetched JSON values are always UTF-8 (the broker
sends JSON as UTF-8 whatever the database charset), although a JSON parameter
is SQL text and is encoded with the connection codec, so an emoji in JSON
raises DataError on insert under euckr. NUMERIC text, timezone names and
the server version string are protocol text and stay UTF-8, and
pycubrid.Binary(str) always encodes as UTF-8. LOB
contents are raw bytes: Lob.read() on a CLOB returns the bytes in the
column charset (EUC-KR bytes in an EUC-KR database), which the application
decodes itself.
No negotiation on the wire. The CAS protocol carries no client charset and the broker does no conversion: the server interprets the bytes it receives in the database charset, and the broker sends each value in its own column's charset. Consequences:
- A default (
utf-8) client on an EUC-KR database cannot read EUC-KR text: it raisesDataErrorinstead of returning wrong characters, and before #86 its writes silently stored mojibake. Connect withcharset="euckr". - A column declared
CHARSET utf8inside an EUC-KR database arrives as UTF-8 and raisesDataErrorundercharset="euckr". Convert it in SQL, for exampleSELECT CAST(u AS VARCHAR(10) CHARSET euckr) FROM t, or readhex(u). - Only one codec applies per connection.
Note
In an EUC-KR database, CUBRID's lexer accepts non-ASCII identifiers only when
quoted ([표] or "표"), and CHAR(n) pads with the full-width space
U+3000. Both are server behaviors, not driver ones.
JDBC difference
The CUBRID JDBC driver exposes a charSet connection-URL property for the
same purpose. pycubrid additionally refuses codecs that are not
ASCII-transparent and always treats JSON as UTF-8.
See also: Type System · API Reference · Examples