Skip to content

Latest commit

 

History

History
2688 lines (2161 loc) · 121 KB

API_REFERENCE.md

File metadata and controls

2688 lines (2161 loc) · 121 KB

API Reference

Complete API documentation for pycubrid — a pure Python DB-API 2.0 driver for CUBRID.


Table of Contents


Module-Level Attributes

These attributes are defined at the module level as required by PEP 249.

Attribute Value Description
apilevel "2.0" DB-API specification version
threadsafety 1 Threads may share the module but not connections
paramstyle "qmark" Question mark parameter style: WHERE name = ?
__version__ "1.8.0" Package version string
import pycubrid

print(pycubrid.apilevel)      # "2.0"
print(pycubrid.threadsafety)  # 1
print(pycubrid.paramstyle)    # "qmark"
print(pycubrid.__version__)   # "1.8.0"

Module-Level Constructor

pycubrid.connect()

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

Create a new database 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
user str "dba" Database user
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" Codec for SQL text, credentials, character values, names and error text; see charset
ssl bool | ssl_module.SSLContext | None None Opt-in TLS for sync and async broker connections; True uses the default verified context with a TLS 1.2 minimum. Connection uses CUBRID's STARTTLS-style upgrade — plaintext CUBRS handshake then TLS upgrade before OPEN_DATABASE. See Connection guide.
**kwargs Any — Additional parameters such as connect_timeout, read_timeout, fetch_size, enable_timing, no_backslash_escapes, and autocommit. An unrecognised keyword is ignored but reported — see Unknown Options

Unknown Options

A keyword that is not a supported connection option is ignored, but emits an UnknownConnectionOptionWarning naming it (with a spelling suggestion when one is close), so a typo such as read_timout=30 is not swallowed silently. The same applies to pycubrid.aio.connect() and to direct Connection(...) / AsyncConnection(...) construction. See Unknown Options for how to escalate the warning to an error or silence it.

decode_collections

When False (the default), collection columns are returned as raw CAS wire bytes for backward compatibility. When True, pycubrid decodes supported SET, MULTISET, and SEQUENCE payloads into Python containers.

JSON Columns

json_deserializer controls JSON column decoding on fetch:

Setting Result
None (default) Return JSON columns as str
callable Pass the raw JSON string to the callable and return its result

charset

charset (default "utf-8") is the Python codec for SQL text (including rendered parameters), batch and schema-info arguments, OPEN_DATABASE credentials, character values (CHAR, VARCHAR, STRING, NCHAR, NCHAR VARYING, ENUM, collection elements), column/table names, defaults and server error text. Set it to the database charset, for example charset="euckr" for a database created with ko_KR.euckr (#86).

  • Accepts Python codec names, the CUBRID names utf8, euckr and iso88591, and a CUBRID locale such as "ko_KR.euckr" (the part after the dot is used), normalized to the Python codec name ("euc_kr"); None means the default. Validated before any socket work: a non-string raises TypeError; an unknown codec, CUBRID binary and codecs that are not ASCII-transparent (UTF-16/32, UTF-7, Shift_JIS, Big5, GBK, GB18030, CP949, ISO-2022, ...) raise ValueError; credentials the codec cannot encode raise DataError.
  • Text that cannot be encoded raises DataError before any byte of that request is sent, on every path (ordinary and compat.native cursors, get_schema_info()), and the session stays usable. With euc_kr, Hangul syllables outside KS X 1001 (such as 똠 or 뷁), which Python would encode as 8-byte makeup sequences, count as unencodable.
  • Bytes that cannot be decoded raise DataError naming the codec. Ordinary cursors keep the session; get_schema_info() retires the connection on any FC9 reply it cannot parse, and the explicit prepared API (pycubrid.compat.native) retires its session and raises OperationalError. Error text and LOB file locators are decoded with errors="replace".
  • JSON values are read back as UTF-8, but a JSON parameter is SQL text and is encoded with the connection codec (an emoji in JSON under euckr raises DataError on insert). NUMERIC, timezone names, the version string and LOB contents are not affected (CLOB bytes are in the column charset); pycubrid.Binary(str) always encodes as UTF-8.
  • The broker does no conversion, so a CHARSET utf8 column in an EUC-KR database raises DataError under charset="euckr"; convert it in SQL with CAST(col AS VARCHAR(n) CHARSET euckr).

See Character Encoding for the full contract.

Returns: A new Connection instance.

Raises: OperationalError if the connection cannot be established.

import pycubrid

# Minimal connection
conn = pycubrid.connect(database="testdb")

# Full connection with timeout
conn = pycubrid.connect(
    host="192.168.1.100",
    port=33000,
    database="production",
    user="app_user",
    password="secret",
    connect_timeout=5.0,
)

Explicit Native Compatibility Subset

The opt-in pycubrid.compat.native module uses the pure-Python sync transport. It now provides a bounded sync-only prepared cursor for scalar values (None, bool, int, float, Decimal, str, bytes/bytearray, date, time and datetime, typed since #840; see Prepared host-variable mapping), SET/MULTISET/SEQUENCE collection values and fetched BLOB/CLOB handles; strings use the connection charset. This does not change ordinary pycubrid.connect() or pycubrid.aio execution: their execute() methods still send complete SQL through FC41. The wrapper pycubrid.compat.cubriddb offers construction, close, explicit commit/rollback, server version/query ping, autocommit access and qualified row cursors but no DB-API globals, a thread-sharing guarantee or complete native C-extension parity.

native.connect(url, user="public", passwd="", *, charset="utf-8") returns a native.connection. The wrapper cubriddb.Connect/connect/connection(*args, **kwargs) factories return cubriddb.Connection(dsn="", user="public", password="", charset="utf8"); the wrapper's .connection property is the same native-style object that owns the single underlying transport. Factory positionals override the matching dsn/user/password keywords, up to three. Both surfaces start with server-backed autocommit enabled and preserve the ordinary driver's separate dba/manual-commit defaults.

Use CUBRID:host:port:database:user:password: with the final colon. Python arguments, including omitted public/empty defaults, take precedence over credentials embedded in that DSN. The wrapper's charset (default "utf8") is passed to the driver's charset option, so CUBRID names such as "euckr" work and an invalid codec fails before connection work. Only the plain CUBRID backend is accepted; alternate backends, HA/TLS URL options and excess arguments fail before connection work. Errors never echo the raw credential-bearing DSN.

The native connection adds cursor(), set(), lob(), commit(), rollback(), set_autocommit(bool), set_isolation_level(level) and close(). Connection utilities are described below. Its cursor supports prepare(sql), one-based bind_param(index, value, bind_type=0), bind_set(index, s), bind_lob(index, lob), execute(option=0, max_col_size=0) -> int, tuple-only fetch_row(how=0), fetch_lob(col, lob), result_info([n]), data_seek(n), row_seek(offset), row_tell() and close(). bind_param() sends bool as INT 1/0, an int as INT, BIGINT or (past 64 bits, up to 38 digits) NUMERIC, float as DOUBLE, Decimal as NUMERIC text, date/time/datetime as DATE, TIME and DATETIME (DATETIMETZ when the datetime is aware) and bytes/bytearray as VARBIT. nan/inf, forged subclasses and other objects raise ProgrammingError; values needing more than 38 digits raise DataError; empty bytes raise NotSupportedError, because the broker would read them as NULL. Server-side host-variable coercion can differ from the literal path (for example, a float is DOUBLE rather than a NUMERIC literal); the differences are listed in the mapping linked above. Nondefault flags and other Python value types fail before a prepared execute. Preparation requires the current broker session to advertise statement pooling; pooling-off or unknown is rejected before FC2. Prepared handles are owned by one physical CAS session, never replayed after reconnect, and do not turn a failed transaction into a retry. commit() preserves an active HOLDABLE SELECT result; rollback() invalidates it, including buffered rows. Broker-originated prepared errors retain their DB-API class, code, errno and SQLSTATE, but their text is redacted because the broker may echo SQL or values. After a complete broker execute error on a non-LOB prepared statement, that call raises its original redacted error and sends no second execute. The next explicit user execute() closes the old handle, prepares the same SQL on the same physical session, restores its already-bound scalar/collection values if the parameter count still matches, then sends one execute for that new call. This corrects repeated conversion errors without replaying a possibly effective statement inside the failing call. It is deliberately not CCI's same-call invalid-plan retry. A transport failure, a changed session, a failed close/prepare or a changed parameter count fails closed; LOB-bound statements require explicit prepare() and rebind after an error because temporary LOB handles may have been consumed. No ordinary FC41 or async behavior changes. Unlike the pinned official native extension, which raises SystemError on bind_param(None), this subset binds SQL NULL explicitly; this is a documented safety deviation rather than an exact native-NULL parity claim. The initial public connection still starts with autocommit enabled. Do not treat this subset as a general DB-API cursor or a public async prepared API. See the typed CAS contract and compatibility guide for the supported boundary.

from pycubrid.compat import native

conn = native.connect("CUBRID:localhost:33000:testdb:::", "dba", "")
try:
    cur = conn.cursor()
    try:
        cur.prepare("SELECT CAST(? AS INTEGER)")
        cur.bind_param(1, 42)
        cur.execute()
        assert cur.fetch_row() == (42,)
    finally:
        cur.close()
finally:
    conn.close()

Native cursor positioning (data_seek, row_seek, row_tell)

These sync native-only operations act on a valid current SELECT result and return no rows themselves. data_seek(n) selects an absolute one-based row and sets both positions to n. row_seek(offset) moves relative to the physical next row; both seeks return None. row_tell() returns the separate official shadow counter: it starts at zero, advances after successful fetches, and is preserved by a same-handle re-execute even though the physical next row returns to row1. A new prepare() resets it. It is therefore not always the next row's absolute position.

An absolute index outside1..N raises InterfaceError(code=-30006) without moving. A relative move outside the result clamps the physical position to before-first or after-end and raises code-20005, preserving the shadow counter; a subsequent fetch returns None until a valid seek recovers it. row_tell() raises code-30012 when the shadow exceeds N. Normal draining ends at tellN, but seeking to1 before draining ends at shadowN+1 and that tell error, matching the safe pinned source.

Use explicit conn.set_autocommit(False) before preparing/executing when broker-backed cross-page movement is needed, while the same owner/result remains live. The factory's autocommitTrue default is unchanged: once its broker delivers the final batch, the result can be released even if the client has not consumed all buffered rows. A local seek may then succeed, but a later out-of-page FETCH fails. The pinned official default-mode diagnosis also observed this limit; there is no unconditional default-mode, post-boundary or lost-session scroll guarantee. Cached-page access is not evidence that the broker still owns a result.

Seeks only change local positions. Fetching outside the one retained response page uses the existing absolute FC8 selector without reprepare, execute or reconnect. Retained-page and streaming-memory observations are candidate-only; the C extension exposes no public page metric. Requested fetch size is not a broker row cap. fetchall() naturally returns a caller-owned O(N) list, not a seek cache.

data_seek/row_seek accept exactly one positional C-int32/index-like value and row_tell accepts none; keywords are rejected. Closed state precedes positional conversion; errors retain pycubrid message-only args and numeric .code. Fresh/non-SELECT/rollback/disconnected/foreign/stale results fail safely. Selector __index__ callbacks that change owner/result/position and undefined C signed-addition overflow are rejected rather than reproduced. Metadata/description and ordinary/async cursors are unchanged. Existing wrappers fetch through their native _cs; no public wrapper forwarding methods are added.

Extended column metadata (result_info)

native.cursor.result_info() reads the last successful prepared execution's cached metadata without a server request or row movement. Omitted n or 0 returns a tuple of all column tuples; a one-based selector returns a one-item outer tuple. A SELECT with zero rows still has metadata. Successfully executed zero-column DML returns None, even for a negative or large in-range selector. Integer conversion happens first: bool and __index__ are accepted, while None, strings, floats and __int__ alone are not; signed C-int32 overflow raises OverflowError. Only zero/one positional argument is accepted.

Each column tuple contains exactly these 15 fields, in the implemented official order, not its differing docstring order:

(cci_type, not_null, scale, precision, name, real_attribute, class_name,
 default_value, auto_increment, unique_key, primary_key, foreign_key,
 reverse_index, reverse_unique, shared)

Types and flags are integers, not booleans. cci_type is the measured extended CCI type, not the normalized ordinary DB-API type: owned 10.2/11.4 observations include 40/72/104 for INTEGER SET/MULTISET/SEQUENCE and 130 for JSON. Attribute, class and default strings are returned as actually supplied; absent textual metadata is "", not None. No attribute name is guessed from an alias, and a default string such as "NULL" is not converted into a Python value.

Fresh and prepare-only cursors or invalid indices raise InterfaceError with .code == -30006; a closed cursor has .code == -30019 and is checked before positional arity/conversion. Keywords raise TypeError before the body, including on a closed cursor. These local errors retain pycubrid's message-only args, not the native extension's (code, message) pair; InterfaceError has .code, not an invented .errno field.

EOF, same-owner commit and rollback retain cached metadata even when rollback invalidates rows. Local execution preflight errors preserve it; an attempted execute failure hides it until another execution succeeds. Closed/disconnected, foreign or stale physical-session owners fail safely, without probing or reconnecting. Ordinary/qualified-wrapper description is unchanged. Text keeps the connection codec; the pinned official UTF-8/Unicode comparisons do not certify non-UTF-8 parity. This is a sync native-only addition, not a schema or positioning API.

Cached settings versus effective setters

The four writable native.connection members autocommit, isolation_level, lock_timeout and max_string_len are cached snapshots. Assigning any Python object to them preserves that object but sends no packet and changes no effective setting. The initial values come from the connection's effective autocommit mode and database-parameter reads for the other three; only a complete server error while reading max_string_len maps to 0. Transport or malformed-reply failures abort construction. As in the pinned official extension, a fresh server at numeric READ COMMITTED level 4 reports "CUBRID_TRAN_UNKNOWN_ISOLATION" in its initial text cache; the effective level remains 4, and calling set_isolation_level(4) repairs the text.

Use conn.set_autocommit(mode, /) with True or False to change the effective mode. It accepts an exact bool, returns None and updates the cache after success. Like CCI, a same-effective-mode call is local, and a mode transition sends a COMMIT only if a transaction is active; the new mode governs subsequent prepared execution. This is not the ordinary Connection.autocommit setter, which retains its existing SET+COMMIT behavior. set_isolation_level(level, /) accepts 4, 5, 6 or the corresponding CUBRIDIsolationLevel members, returns None, and changes the actual session level with SET_DB_PARAMETER without committing the current transaction. A same-effective-level call updates only the symbolic cache; after physical-session replacement it sets the requested level again. Other input types and legacy levels are rejected before I/O rather than reproducing unsafe C-extension parser behavior. There are no effective lock_timeout or max_string_len setters here.

The wrapper's get_autocommit() and .autocommit getter return the native cached member, even if it was overwritten with another object. In contrast, wrapper.set_autocommit(value) and wrapper.autocommit = value require a bool (ValueError otherwise) and delegate the effective native setter. For migration, replace official raw member assignments intended to change behavior with an explicit setter; keep cached reads separate from effective state checks. Ordinary pycubrid.connect() still defaults to manual commit.

from pycubrid.compat import cubriddb, native

conn = native.connect("CUBRID:localhost:33000:testdb:::", "dba", "")
try:
    conn.autocommit = False       # cache only; SQL still autocommits
    conn.set_autocommit(False)   # effective manual transaction mode
    conn.set_isolation_level(5)  # REPEATABLE READ, no implicit commit
    conn.rollback()
finally:
    conn.close()

wrapper = cubriddb.Connect("CUBRID:localhost:33000:testdb:::", "dba", "")
try:
    wrapper.autocommit = False   # effective setter, unlike native raw assignment
finally:
    wrapper.close()

Native connection utilities (#666)

The native connection provides zero-argument server_version() -> str, client_version() -> str and ping() -> int. The wrapper provides only server_version() and ping(), each delegating once to its exact native owner. Invalid positional/keyword arguments fail before connection or network work.

server_version() makes a fresh FC15 request and returns full valid server text; the official extension's fixed C buffer can truncate it. client_version() is this driver's frozen package-version string, needs no I/O and works after close. It is not the official build ID or a promise of a four-component version format. ping() executes the constant select 1+1 from db_root query and returns integer 1 when it finds the expected value, otherwise 0. Query errors propagate; it is not ordinary Connection.ping()'s CHECK_CAS/bool/optional-recovery operation.

Live utilities use the effective autocommit mode, not a writable native cache, and the exact current driver, socket and physical generation. They do not retry, reconnect or explicitly commit/rollback. An active private schema result blocks effective-autocommit utility requests; this is not a public native schema API. Closed or replaced owners fail rather than using another session. Ping validates consumed result pages and closes only a still-owned handle before returning success; transaction-retired handles are not closed again. A cleanup-only error propagates, while secondary cleanup cannot replace an earlier query error. Ordinary/async utilities, defaults and thread-sharing guarantees are unchanged. The scoped comparison case covers healthy native/wrapper calls in default True and explicit manual False modes; client IDs are separate per-driver observations, not a matching-ID, recovery, error-profile or upstream-regex certificate.

Wrapper transaction boundaries (#662)

cubriddb.Connection.commit() -> None and rollback() -> None take no arguments. Each delegates once to the exact owned native connection, returns None and lets its exception propagate without new wrapping. Extra positional/keyword arguments fail before delegation; closed-owner checks remain native-owned. For pending work, first use set_autocommit(False) or .autocommit = False; the compatibility factory's autocommit default is unchanged.

Existing native result callbacks apply only after a successful boundary: commit preserves local result state; rollback invalidates native fetching. Wrapper cursor rowcount and description remain cached execution snapshots, so populated metadata after rollback does not mean the result is fetchable. A failed delegation adds no facade result notification or snapshot reset. These delegates add no connection, SQL, retry or recovery logic; underlying native rules remain, without a universal remote-result lifetime or thread-sharing promise. Ordinary/async APIs, defaults and broader wrapper execution are unchanged.

Qualified wrapper row cursors (#466)

cubriddb.Connection.cursor(dictCursor=None) returns a tuple-row cursor for a falsey argument and a dictionary-row cursor for a truthy argument. Direct construction is available as pycubrid.compat.cursors.Cursor(conn) and DictCursor(conn); these classes are not added to the package-level exports. The cursor starts with arraysize=1, rowcount=-1 and description=None. execute(query, args=None, set_type=None) delegates int, str and None scalars to native bind_param() (since #840 an int past signed 32-bit binds BIGINT or NUMERIC instead of raising DataError), and since #610 binds list/tuple/set/frozenset arguments as collections; executemany(query, args_list) prepares once and executes each group (see Wrapper collection arguments). Mapping arguments and unsupported values fail before execution; LOB arguments and broad DB-API execution are not part of this wrapper slice.

For a SELECT, description contains seven-field tuples (name, native_type, 0, 0, precision, scale, null_ok) with integer null_ok. Dictionary keys preserve the exact column name and case; duplicate names overwrite earlier values without moving their original key position. SQL NULL remains None, distinct from an empty string. The public description is a writable snapshot, but row keys and converter arguments come from the private execution metadata even if a caller overwrites that public attribute. For non-SELECT statements this facade consistently sets description=None; the official extension can instead leave the attribute missing after a reprepare. This is a deliberate safe deviation.

conn.set_fetch_value_converter(func) stores a connection-local value read by existing cursors at each fetch. A truthy converter is called with the already shaped tuple/dictionary and its native-style description; its result is returned unchanged, including falsey values or None. A falsey converter disables conversion; a truthy non-callable raises TypeError after consuming the row. Callback errors also consume that row. Matching the official wrapper, fetchmany() and fetchall() stop after consuming any falsey converted result, while iteration stops only on None; a later fetch may resume after either. Like the official wrapper, fetchmany(size) with size <= 0 returns [] without a warning; this differs on purpose from the native cursor, which deprecates that call. next() is the official wrapper's explicit alias of __next__(). Call close() for deterministic handle release. An abandoned wrapper cursor attempts same-session cleanup during garbage collection without committing or reconnecting, but that cleanup is best-effort, can block on I/O and cannot replace an explicit close.

from pycubrid.compat import cubriddb

conn = cubriddb.connect("CUBRID:localhost:33000:testdb:::", "dba", "")
try:
    cur = conn.cursor(dictCursor=True)
    try:
        cur.execute('SELECT CAST(1 AS INTEGER) AS "ItemID"')
        row = cur.fetchone()  # {"ItemID": 1}
    finally:
        cur.close()
finally:
    conn.close()

Collection binding (set, imports, bind_set)

Since #440, the native subset binds SET, MULTISET and SEQUENCE parameter values with the official names: conn.set() (or native.set(conn)) returns an empty native.set, s.imports(data, type, /, *, kind=SET) gives it a value, and cur.bind_set(index, s) binds that value to a one-based parameter. Nothing is sent until execute(), and the set holds no server resource.

  • data must be a tuple (anything else raises InterfaceError, as in the official driver). Elements are str, None for a NULL element, or, when type is INT, int in signed 64-bit range (others raise DataError); int and digit-string elements may be mixed because both are sent as text. Other element types (bool, float, bytes, nested containers), a NUL in a string and an unencodable string raise ProgrammingError or DataError, and the set keeps its previous value. A str subclass is copied as plain text first.
  • type is a CCI element type code such as CHAR (1), STRING/VARCHAR (2), NUMERIC (7), INT (8) or DATE (13), for example CUBRIDDataType.NUMERIC from pycubrid.constants. As in the official driver, any code is accepted and only labels the import; BIT (5) and VARBIT (6), which the official driver converts to bit strings, raise NotSupportedError, and a non-int code raises InterfaceError.
  • As in the official driver, every element is sent as a STRING (2) element whatever type is, and the server converts it to the column's element type. An int element is sent as its decimal text, so imports((1, 2), INT) sends the same bytes as the official imports(('1', '2'), INT). A value the column cannot hold fails on the server at execute(), and the prepared handle stays usable. Because the elements are strings, an untyped SET column stores them as strings.
  • kind is SET (16, the default), MULTISET (17) or SEQUENCE (18). The default sends exactly the official bytes, so SET semantics apply even for a MULTISET or SEQUENCE column: duplicates are dropped and order is not kept. Pass kind=MULTISET to keep duplicates or kind=SEQUENCE to keep order and duplicates. CUBRID 10.2 and 11.4 brokers reject the MULTISET bind kind (error -454), so kind=MULTISET is sent as SEQUENCE, which the server stores into a MULTISET column with its duplicates.
  • imports() replaces the value. bind_set() binds the value the set has at that moment, so a later imports() does not change an earlier bind. A set that was never imported binds SQL NULL, as in the official driver; bind_param(index, None) also binds SQL NULL.
  • bind_set() raises InterfaceError for anything that is not a native.set, and ProgrammingError for a bad index or a set imported under another charset. Stale-session and closed-cursor rules are the same as for bind_param().

Deliberate differences from the official driver, each pinned by a live differential claim (bind-* in tests/fixtures/official_differential_claims.json): None is the NULL element and the text 'NULL' stays a string (official turns 'NULL' into a NULL element); the empty string and Python int elements are accepted (official raises InterfaceError); an element with a NUL raises ProgrammingError (official silently truncates it); kind is a pycubrid extension (official always binds a SET); and error classes follow the #439 prepared cursor: a float/bytes element and a bad bind_set index raise ProgrammingError (official InterfaceError), native.set() with a non-connection raises InterfaceError (official TypeError), and server error -494 raises ProgrammingError by the driver-wide mapping (official IntegrityError).

As with the official module, from pycubrid.compat.native import * binds the name set to native.set, shadowing the builtin set in that namespace. The wrapper execute(query, args, set_type) and executemany() collection shapes build on this API (next section). Ordinary pycubrid cursors still use the typed pycubrid.types.Set/Multiset/Sequence literal parameters (#567).

from pycubrid.compat import native
from pycubrid.constants import CUBRIDDataType

conn = native.connect("CUBRID:localhost:33000:testdb:::", "dba", "")
try:
    cur = conn.cursor()
    try:
        cur.prepare("INSERT INTO t (tags, scores) VALUES (?, ?)")
        tags = conn.set()
        tags.imports(("a", "b"), CUBRIDDataType.STRING)  # SET(VARCHAR)
        scores = conn.set()
        scores.imports((3, 1, 3), CUBRIDDataType.INT, kind=CUBRIDDataType.MULTISET)
        cur.bind_set(1, tags)
        cur.bind_set(2, scores)
        cur.execute()
    finally:
        cur.close()
finally:
    conn.close()

Wrapper collection arguments (#610) {#wrapper-collection-arguments}

The qualified wrapper cursors accept the official CUBRIDdb collection call shapes (pinned upstream CUBRIDdb/cursors.py at e75ec36). Each collection argument becomes conn.connection.set(), imports(tuple(elements), code) and bind_set(index, s) on the native API above. Every element is sent as STRING text with the SET kind, and the type code only labels the import. For collections whose elements are all supported and not None, the request bytes equal the official driver's; a None element and the cases listed under the differences below are not byte-equivalent.

Item Contract
Call shapes execute(query, args=None, set_type=None) -> int and executemany(query, args_list) -> None
args None, a tuple or list of positional values, or a bare int/str (wrapped). Other top-level values, including a set, raise ProgrammingError
Positional values int (INT32; since #840 BIGINT or NUMERIC past signed 32-bit), str and None (SQL NULL) as before; a list, tuple, set or frozenset is a collection. Other values (bool, float, bytes, dict, generators, other iterables) raise ProgrammingError
set_type None infers every collection. An int code applies to every collection. A list/tuple gives the code by position; a None entry or a position past its end is inferred. It is ignored when no collection is passed. A used code that is not an int (or is a bool) raises InterfaceError
Inferred codes int and bool INT (8), float FLOAT (11), Decimal NUMERIC (7), date and datetime DATE (13), time TIME (14), str STRING (2). None elements are skipped; an empty or all-None collection is STRING
Elements None is a NULL element; str is sent as is; other supported elements are sent as str(element) (True becomes 'True', a datetime its ISO text) and the server converts the text to the column type
Errors Mixed inferred categories raise TypeError, as upstream. bytes/bytearray elements and the BIT (5)/VARBIT (6) codes raise NotSupportedError. Other element types (nested containers, objects) raise ProgrammingError; NUL and charset errors follow imports()
executemany Checks every group (inferred types, no set_type) and builds its collections, then prepares once. Every group's value count must equal the placeholder count, else ProgrammingError and no group runs. Then each group is bound and executed in order. rowcount and description come from the last execution. An empty args_list only prepares and leaves rowcount=-1, description=None; this is pycubrid's own choice, not compared with the official wrapper
Timing and state Argument, type and collection errors of execute() and of every executemany() group are raised before the prepare, with no server I/O. Like the official prepare-then-bind, they discard the previous result (a later fetchone() raises InterfaceError) and set rowcount=-1, description=None. executemany() group counts are checked after the prepare but before any execute. Scalar range, NUL and charset errors are raised at bind time, after the prepare. After the prepare, a failure leaves rowcount and description at their previous values; a server error from one executemany() group leaves the earlier groups executed, and the cursor stays usable

Deliberate differences from the official wrapper. The first four are pinned by live differential claims (wrapper-collection-null-element and wrapper-collection-errors); the others are covered only by offline tests (tests/test_compat_wrapper_collections.py):

  • A None element binds a NULL element; the official wrapper sends the text 'None', or raises UnboundLocalError when inference meets None first.
  • BIT/VARBIT collection elements are not supported (NotSupportedError); the official wrapper converts bytes to bit strings.
  • Only list, tuple, set and frozenset arguments are collections; the official wrapper treats any iterable as one (a generator is consumed while inferring and binds an empty set).
  • Server error -494 raises ProgrammingError (official IntegrityError).
  • A type or argument error in a later executemany() group, or a group whose value count does not match the placeholders, runs no group at all; the official wrapper executes the earlier groups first.
  • With an explicit set_type, unsupported elements still raise (a nested list or object is ProgrammingError, bytes with a non-VARBIT code is NotSupportedError); the official wrapper sends their str() text.
  • The scalar subset is otherwise unchanged: the official wrapper also binds bool, float, Decimal, dates and bytes scalars and skips None arguments, while this wrapper rejects those types and binds None as SQL NULL. Since #840 its int arguments past signed 32-bit bind BIGINT or NUMERIC, as native bind_param() does.

Like the official wrapper, every collection binds with the SET kind (claim wrapper-collection-executemany-kinds), so a MULTISET or SEQUENCE column receives deduplicated, server-ordered elements; use the native imports(..., kind=...) to keep duplicates or order.

from pycubrid.compat import cubriddb
from pycubrid.constants import CUBRIDDataType

conn = cubriddb.connect("CUBRID:localhost:33000:testdb:::", "dba", "")
try:
    cur = conn.cursor()
    try:
        sql = "INSERT INTO t (id, tags, scores) VALUES (?, ?, ?)"
        cur.execute(sql, (1, ("a", "b"), (3, 1)))  # inferred STRING and INT
        cur.execute(sql, (2, ["c"], ["4"]), set_type=[None, None, CUBRIDDataType.INT])
        cur.executemany(sql, [(3, {"d"}, (5,)), (4, (), ())])
    finally:
        cur.close()
finally:
    conn.close()

Native LOB streams and handles (lob, fetch_lob, bind_lob)

Since #441, the native subset fetches and binds BLOB/CLOB handles with the official names: conn.lob() (or native.lob(conn)) returns an empty native.lob, cur.fetch_lob(col, lob) fills it from the next row, and cur.bind_lob(index, lob) binds its handle to a one-based parameter. The typical use copies a stored LOB into another row without reading it into Python. The server copies the value when the statement runs, so after execute() the source row can change or be deleted.

  • fetch_lob(col, lob) consumes the next row of the current SELECT result, like fetch_row() (including FETCH paging), and puts the handle at the one-based column col into lob. The type of that column decides BLOB or CLOB. A col that is not an int raises TypeError first, as the official argument parser does. At the end of the result it returns None and changes nothing, before the column range or type or the lob's state is checked, as in the official driver. Otherwise a column that is not BLOB/CLOB or out of range raises ProgrammingError without consuming the row, and a closed lob or a lob created by another connection raises InterfaceError. It returns None, as in the official driver. A NULL cell consumes the row and leaves lob without a value. If the server's cell at col is not a handle of the column's LOB type, DataError is raised without consuming the row and the connection stays usable; a handle with damaged framing raises OperationalError and the physical session is retired.
  • bind_lob(index, lob) binds the handle lob holds at that moment and sends nothing until execute(). A fetched handle names a committed stored value that the server copies into the new row, so, as in the official driver, it can be bound again, on another open connection, and even after its own connection has closed or reconnected. Anything that is not a native.lob raises TypeError (as official; a non-int index is reported first, as the official argument parser does), and an out-of-range index raises ProgrammingError. InterfaceError is raised, before any request, for a closed lob, a lob without a value (never filled, or filled from a NULL cell), and a closed cursor or connection. Bind SQL NULL with bind_param(index, None).
  • lob.close() drops the handle locally and sends nothing: the CAS protocol has no LOB free request, and the server keeps a fetched value with its row. A binding made before close() stays valid. A closed lob cannot be filled or bound again; create a new one with conn.lob().
  • lob.write(data, type="B", /) -> None accepts str (encoded as UTF-8, independent of the connection charset) or bytes. The first write creates a BLOB by default; use "C" for a CLOB. Later type arguments do not change the handle's type. Even an empty first write creates the handle, without a LOB_WRITE request. Position and packed size advance by confirmed byte count. CUBRID external storage is append-only: seek to the current end before a write. A non-end write raises NotSupportedError before broker I/O; it does not overwrite or fill a hole. Appending to a fetched handle updates that handle's size; the source row keeps its old declared size, but an ordinary explicit-offset read beyond that size can observe the shared file's extra bytes. Bind the updated handle to a new row for the new logical size.
  • lob.read(length=0, /) -> str reads at the current byte position and advances by bytes actually received. Omitted or zero length reads the remainder, with bounded wire chunks for large values. Both BLOB and CLOB return strict UTF-8 text, matching the official Python 3 extension for valid UTF-8; use ordinary Lob.read() for arbitrary binary bytes. A split UTF-8 character raises UnicodeDecodeError after the position advances by the bytes received. Empty values, EOF and requests longer than the remaining size safely return "" or the available text. If a later chunk fails, bytes accepted from earlier replies remain reflected in the position (a live session can resume from there). The official CCI wrapper can instead error at EOF or read past its declared buffer. Those unsafe edge behaviors are not emulated.
  • lob.seek(offset, whence=SEEK_CUR, /) -> int changes the byte position. native.SEEK_SET, SEEK_CUR and SEEK_END are exported; as in the official driver, SEEK_END computes size - offset (a positive offset moves backward). seek(0, SEEK_CUR) reports the current position; there is no separate tell(). Invalid whence or a resulting negative/out-of-range position is rejected without changing position, unlike the official driver's unchecked negative seek. Filling the same holder from another row preserves its byte position, as in the official extension; use seek(0, SEEK_SET) to restart. A nonnegative position past EOF is virtual: reads return "" and writes remain append-only, so it cannot create a hole.
  • The native connection starts in autocommit mode, but set_autocommit(False) allows manual transactions. A LOB fetched while autocommit is enabled can be bound on another connection; one fetched in manual mode keeps a conservative non-transferable flag even after a later commit(). Re-fetch it in confirmed autocommit mode before cross-connection binding. A new LOB_NEW handle is temporary and belongs to its original live physical session: the first autocommit statement that binds it consumes its temporary file (also if that statement fails). Reusing that handle then receives the server's stale-locator error; fetch the committed row into a fresh lob for another bind. bind_lob() snapshots the handle at bind time, so bind again after a later append. Read/write/seek require the handle's original live session; a committed fetched handle may still be bound from another connection as described above.
  • Migrating from official _cubrid.lob: use this explicit sync namespace, retain byte-based seek positions and the official SEEK_END direction, and replace a closed holder with conn.lob() rather than writing after close. Ordinary pycubrid.lob.Lob.read(length, offset=0) and write(bytes, offset=0) remain absolute-offset byte APIs; they did not become stateful. Native file operations are described below; async LOBs are not provided by this facade.

Deliberate differences from the official driver, each pinned by a live differential claim (lob-* in tests/fixtures/official_differential_claims.json): binding a lob without a value or a closed lob raises InterfaceError (official binds a NULL handle and stores SQL NULL); fetch_lob() into a closed lob or a lob created by another connection raises InterfaceError (official fills it, and its close() is reusable); and a non-LOB column in fetch_lob() or a bad bind_lob() index raises ProgrammingError (official InterfaceError). The official driver types the handle from column 1 instead of col, so its bind type byte can differ; the broker uses the handle's own type, and on CUBRID 10.2 and 11.4 the stored copy is the same.

from pycubrid.compat import native

conn = native.connect("CUBRID:localhost:33000:testdb:::", "dba", "")
try:
    src, dst = conn.cursor(), conn.cursor()
    try:
        src.prepare("SELECT id, photo FROM images WHERE id = 1")
        src.execute()
        photo = conn.lob()
        src.fetch_lob(2, photo)  # column 2 is a BLOB
        dst.prepare("INSERT INTO archive (photo) VALUES (?)")
        dst.bind_lob(1, photo)
        dst.execute()
        photo.close()
    finally:
        src.close()
        dst.close()
finally:
    conn.close()

Native LOB files (imports, export) {#native-lob-files}

The explicit sync-only native.lob provides positional-only imports(file: str, type: str = "B", /) -> None and export(file: str, /) -> None. Both transfer raw bytes, including CLOB contents: they do not decode text, convert newlines or use the connection charset. Local reads/writes and wire requests use chunks of at most 64 KiB, without joining the whole value. Neither method changes the receiver's byte position.

  • Paths and the import type must be exact built-in strings, not bytes, PathLike, string subclasses or file objects. Embedded path NUL raises ValueError; invalid argument types raise TypeError. Import accepts only "B"/"b" or "C"/"c"; other strings raise InterfaceError with .code == -30006 before any file or server operation. Relative paths are resolved once at entry; no tilde/environment expansion or parent creation occurs.
  • imports() opens the input before creating a replacement temporary LOB. It adopts the new handle only after the complete read, confirmed writes and input close succeed. A failure leaves the prior receiver state and position unchanged unless the caller mutated them or the session was retired. Earlier prepared bindings remain snapshots. Staging does not promise rollback or reclamation of server temporary files; the adopted handle retains the ordinary created-LOB session and first-autocommit-bind lifetime described above.
  • export() requires a populated handle on its original live physical session, and reads from offset zero regardless of the current byte position. It writes one exclusively created sibling temporary file, then replaces the destination only after all bytes, flush and close succeed. A populated empty LOB produces an empty file without a server read; a holder without a value raises InterfaceError (-30018) before creating a file. Export the freshly fetched stored handle, not a created handle whose first autocommit bind consumed it.
  • Local open/path/temp-file failures use InterfaceError code -30009, input read/close failures -30016, and output write/flush/close/replace failures -30017. The original OSError, when present, is the cause. These errors keep pycubrid's message-only args, not the official integer/message pair; no new errno or SQLSTATE is invented. Server/transport errors retain their own classes.

Staged import, exact-string validation and replace-on-success export are deliberate safety differences from the pinned C extension's early mutation and direct truncate/write/unlink behavior. Replacement also changes inode/hard-link and symlink behavior and normally uses the temporary file's restrictive permissions, not the old destination's metadata. This is not a directory sandbox, an fsync/crash durability guarantee or an atomic database/filesystem transaction. Receiver/session changes during supported local I/O are rejected before adoption/publication, but arbitrary audit-hook changes inside the final os.replace() are not transactional. Use trusted, caller-owned paths. Ordinary/async/wrapper APIs are unchanged; success claims do not certify unsafe C creation, fault-injection or empty-file equivalence.

Async Module Constructor

pycubrid.aio.connect()

async 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,
) -> AsyncConnection

Create and open an async connection.

  • Returns a connected AsyncConnection.
  • Accepts the same collection / JSON decoding kwargs and the same charset option as pycubrid.connect().
  • Supports autocommit=True, applied automatically once the connection is established. AsyncConnection itself now accepts autocommit directly too (as a keyword-only constructor argument), so constructing it without going through this factory no longer silently drops the flag.
  • Provides a similar async surface to the sync API, including await conn.ping(reconnect=...); create_lob() remains sync-only, and auto-commit changes go through await conn.set_autocommit(...) instead of a property setter.
  • Accepts the same ssl parameter as pycubrid.connect(): True, False/None, or a custom SSLContext; when True, the default verified context enforces a TLS 1.2 minimum. Async TLS uses CUBRID's STARTTLS-style upgrade — the CUBRS handshake is sent in plaintext, then the transport is upgraded via asyncio.AbstractEventLoop.start_tls() (bounded by ssl_handshake_timeout) before OPEN_DATABASE. See the Connection guide for full details.
import asyncio
import pycubrid.aio

async def main() -> None:
    conn = await pycubrid.aio.connect(database="testdb")
    cur = conn.cursor()
    await cur.execute("SELECT 1")
    print(await cur.fetchone())
    await cur.close()
    await conn.close()

asyncio.run(main())

Connection Class

pycubrid.connection.Connection

Represents a single connection to a CUBRID database via the CAS broker protocol.

Connection Constructor

class Connection:
    def __init__(
        self,
        host: str,
        port: int,
        database: str,
        user: str,
        password: str,
        autocommit: bool = False,
        **kwargs: Any,
    ) -> None

Note: Do not instantiate Connection directly. Use pycubrid.connect() instead.

Selected **kwargs:

Name Type Default Description
enable_timing bool | None None Enable optional driver-level timing instrumentation for connect, execute, fetch, and close operations. When None, falls back to the PYCUBRID_ENABLE_TIMING environment variable (truthy values: 1, true, yes, case-insensitive). When disabled, the timing module is not imported and connection.timing_stats is None (zero overhead). See Timing & Profiling Hooks.
ssl bool | ssl.SSLContext | None None TLS configuration shared by sync and async connections. True uses the default verified context with minimum_version = TLSv1_2; see Connection guide.
read_timeout float | None None Timeout in seconds for each socket send or receive call (the async connection bounds each whole request round trip instead); not an SQL wall-clock deadline. See What the timeouts bound.
fetch_size int 100 Server-side fetch batch size.
json_deserializer Callable[[str], Any] | None None Opt-in JSON column decoder.
charset str "utf-8" Connection codec; see charset. Kept across reconnects.
decode_collections bool False Decode SET/MULTISET/SEQUENCE columns into Python collections.

Connection Methods

connect()

def connect(self) -> None

Establish a TCP CAS session with broker handshake and open the database. Called automatically by the constructor. Calling on an already-connected instance is a no-op.

Raises: OperationalError on network failure.


close()

def close(self) -> None

Close the connection and all tracked cursors. Sends a CloseDatabasePacket to the server. Calling on an already-closed connection is a no-op. After close(), any further method calls will raise InterfaceError.

conn = pycubrid.connect(database="testdb")
# ... work ...
conn.close()  # Connection and all cursors are closed

commit()

def commit(self) -> None

Commit the current transaction. Sends CLOSE_REQ for query handles still held by unclosed cursors, then a CommitPacket to the server (#485). Rows already received stay readable. In autocommit mode no END_TRAN is sent, so close cursors to release their server handles; with a statement-pooling broker the release rides on the next statement instead of its own CLOSE_REQ, and a cursor collected without close() is released the same way (deferred close, #488). If the CAS recycled its session after an earlier out-of-transaction reply, the request is preceded by the verified reconnect described in CAS recycled at a transaction boundary.

Raises: InterfaceError if the connection is closed.


rollback()

def rollback(self) -> None

Roll back the current transaction. Sends CLOSE_REQ for query handles still held by unclosed cursors, then a RollbackPacket to the server (#485).

Raises: InterfaceError if the connection is closed.


cursor()

def cursor(self) -> Cursor

Create and return a new Cursor bound to this connection. The cursor is tracked by the connection and will be closed when the connection closes.

Returns: A new Cursor instance.

Raises: InterfaceError if the connection is closed.

conn = pycubrid.connect(database="testdb")
cur = conn.cursor()
cur.execute("SELECT 1 + 1")
print(cur.fetchone())  # (2,)
cur.close()

get_server_version()

def get_server_version(self) -> str

Return the server engine version string (e.g., "11.2.0.0378").

conn = pycubrid.connect(database="testdb")
print(conn.get_server_version())  # "11.2.0.0378"

get_last_insert_id()

def get_last_insert_id(self) -> str | None

Return the cached broker-reported identity captured after the most recent INSERT, as a string, or None when unavailable. The corresponding cursor keeps an integer lastrowid snapshot. A later INSERT on another cursor updates the connection cache without changing the earlier cursor's snapshot. Reading this method performs no I/O.

commit(), rollback(), and non-INSERT statements preserve the observation. A new INSERT attempt (including a failed one), nonempty executemany_batch(), or physical connection discard/reconnect clears it. Empty, malformed, or failed identity retrieval leaves it None. An empty batch leaves it unchanged.

A normal transaction boundary keeps the same physical connection and cache. After an actual connection failure, explicit ping(reconnect=True) recovery, or the automatic reconnect after a failed out-of-transaction CHECK_CAS (#485), clears the connection cache. If the CAS is recycled right after an autocommit INSERT, the INSERT is committed but lastrowid is None and a WARNING is logged. A failed automatic reconnect leaves the connection disconnected, not closed; ping(reconnect=True) reconnects it. The earlier cursor's lastrowid snapshot remains available across that physical connection change.

This is a snapshot of cursor operations whose server response identifies an INSERT, replacing the previous live broker-state query. It does not refresh for CALL, INSERTs performed inside stored procedures, or out-of-band SQL. If a procedure inserts rows, return its identity explicitly or query it according to the procedure's server-side contract; do not use this cached method to discover it.

The broker may retain an earlier identity after an INSERT into a table without an AUTO_INCREMENT column. The reported ID does not prove the latest INSERT generated it, and retaining it after rollback does not prove a row exists.

cur.execute("INSERT INTO users (name) VALUES ('alice')")
conn.commit()
print(conn.get_last_insert_id())  # "1"

Migration from the previous unavailable "" result: use value is None instead of value == "", and check before converting to int:

value = conn.get_last_insert_id()
new_id = int(value) if value is not None else None

Successful values retain their existing string type; cursor.lastrowid remains int | None. The async method has the same semantics.

When an empty batch resets the cursor's lastrowid to None, the connection cache remains unchanged. If closing a previous query fails before a nonempty batch can start, both previous identity values remain unchanged and the exception propagates.


ping(reconnect=True)

def ping(self, reconnect: bool = True) -> bool

On a healthy session, perform a lightweight CHECK_CAS health check without SQL. Recovery of an automatically configured connection can execute the read-only escape-mode probe before accepting application SQL.

  • Returns True when the CAS connection is alive. CAS_INFO[0]=0 means OUT_TRAN, not a disconnected session, and does not itself cause a reconnect. Ordinary requests after an OUT_TRAN reply are preceded by an automatic CHECK_CAS and reconnect only when it fails (#485); a healthy ping() also counts as that check.
  • With reconnect=False, checks an open socket without reconnecting and returns False when disconnected or when the check fails.
  • With reconnect=True, probes the existing socket first and attempts one reconnect if disconnected, if the check fails with a transport/protocol error, or if CHECK_CAS returns a negative code (broken CAS-to-DB link). reconnect=False reports the negative response as False without reconnecting and closes that broken session (later calls raise InterfaceError), as the async driver does. Only explicitly set autocommit is restored after successful recovery. Interrupted SQL is not replayed; the caller must decide whether retry is safe. An automatically detected no_backslash_escapes mode is probed again on a new physical session before use; an explicit True/False remains pinned. A healthy same-session ping does not probe. Probe failure retires the replacement and returns False, without guessing a mode or replaying SQL.
  • A failed reconnect returns False whatever the error, including a server rejection of OPEN_DATABASE such as a wrong password (-171) or an unknown user (-165); before #896 the sync driver raised that DatabaseError. The async driver behaves the same.
if not conn.ping():
    raise RuntimeError("database connection is unavailable")

create_lob(lob_type)

def create_lob(self, lob_type: int) -> Lob

Create a new LOB (Large Object) on the server.

Parameters:

Parameter Type Description
lob_type int LOB type code: 23 for BLOB, 24 for CLOB

Returns: A new Lob instance.

from pycubrid.constants import CUBRIDDataType

lob = conn.create_lob(CUBRIDDataType.CLOB)  # 24
lob.write(b"Hello, CUBRID!")

Important: Lob objects cannot be passed as query parameters to execute() or the default executemany(). Insert strings/bytes directly into CLOB/BLOB columns instead. See TYPES.md for details. The one exception is executemany(..., prepared=True) on the sync cursor with autocommit off, which binds a Lob of the same connection by its handle.


get_schema_info(schema_type, table_name="", pattern_match_flag=1, *, arg2=None)

def get_schema_info(
    self,
    schema_type: int,
    table_name: str = "",
    pattern_match_flag: int = 1,
    *,
    arg2: str | None = None,
) -> GetSchemaPacket

Create a schema result owned by this connection and its current CAS session. Consume it with fetch_schema_info() or abandon it with close_schema_info().

Parameters:

Parameter Type Default Description
schema_type int — Schema type code (see CCISchemaType)
table_name str "" Table name filter
pattern_match_flag int 1 Pattern match flag
arg2 str | None None Keyword-only second name/pattern, e.g. an ATTRIBUTE filter

Returns: The raw GetSchemaPacket, retaining query_handle, tuple_count and columns. Each condensed column has only column_type, scale, precision and name; SELECT nullability/default/constraint metadata is not present in FC9. NULL (None) and the empty string are distinct wire arguments. For all ATTRIBUTE names use arg2="%" with flag 2; NULL is not an all-attributes shortcut.

from pycubrid.constants import CCISchemaType

packet = conn.get_schema_info(CCISchemaType.CLASS, "my_table", 0)
try:
    rows = conn.fetch_schema_info(packet)
finally:
    conn.close_schema_info(packet)  # Also safe after successful consumption.
print(rows)

Available CCISchemaType values:

Code Name Description
1 CLASS Tables and views
2 VCLASS Views
4 ATTRIBUTE Columns
11 CONSTRAINT Index-family entries
16 PRIMARY_KEY Primary keys
17 IMPORTED_KEYS Foreign keys (imported)
18 EXPORTED_KEYS Foreign keys (exported)

The owned-object live matrix (#457) verifies these seven types in both sync and async modes on CUBRID 10.2.18 and 11.4.6, including exact/pattern filters, empty results, composite keys and multi-FETCH ATTRIBUTE rows. Other CCISchemaType values and native-driver parity are not certified by this matrix.

Interpret rows using packet.columns names. CLASS can include views (TYPE=1) as well as ordinary tables (TYPE=2). PRIMARY_KEY rows can arrive in attribute-name order: use KEY_SEQ for declared composite-key order. CONSTRAINT reports index families, not every primary/foreign key; use the dedicated key types for those relationships. Its page/key fields are not reliable index statistics. Owner-qualified names and broker-encoded ATTRIBUTE DOMAIN integers are retained as returned; do not assume names are always unqualified or DOMAIN equals a scalar type constant.

fetch_schema_info(packet) and close_schema_info(packet)

fetch_schema_info(packet) -> list[tuple[Any, ...]] eagerly fetches exactly the advertised rows, then closes the original handle, including zero-row results. Premature EOF, inconsistent counts or cleanup failure raise instead of returning partial rows. close_schema_info(packet) -> None explicitly abandons the result; repeat closes of the same owner's retired packet are no-ops. Foreign/unowned packets and retired fetches raise InterfaceError before network I/O. Changing the raw packet's public fields cannot change the tracked handle or metadata.

Commit/rollback first close active schema handles, then retire ownership. Cursor statements with effective autocommit do the same before sending SQL, including executemany_batch(..., auto_commit=True) on a manual-commit connection. With connection autocommit enabled, get_server_version() also closes owned schema handles before its auto-committing version request. Fetching such a retired packet raises InterfaceError locally before another RPC. Physical disconnect/reconnect and connection close also retire it; results cannot be replayed in a replacement CAS session. FETCH/CLOSE never reconnect or implicitly commit. A failed schema creation or close discards its uncertain session; a FETCH error retains the original exception if cleanup also fails (the cleanup error is logged). Async methods use await and hold the connection lock through fetch/cleanup. Cancellation during schema I/O discards the session and re-raises cancellation; cancellation while merely waiting for the lock does not discard another task's session. KeyboardInterrupt or SystemExit during async schema FETCH also discards the uncertain session without sending CLOSE on a stream with a pending reply.


Connection Properties

autocommit

@property
def autocommit(self) -> bool

@autocommit.setter
def autocommit(self, value: bool) -> None

Get or set the auto-commit mode. When enabled, each statement is committed immediately. Setting this property sends a SetDbParameterPacket and CommitPacket to flush the transaction state on the server. Both take effect on one CAS session: if the CAS is recycled between them, the new value is restored on the replacement session before the COMMIT is sent there (at most one reconnect per call). If the COMMIT fails, the connection is closed, the previous value is kept and OperationalError is raised with the cause chained (#551).

conn = pycubrid.connect(database="testdb")
print(conn.autocommit)  # False

conn.autocommit = True
# Statements now auto-commit

timing_stats

@property
def timing_stats(self) -> TimingStats | None

Return the per-connection TimingStats accumulator when the connection was opened with enable_timing=True (or with PYCUBRID_ENABLE_TIMING set). Returns None when timing is disabled — in which case the timing module is never imported and the hot path incurs no overhead.

import pycubrid

conn = pycubrid.connect(database="testdb", enable_timing=True)
cur = conn.cursor()
cur.execute("SELECT 1")
cur.fetchall()

stats = conn.timing_stats
assert stats is not None
print(stats)
# TimingStats(connect=1 calls, 12.345ms total, 12.345ms avg,
#             execute=1 calls, 0.987ms total, 0.987ms avg,
#             fetch=0 calls,
#             close=0 calls)

print(stats.execute_count, stats.execute_total_ns)  # 1 987000

The escape-mode probe that connect() runs on a new session is not counted. The single row of SELECT 1 arrives with the execute reply, so fetchall() sends no FETCH request and fetch stays at 0 (#899).

stats.reset()  # clear all counters

See Timing & Profiling Hooks for the full guide.


Connection Context Manager

Connection implements the context manager protocol (__enter__ / __exit__).

  • On successful exit: calls commit() then close().
  • On exception: calls rollback() then close().
  • The block's exception always propagates unchanged: a session it already retired is only closed (no rollback or reconnect), and a failed rollback is logged and noted on the original exception. AsyncConnection (async with) behaves the same, and a cancelled block stays cancelled. See Context Manager Protocol.
with pycubrid.connect(database="testdb") as conn:
    cur = conn.cursor()
    cur.execute("INSERT INTO users (name) VALUES ('bob')")
    # Auto-commits on exit

# conn is closed here

Cursor Class

pycubrid.cursor.Cursor

Represents a database cursor for executing SQL statements and fetching results.

Cursor Constructor

class Cursor:
    def __init__(self, connection: Connection) -> None

Note: Do not instantiate Cursor directly. Use connection.cursor() instead.

Cursor Methods

execute(operation, parameters)

def execute(
    self,
    operation: str,
    parameters: Sequence[Any] | None = None,
) -> Cursor

Prepare and execute a SQL statement.

Parameters:

Parameter Type Description
operation str SQL statement with optional ? placeholders
parameters Sequence or None Non-string positional parameter values to bind

Returns: The cursor itself (for chaining).

After closing the previous query handle, execute() clears its result state before binding parameters or sending the new statement, discarding buffered rows and any held fetch-page error. If binding or the request fails, description is None, rowcount is -1, lastrowid is None, and fetch methods raise InterfaceError("No result set available"). A later successful execute() can reuse the cursor. If closing the previous handle fails, execute() keeps the buffered result and its page error; connection invalidation or reconnect handling may still retire the handle. An undecodable replacement reply may open a new query handle, which stays tracked for cleanup. This behaviour applies to both Cursor and AsyncCursor.

Raises:

  • InterfaceError if the cursor is closed
  • ProgrammingError on SQL errors or parameter mismatch
# Simple query
cur.execute("SELECT * FROM users")

# Parameterized query (qmark style)
cur.execute("SELECT * FROM users WHERE age > ?", [21])

# INSERT with parameters
cur.execute("INSERT INTO users (name, age) VALUES (?, ?)", ["alice", 30])

Important: mappings such as dict are not supported for parameter binding. Passing a mapping raises ProgrammingError("parameters must be a sequence").

Supported parameter types:

Python Type SQL Literal
None NULL
bool 1 / 0
str 'escaped'
bytes X'hex'
int, float Numeric literal
Decimal Numeric literal
datetime.date DATE'YYYY-MM-DD'
datetime.time TIME'HH:MM:SS'
datetime.datetime DATETIME'YYYY-MM-DD HH:MM:SS.mmm'
Set / Multiset / Sequence SET{...} / MULTISET{...} / SEQUENCE{...}

executemany(operation, seq_of_parameters, *, prepared=False)

def executemany(
    self,
    operation: str,
    seq_of_parameters: Sequence[Sequence[Any]],
    *,
    prepared: bool = False,
) -> Cursor

Execute the same SQL statement repeatedly with different parameter sets. Each element must be a non-string sequence. For non-SELECT statements, rowcount is set to the cumulative total of affected rows.

With an empty parameter list, executemany(operation, []) executes no SQL, closes any previous query handle, and resets the cursor to description=None, rowcount=0, lastrowid=None, with no fetchable rows. It returns the cursor. No request is sent when there is no active query handle. If closing the previous handle fails, the exception propagates and the handle remains tracked. The same contract applies to the async cursor.

data = [("alice", 30), ("bob", 25), ("carol", 28)]
cur.executemany("INSERT INTO users (name, age) VALUES (?, ?)", data)
print(cur.rowcount)  # 3
Partial failure: which rows failed

When rows of a DML batch fail, the server still runs every other row and undoes only the failing ones (#841). With autocommit on, each successful row is committed; with autocommit off, commit() keeps them and rollback() removes them all. executemany() raises the first failing row's error, with the same class, message, errno and sqlstate as before, and the exception carries every row's outcome:

Attribute Type Meaning
batch_index int Position in seq_of_parameters of the row this exception describes (the first failing row, or the interrupted row of a stopped prepared batch)
batch_errors tuple[tuple[int, DatabaseError], ...] (index, error) for every failed row, in row order; each error is mapped like the raised one and has its own batch_index
batch_rowcounts tuple[int, ...] One entry per executed row, in row order: the row's affected count, or -1 for a failed row

rowcount stays -1 after a partial failure, as before: the batch as a whole failed, and with autocommit off the applied rows may still be rolled back. The number of applied rows is sum(c for c in exc.batch_rowcounts if c >= 0).

try:
    cur.executemany("INSERT INTO users (id) VALUES (?)", [(1,), (2,), (1,), (3,)])
except pycubrid.IntegrityError as exc:
    print(exc.batch_index)  # 2
    print([i for i, _error in exc.batch_errors])  # [2]
    print(exc.batch_rowcounts)  # (1, 1, -1, 1)
    print(cur.rowcount)  # -1

Exceptions not raised for a batch row failure have batch_index, batch_errors and batch_rowcounts set to None. A non-DML statement (for example SELECT) runs row by row through execute(), stops at the first failure and does not set them. If the server interrupts a statement (ER_INTERRUPTED), it stops there: batch_rowcounts ends at the interrupted row, and the rows past its end did not run (when the last row is the interrupted one, it covers the whole batch). The literal batch still raises its first failing row's error in that case. The same contract applies to the async cursor and to executemany(..., prepared=True).

Prepared batch: prepared=True

executemany(..., prepared=True) (#839) sends an INSERT, UPDATE, DELETE or MERGE as one PREPARE and one EXECUTE_ARRAY request (function code 21) that carries every row's values as typed host variables. The default (prepared=False) keeps the literal batch described above. For 10,000-row batches on CUBRID 10.2-11.4 it took 0.33-0.62x of the literal batch's time for INSERT, UPDATE and DELETE and 0.12-0.21x for MERGE (see PERFORMANCE.md).

cur.executemany(
    "INSERT INTO users (name, age) VALUES (?, ?)",
    [("alice", 30), ("bob", None), ("carol", 2**40)],
    prepared=True,
)
  • Statements. Only statements whose first keyword is INSERT, UPDATE, DELETE or MERGE are accepted (a comment right after the verb, as in INSERT/*x*/INTO, ends the keyword, #898); anything else (including SELECT, CALL and WITH ... INSERT) raises NotSupportedError. There is no fallback to the literal batch. Every placeholder must be a value the server can type: in a MERGE source, write CAST(? AS INT).
  • Values. The prepared host-variable types of PARAMETER_BINDING.md: None, bool, int (INT, BIGINT or NUMERIC by range), float, Decimal, str, non-empty bytes/bytearray, date, time and datetime. Each value carries its own type byte, so one column may mix types from row to row. pycubrid.types.Set, Multiset and Sequence are sent as collections of string elements (str, int or None) that the server converts; a Multiset is sent as a SEQUENCE, because the broker rejects the MULTISET bind kind. pycubrid.Lob values are accepted on the sync cursor with autocommit off (see below).
  • Checks before send. seq_of_parameters may be any iterable of rows (a generator is consumed once; the annotation stays Sequence[Sequence[Any]], as for the default path). Every row is encoded before any request: a row that is not a sequence, rows of different lengths or a value outside the set above raises (ProgrammingError, DataError or NotSupportedError) with its position, for example rows[2][1]: nan and inf are not supported by CUBRID; empty rows raise NotSupportedError. The value itself never appears in the message. After PREPARE, a server marker count different from the row length raises ProgrammingError and no row is executed.
  • Partial failure and rowcount. As with the literal batch, the server runs every row and undoes only a failing one. With autocommit on, each successful row is committed; with autocommit off, commit() keeps the successful rows and rollback() removes them all. The first failing row's error is raised with the same exception mapping and the same per-row attributes as the literal batch (Partial failure) and rowcount stays -1. On success rowcount is the sum of the per-row counts. lastrowid is None. With autocommit off, a deadlock or a server shutdown aborts the whole transaction and fails the whole request. An interrupted row (ER_INTERRUPTED) stops the server: the rows after it are not executed, and the interruption's error is raised instead of an earlier row's error. Its batch_index is the interrupted row, batch_errors still starts with the first failing row, and batch_rowcounts ends at the interrupted row, so the rows past its end did not run. An interruption of the last row is raised the same way.
  • Server text. Values are coerced as host variables, not as literals, so a conversion error reads "Cannot coerce host var to type ..." (error -494, the same exception class as the literal batch).
  • LOB values. A Lob from this connection's create_lob() is bound by its handle. With autocommit on, each row's commit removes every temporary LOB file of the session and the second row would fail with -1016, so a batch that contains a Lob raises NotSupportedError before anything is sent: turn autocommit off and call commit() after the batch. A Lob that is closed, belongs to another connection or was created before a reconnect raises InterfaceError. The async cursor rejects Lob values with NotSupportedError.
  • Handles. The prepared statement handle is released like any other result: with the next statement on the cursor, by commit()/rollback(), or by close(). With autocommit on and broker statement pooling, the release rides on the next statement's request (a PREPARE_AND_EXECUTE or the next batch's PREPARE) instead of a separate CLOSE_REQ. With autocommit on, a broker that closes its connection right after PREPARE makes the batch fail with OperationalError; nothing is retried automatically.

prepared=False keeps every behaviour of the literal batch. A non-bool prepared raises ProgrammingError. The same contract applies to AsyncCursor.executemany().

Async encoding and cancellation (#882). AsyncCursor.executemany() with prepared=True, and the default literal batch of an INSERT, UPDATE, DELETE or MERGE, encode or render every row synchronously before the first request, so no other task on the event loop runs meanwhile and a cancellation cannot interrupt it. Other statements (for example SELECT) run one awaited execute() per row: other tasks run between rows, and a cancellation during a later row lands after the earlier rows' requests. The time is about the cost of encoding the rows, see Sync and Async Characteristics; split very large batches if the loop must stay responsive. A slow or blocking row iterator blocks the loop the same way. Where a cancellation lands:

  • Requested while the rows are encoded: raised once encoding ends, before any request is sent. The session stays open. Nothing ran on the server.
  • After PREPARE was sent: the session is retired (the reply could arrive later), and EXECUTE_ARRAY is never sent.
  • After EXECUTE_ARRAY (or, for the literal batch, EXECUTE_BATCH) was sent: the rows may have run, and with autocommit been committed. The session is retired, and the batch is never sent again; check the data before you retry.

nextset()

def nextset(self) -> None

DB-API compatibility method. pycubrid does not expose multiple server result sets through this interface, so nextset() always returns None after checking that the cursor is still open.

assert cur.nextset() is None

executemany_batch(sql_list, auto_commit)

def executemany_batch(
    self,
    sql_list: list[str],
    auto_commit: bool | None = None,
) -> list[tuple[int, int]]

Execute multiple distinct SQL statements in a single batch request to the server. This is more efficient than calling execute() in a loop when the SQL statements differ.

Parameters:

Parameter Type Description
sql_list list[str] List of complete SQL statements
auto_commit bool or None Override auto-commit for this batch (default: connection setting)

Returns: List of (statement_type, result_count) tuples.

results = cur.executemany_batch([
    "CREATE TABLE t1 (id INT AUTO_INCREMENT PRIMARY KEY, val VARCHAR(50))",
    "INSERT INTO t1 (val) VALUES ('hello')",
    "INSERT INTO t1 (val) VALUES ('world')",
])
# results: [(4, 0), (20, 1), (20, 1)]
# statement_type 4 = CREATE_CLASS, 20 = INSERT

Note: executemany_batch is a pycubrid extension, not part of PEP 249. Once the previous query handle is closed, prior cursor result state is cleared before the batch request. A failed batch, including transport or response-parse errors, leaves description=None, rowcount=-1, lastrowid=None, and no fetchable rows. A per-statement error still raises the appropriate database exception; successful partial results do not establish a final row count. The exception carries batch_index, batch_errors and batch_rowcounts with positions in sql_list (see Partial failure). If closing the previous handle fails, no batch is sent and the handle remains tracked. SQL passed directly to executemany_batch is rendered by the caller, so it is not generation-fenced: after an automatic reconnect (#485) it is sent to the new session as given. Only SQL that execute()/executemany() render from parameters is rejected when the session changed after binding.


fetchone()

def fetchone(self) -> tuple[Any, ...] | None

Fetch the next row of a query result set. Returns None when no more rows are available. Automatically fetches more rows from the server when the local buffer is exhausted (100 rows per fetch).

cur.execute("SELECT name, age FROM users")
row = cur.fetchone()
if row:
    name, age = row

Note on explicit connection recovery: A normal CAS_INFO[0]=0 response does not replace the session or invalidate a cursor. If an actual connection failure is followed by successful ping(reconnect=True) recovery while a result is only partially buffered, rows already in the buffer remain readable. Once exhausted, subsequent fetchone()/fetchmany()/fetchall() calls raise OperationalError with the existing message result set lost due to broker reconnect mid-fetch because the former server-side handle is no longer valid. Re-execute the query explicitly; it is never replayed automatically. execute() and close() reset the invalidation flag.

Transaction-boundary fetching: commit() and rollback() invalidate query handles, not rows already received into the local buffer. Cached rows remain readable, and fully buffered or exhausted results retain normal EOF behavior. If an unfinished result requires another server FETCH after its handle was invalidated, sync and async fetchone()/fetchmany()/fetchall() raise InterfaceError rather than silently reporting EOF. A fetchmany() or fetchall() crossing that boundary does not return a successful partial list; it may already have consumed local rows before raising. Execute a new query explicitly to continue. There is no transparent SELECT replay or holdable-result guarantee; reconnect invalidation retains its separate OperationalError.

Data errors on a later fetch page (#507): When a FETCH page contains a value pycubrid cannot represent (text invalid in the connection charset #492, an unresolved zone #413, a zero date #512), the fetchone(), fetchmany() or fetchall() call (or iteration step) that reaches the page raises DataError. The whole page is withheld, including its rows before the bad value. Rows that call had already collected are not lost: the next fetch calls return them without contacting the server, so a fetchmany() or fetchall() after the error returns those rows (possibly fewer than requested). After that, every fetch raises the same DataError again without requesting the page, until execute() or close(); no row of the failing page or after it is ever returned. The connection stays usable and the cursor keeps its server handle. Sync and async cursors behave the same. To read past the value, convert it in SQL (see Zero Date or Datetime Value) and execute again.


fetchmany(size)

def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]]

Fetch the next size rows. If size is omitted or None, use cursor.arraysize. An explicit size must be a plain Python int; floats, booleans and other types raise ProgrammingError before consuming rows or requesting another page. Zero and negative integers return an empty list without consuming rows or contacting the server, and emit a DeprecationWarning (see below). These rules also apply to AsyncCursor.fetchmany(). Closed-cursor and missing-result checks run first.

cur.execute("SELECT * FROM users")
batch = cur.fetchmany(10)  # Up to 10 rows

Deprecated: fetchmany(size) with size <= 0. Starting with pycubrid 1.13.0, sync and async fetchmany() emit a DeprecationWarning that points at the calling line when an explicit integer size is 0 or negative. The result is unchanged in 1.x ([], no row consumed, no FETCH sent). pycubrid 2.0 will raise ProgrammingError("size must be a positive integer") instead, the same rule the arraysize setter already applies (#678). Omitted/None sizes and positive integers do not warn, and the pycubrid.compat wrapper keeps returning [] for size <= 0 without a warning. Skip the call when the computed size can reach 0; this works on 1.x and 2.0:

rows = cur.fetchmany(remaining) if remaining > 0 else []

# AsyncCursor
rows = await cur.fetchmany(remaining) if remaining > 0 else []

fetchall()

def fetchall(self) -> list[tuple[Any, ...]]

Fetch all remaining rows of a query result. Returns an empty list if no rows remain.

cur.execute("SELECT * FROM users")
all_rows = cur.fetchall()
for row in all_rows:
    print(row)

callproc(procname, parameters)

def callproc(
    self,
    procname: str,
    parameters: Sequence[Any] = (),
) -> Sequence[Any]

Call a stored procedure. Constructs and executes a CALL procname(?, ?, ...) statement. procname must be a single identifier or non-empty dot-separated identifiers (for example, schema.my_procedure). Empty segments such as schema..proc raise ProgrammingError before any SQL is executed.

Returns: The original parameters sequence (as per PEP 249).

A stored function's return value is the one row of the result set: fetch it with fetchone(). The same holds for execute("CALL ...") (including method calls such as CALL find_user('dba') ON CLASS db_user) and EVALUATE. Each value carries its own type on the wire and is decoded like a column of that type (INT to int, VARCHAR to str, DATETIME to datetime, an object to its "OID:@page|slot|volume" string, SQL NULL to None); before #542 these values came back as raw bytes. description reports the column type as NULL (0) because the broker announces no type for it.

cur.callproc("my_procedure", [1, "hello"])
cur.callproc("my_function")
(value,) = cur.fetchone()

setinputsizes(sizes)

def setinputsizes(self, sizes: Any) -> None

DB-API no-op. Accepted for compatibility.


setoutputsize(size, column)

def setoutputsize(self, size: int, column: int | None = None) -> None

DB-API no-op. Accepted for compatibility.


close()

def close(self) -> None

Close the cursor and release the active query handle. Calling on an already-closed cursor is a no-op.


Cursor Properties

description

@property
def description(self) -> tuple[DescriptionItem, ...] | None

Return result-set metadata for the last executed statement, or None if no query has been executed or the last statement did not return rows.

Each item is a 7-tuple:

(name, type_code, display_size, internal_size, precision, scale, null_ok)
#  str    int        None           None          int       int    bool
Index Field Type Description
0 name str Column name
1 type_code int CUBRID data type code (see CUBRIDDataType)
2 display_size None Not used
3 internal_size None Not used
4 precision int Column precision
5 scale int Column scale
6 null_ok bool Whether the column is nullable

null_ok is True when the broker reports that the column permits NULL, and False for NOT NULL and primary-key columns. CAS transmits the inverse is_non_null flag; pycubrid converts it to the PEP 249 meaning in both sync and async cursors. Size fields remain None, and collection type codes remain 16/17/18; this correction does not select a CUBRIDdb compatibility profile.

cur.execute("SELECT name, age FROM users")
for col in cur.description:
    print(f"{col[0]}: type={col[1]}, precision={col[4]}, nullable={col[6]}")

rowcount

@property
def rowcount(self) -> int

Number of rows affected by the last execute() call. Returns -1 for SELECT statements or if no statement has been executed. After a failed batch it is -1, even when some rows were applied.


lastrowid

@property
def lastrowid(self) -> int | None

Last generated auto-increment identifier for an INSERT statement. None if no INSERT has been executed or the table has no auto-increment column.

cur.execute("INSERT INTO users (name) VALUES ('alice')")
print(cur.lastrowid)  # e.g., 1

arraysize

@property
def arraysize(self) -> int

@arraysize.setter
def arraysize(self, value: int) -> None

Default number of rows for fetchmany(). Defaults to 1.

The value must be a positive integer. Booleans and floats are not accepted. This validation also applies to AsyncCursor.arraysize.

Raises: ProgrammingError if the value is not a positive integer. An invalid assignment leaves the previous value unchanged.


Iterator Protocol

Cursor implements the iterator protocol, allowing you to iterate directly over result rows:

cur.execute("SELECT name, age FROM users")
for name, age in cur:
    print(f"{name} is {age} years old")

Calls fetchone() internally. Raises StopIteration when no more rows.


Cursor Context Manager

with conn.cursor() as cur:
    cur.execute("SELECT 1")
    print(cur.fetchone())
# cur is automatically closed

AsyncConnection Class

pycubrid.aio.connection.AsyncConnection

Async counterpart to Connection for use with asyncio, with a similar surface rather than full method-for-method parity.

Selected Methods

Method Signature Notes
cursor() def cursor(self) -> AsyncCursor Returns an async cursor immediately; do not await it
commit() async def commit(self) -> None Commit current transaction
rollback() async def rollback(self) -> None Roll back current transaction
close() async def close(self) -> None Close connection and tracked cursors
ping() async def ping(self, reconnect: bool = True) -> bool Native CHECK_CAS health check with optional reconnect
get_server_version() async def get_server_version(self) -> str Fetch engine version
get_last_insert_id() async def get_last_insert_id(self) -> str | None Cached broker identity as a string, or None
get_schema_info() async def get_schema_info(...) -> GetSchemaPacket Returns the parsed packet object
fetch_schema_info() async def fetch_schema_info(packet) -> list[tuple[Any, ...]] Eager rows, then original-handle cleanup
close_schema_info() async def close_schema_info(packet) -> None Explicit, idempotent abandonment
set_autocommit() async def set_autocommit(self, value: bool) -> None Sends SetDbParameterPacket and CommitPacket

AsyncConnection exposes async ping() parity with sync Connection.ping(). create_lob() remains sync-only. Concurrent awaiters on the same AsyncConnection are serialized with a per-connection asyncio.Lock, so shared use is safe but requests still execute one at a time.

While await conn.connect() opens a session and configures it (backslash-escape probe, autocommit) — including the reconnect done by ping(reconnect=True) and connect() after close() — other tasks' operations on the same connection wait for that setup. If setup fails, the session is discarded and each waiting task raises its own exception (#554): a pycubrid error is re-raised as a new instance of the same class (or of the nearest pycubrid.exceptions class when a subclass has a different constructor) with the same code, errno, sqlstate and error_source (#952) and the original as __cause__; any other error as OperationalError naming it (connection setup failed in another task: TimeoutError()); and a cancelled setup as OperationalError — cancelling the task that runs connect() cancels only that task. A waiting task that is itself cancelled still raises asyncio.CancelledError. The CHECK_CAS recovery inside a request (#485) runs under the connection lock instead; its failure is raised in the request that triggered it, and later requests find the connection closed (InterfaceError).

A request that passed that wait before the setup began, and then takes the lock while the setup task has released it for its escape probe, sends nothing and raises the retryable OperationalError ("connection setup in progress; retry operation") (#894). This refusal never drops or replaces the session being set up: get_schema_info(), commit(), rollback() and cursor statements raise it before any cleanup or CHECK_CAS recovery, so the setup task's probe and ping(reconnect=True) still succeed. A schema result from the replaced session was already retired, so fetch_schema_info() still raises InterfaceError ("schema result has been retired") and close_schema_info() is still a no-op for it.

AsyncConnection.__init__ accepts a keyword-only autocommit: bool = False argument, applied automatically the first time await conn.connect() completes — the same effect as await conn.set_autocommit(True), but usable when constructing AsyncConnection directly instead of through the pycubrid.aio.connect() factory.

async with await pycubrid.aio.connect(database="testdb") as conn:
    cur = conn.cursor()
    await cur.execute("INSERT INTO logs (msg) VALUES (?)", ["ok"])
    await conn.set_autocommit(True)
    await cur.close()

set_autocommit(value)

AsyncConnection.autocommit is read-only; use await conn.set_autocommit(True) to change it. Like the sync setter, this sends both SetDbParameterPacket and CommitPacket on one CAS session, with the same recycle and failure behavior (#551).

ping(reconnect=True)

async def ping(self, reconnect: bool = True) -> bool

On a healthy session, perform a lightweight native CHECK_CAS health check without SQL. Recovery can execute the read-only escape-mode probe.

  • Returns True when the CAS connection is alive.
  • Issues the native CHECK_CAS round-trip when the socket is open. CAS_INFO[0]=0 denotes OUT_TRAN after a transaction boundary, not a disconnected session. Ordinary requests after an OUT_TRAN reply are preceded by an automatic CHECK_CAS and reconnect only when it fails (#485).
  • With reconnect=False, does not reconnect; returns False when disconnected or when the check fails.
  • With reconnect=True, probes the existing socket first and attempts one reconnect if disconnected, after a transport/protocol failure, or when CHECK_CAS returns a negative code (broken CAS-to-DB link). reconnect=False reports the negative response as False without reconnecting and closes that broken session (later calls raise InterfaceError), as the sync driver does. Only explicitly set autocommit is restored; arbitrary SQL is never replayed. Automatic no_backslash_escapes detection runs on each new physical session, not on healthy same-session checks; explicit True/False remains pinned. Probe failure retires the replacement and returns False. Parameterized SQL bound against a prior session generation is rejected before send rather than silently rebound or replayed; retry is the caller's decision.
  • A failed reconnect returns False whatever the error, including a server rejection of OPEN_DATABASE such as a wrong password (-171), as in the sync driver.
if not await conn.ping(reconnect=False):
    await conn.ping(reconnect=True)

AsyncCursor Class

pycubrid.aio.cursor.AsyncCursor

Async counterpart to Cursor.

Selected Methods

Method Signature Notes
execute() async def execute(self, operation: str, parameters: Sequence[Any] | None = None) -> AsyncCursor Sequence-only parameter binding, same rules as sync cursor
executemany() async def executemany(self, operation: str, seq_of_parameters: Sequence[Sequence[Any]], *, prepared: bool = False) -> AsyncCursor Batch path for non-SELECT statements; prepared=True sends PREPARE + EXECUTE_ARRAY (no Lob values)
fetchone() async def fetchone(self) -> tuple[Any, ...] | None Returns one row or None
fetchmany() async def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]] Omitted/None uses arraysize; plain int required, nonpositive integers return [] with a DeprecationWarning (2.0 raises ProgrammingError)
fetchall() async def fetchall(self) -> list[tuple[Any, ...]] Returns remaining rows
nextset() async def nextset(self) -> None DB-API compatibility method; always returns None
close() async def close(self) -> None Releases active query handle
cur = conn.cursor()          # not awaitable
await cur.execute("SELECT * FROM users WHERE age > ?", [21])
rows = await cur.fetchall()
assert await cur.nextset() is None

Lob Class

pycubrid.lob.Lob

Represents a CUBRID Large Object (BLOB or CLOB).

A Lob belongs to the physical session it was created or wrapped on (#932). After ping(reconnect=True) or a failed out-of-transaction CHECK_CAS probe replaces that session, read() and write() of the older Lob raise InterfaceError ("LOB handle belongs to an earlier physical session") before any request is sent. They do not reconnect, retry or adopt the new session. If the replacement happens in the CHECK_CAS probe of the call itself, that call raises OperationalError ("LOB handle lost due to broker reconnect") and later calls raise InterfaceError. Create the LOB again with connection.create_lob(), or fetch the row again and wrap its handle in a new Lob. Lob does not record whether its handle was created or fetched, so this also applies to a handle of a committed row. Async connections have no LOB support; compat.native lobs keep their own session check.

Lob Factory Method

Lob.create(connection, lob_type)

@classmethod
def create(cls, connection: Connection, lob_type: int) -> Lob

Create a new LOB object on the server. Prefer using connection.create_lob() instead.

Parameters:

Parameter Type Description
connection Connection Active database connection
lob_type int CUBRIDDataType.BLOB (23) or CUBRIDDataType.CLOB (24)

Raises: ValueError if lob_type is not BLOB or CLOB.


Lob Methods

write(data, offset)

def write(self, data: bytes | bytearray | memoryview, offset: int = 0) -> int

Write bytes to the LOB starting from offset.

offset must be a non-negative Python int; type(offset) is not int is rejected, so bool (a subclass of int) and other types such as float or str raise InterfaceError before the open-LOB check's connection step or any packet send.

data must be bytes, bytearray or memoryview (#898). Anything else, including str and None, raises InterfaceError after the offset check and before the connection check or any packet send. A bytearray or memoryview is copied through the buffer protocol, so a multi-byte memoryview writes and returns its byte count (nbytes), not its item count.

An empty value of any of these types returns 0 without a broker request after the same open-LOB, offset-type-and-range and connected-connection checks. Existing wire argument validation still applies before this local return. The LOB's bytes and handle are unchanged; nonempty writes retain server-acknowledgement checks. Signed-64-bit packing failures still raise DataError, as they do through the real connection's nonempty write path, with the serialization error as the cause. This does not introduce async LOB support.

Returns: Number of bytes written.


read(length, offset)

def read(self, length: int, offset: int = 0) -> bytes

Read up to length bytes from the LOB starting from offset.

length and offset must be non-negative Python int values; bool and other types such as float or str raise InterfaceError before the connection check or any packet send.

The read is issued in as many round-trips as the broker requires: CUBRID caps a single LOB_READ response at a fixed size, so read() loops internally, advancing the offset by the bytes returned, until length bytes are collected or the end of the LOB is reached. read(0) returns b"" without a server round-trip.

Returns: The read bytes.


Lob Properties

lob_handle

@property
def lob_handle(self) -> bytes

The raw LOB handle bytes used for server communication.

The handle embeds the LOB size, which the server trusts when a handle is sent back. After each successful (or partly successful) write(), the size field is raised to offset + bytes written, never lowered, as the official CCI driver does (#441). Before this fix a written handle kept the size it had when it was created (usually 0).


lob_type

@property
def lob_type(self) -> int

The LOB type code (23 = BLOB, 24 = CLOB).


LOB Usage Notes

LOB columns return a dict on fetch, not a Lob object:

cur.execute("SELECT clob_col FROM my_table")
row = cur.fetchone()
lob_info = row[0]
# {'lob_type': 24, 'lob_length': 1234, 'file_locator': '...', 'packed_lob_handle': b'...'}

To insert LOB data, pass strings/bytes directly:

cur.execute("INSERT INTO my_table (clob_col) VALUES (?)", ["large text content"])

TimingStats Class

pycubrid.timing.TimingStats — also re-exported as pycubrid.TimingStats.

Per-connection accumulator for optional driver-level timing instrumentation. Instances are created automatically when a connection is opened with enable_timing=True (or with PYCUBRID_ENABLE_TIMING set) and are exposed via Connection.timing_stats.

When to use: quick in-process diagnosis of where wall-clock time is going (connect vs execute vs fetch vs close) without reaching for cProfile or external profilers. For deep hot-path analysis, prefer the standalone profiling scripts in Performance Investigation.

Counters

All counters are plain int attributes. Durations are nanoseconds captured via time.perf_counter_ns(). Updates are serialised by an internal threading.Lock so the object is safe to read from a thread other than the one driving the connection.

Attribute Type Description
connect_count / connect_total_ns int Total number of Connection.connect() calls and cumulative elapsed time. Recorded even on failure.
execute_count / execute_total_ns int Successful execute() and executemany() calls on sync and async cursors, and cumulative elapsed time. One executemany() call counts once, whatever its path (EXECUTE_BATCH, prepared=True or the per-row loop). Driver-internal requests, such as the escape-mode probe of a new session, are not counted (#899).
fetch_count / fetch_total_ns int Server FETCH requests that fetchone() / fetchmany() / fetchall() send for rows beyond those already buffered, and cumulative elapsed time. Rows that arrive with the execute reply cost no FETCH, so a fetch call served from the buffer is not counted.
close_count / close_total_ns int Connection.close() calls and cumulative elapsed time.

Methods

reset()

def reset(self) -> None

Reset every counter to zero atomically. Useful for measuring a specific code section after a warm-up phase.

__repr__()

Returns a human-readable summary, e.g.:

TimingStats(connect=1 calls, 12.345ms total, 12.345ms avg,
            execute=10 calls, 4.200ms total, 0.420ms avg,
            fetch=10 calls, 1.500ms total, 0.150ms avg,
            close=0 calls)

Async parity

pycubrid.aio.AsyncConnection accepts the same enable_timing keyword and exposes the same timing_stats property with identical semantics. Async durations include event-loop scheduling latency — treat them as end-to-end client-side latency, not pure server time.


Exception Hierarchy

pycubrid implements the full PEP 249 exception hierarchy:

graph TD
    exception[Exception]
    warning[Warning]
    error[Error]
    interface[InterfaceError]
    database[DatabaseError]
    data[DataError]
    operational[OperationalError]
    integrity[IntegrityError]
    internal[InternalError]
    programming[ProgrammingError]
    notsupported[NotSupportedError]

    exception --> warning
    exception --> error
    error --> interface
    error --> database
    database --> data
    database --> operational
    database --> integrity
    database --> internal
    database --> programming
    database --> notsupported
Loading

All exceptions have msg (str) and code (int) attributes. DatabaseError and its subclasses additionally have errno (int | None) and sqlstate (str | None).


Warning

class Warning(Exception):
    def __init__(self, msg: str = "", code: int = 0) -> None

Raised for important warnings (e.g., data truncation during insertion).


Error

class Error(Exception):
    def __init__(self, msg: str = "", code: int = 0) -> None

Base class for all pycubrid errors.


InterfaceError

class InterfaceError(Error)

Raised for errors related to the database interface — calling methods on closed connections/cursors, invalid arguments, etc.


DatabaseError

class DatabaseError(Error):
    def __init__(
        self,
        msg: str = "",
        code: int = 0,
        errno: int | None = None,
        sqlstate: str | None = None,
    ) -> None

Base class for database-side errors. Includes errno and sqlstate for server-reported error details.

An exception raised for a failed row of an executemany() or executemany_batch() batch also has batch_index (int), batch_errors (tuple[tuple[int, DatabaseError], ...]) and batch_rowcounts (tuple[int, ...]), described in Partial failure (#841). On every other DatabaseError they are None. They survive pickle and copy, and str() and repr() do not include them.

error_source (read-only, str | None, #903) says who generated an error the broker reported. The broker puts an indicator before each error code (CAS_ERROR_INDICATOR or DBMS_ERROR_INDICATOR in CUBRID src/broker/cas.h):

error_source Indicator Example
"cas" -1: the CAS itself get_schema_info() with an invalid schema type: errno=-1015
"server" -2: the database server a syntax error (-493), a wrong password (-171), a failed batch row
None none an error the driver raised itself, such as a closed connection or a malformed reply

errno and code keep the number on the wire. The driver does not request the renewed CAS numbering, so a CAS error keeps its old value (-1000 to -1200, the CAS code plus 9000). That range overlaps server codes; error_source tells them apart. CAS_ER_NOT_AUTHORIZED_CLIENT (-1018, a client address rejected by the broker's access control) is sent with the server indicator and reports "server". error_source survives pickle and copy, and is not part of str() or repr().


DataError

class DataError(DatabaseError)

Raised for data processing problems (division by zero, numeric overflow, etc.). Also raised when a fetched character value (CHAR, VARCHAR, NCHAR, ENUM) or column name is not valid in the connection charset (a JSON value: not valid UTF-8), when text sent to the server cannot be encoded with it (nothing of that request is sent), and when a TIMESTAMPTZ/TIMESTAMPLTZ/ DATETIMETZ/DATETIMELTZ value names a zone region the client's IANA time zone database cannot resolve (install tzdata) or an offset outside ±24 hours (#413). The reply was fully read, so with ordinary cursors the connection stays usable; on a later fetch page, rows collected before that page are still returned first (see Data errors on a later fetch page under fetchone(), #507). get_schema_info() retires the connection on any FC9 reply it cannot parse, and the explicit prepared API (pycubrid.compat.native) raises OperationalError and retires the session instead.


OperationalError

class OperationalError(DatabaseError)

Raised for database operation errors (unexpected disconnect, memory errors, transaction failures, connection lost).


IntegrityError

class IntegrityError(DatabaseError)

Raised when relational integrity is affected (foreign key violation, duplicate key, constraint violation), including a DELETE, UPDATE or TRUNCATE blocked by a referencing foreign key.


InternalError

class InternalError(DatabaseError)

Raised for internal database errors (invalid cursor state, out-of-sync transactions).


ProgrammingError

class ProgrammingError(DatabaseError)

Raised for programming errors (SQL syntax errors, table not found, wrong number of parameters, unsupported parameter types).


NotSupportedError

class NotSupportedError(DatabaseError)

Raised when an unsupported method or API is called.


UnknownConnectionOptionWarning

class UnknownConnectionOptionWarning(UserWarning)

A Python warning category, not a PEP 249 exception — it sits outside the hierarchy above and is deliberately distinct from pycubrid.Warning (the PEP 249 database warning, which is raised).

Emitted when a connection constructor receives a keyword it does not recognise. The keyword is still ignored, but the warning names it — with a spelling suggestion when one is close — so a typo such as read_timout=30 is not swallowed silently.

import warnings
import pycubrid

# Strict: an unknown connection option becomes an error.
warnings.simplefilter("error", pycubrid.UnknownConnectionOptionWarning)

# Lenient: silence it (e.g. in a wrapper that forwards arbitrary kwargs).
warnings.simplefilter("ignore", pycubrid.UnknownConnectionOptionWarning)

See Unknown Options.


Error Classification

pycubrid primarily classifies server errors by their numeric error code, not by message wording. Native -631 (ER_NULL_CONSTRAINT_VIOLATION), -922 (ER_FK_INVALID, inserting or updating a child row without a parent), -924 (ER_FK_RESTRICT, deleting or updating a referenced parent row) and -1284 (ER_TRUNCATE_PK_REFERRED, truncating a referenced parent table on CUBRID 11.4; 10.2 reports -924) raise IntegrityError with SQLSTATE 23000, even when the message is localized. Dropping a referenced primary key (-923, ER_FK_CANT_DROP_PK_REFERRED) rejects a schema change and remains DatabaseError. Single statements and per-statement batch failures preserve the original numeric value in both code and errno. Unknown codes remain DatabaseError rather than being classified from constraint-like message text.

Native -493 (ER_PT_SYNTAX) and -494 (ER_PT_SEMANTIC) use ProgrammingError / 42000, with syntax and semantic descriptions respectively. -493 can accompany either malformed SQL or an unknown class; the code alone does not establish a missing-table SQLSTATE. Native -671 (ER_CSS_RECV_OR_SEND) uses OperationalError / 08S01, not an integrity error. Native -4 (ER_INTERRUPTED, an interrupted statement on a live session) uses OperationalError / HY008 (operation canceled), not the 08S01 communication link failure that pools and ORMs treat as a dead connection (#895). The connection stays open, but the server may have aborted the interrupted transaction (after cubrid killtran the next statement reports -111), so roll back before reusing it. Native -3 (ER_OUT_OF_VIRTUAL_MEMORY) and the I/O errors -13, -14 and -15 (ER_IO_READ, ER_IO_WRITE, ER_IO_WRITE_OUT_OF_SPACE) use OperationalError, and -2 (ER_GENERIC_ERROR) uses InternalError. The authentication failures -165 (ER_AU_INVALID_USER) and -171 (ER_AU_INVALID_PASSWORD) use OperationalError / 28000 (invalid authorization specification) in both drivers (#903); earlier releases raised DatabaseError / HY000. SQLSTATE values are driver translations of the native meanings. Batch dispatch uses the same known-code SQLSTATE lookup as single statements, retaining the class default for unknown codes.

Consumers must not infer missing tables from -493 alone or from generated description text. The corresponding SQLAlchemy reflection correction is tracked in sqlalchemy-cubrid #454.

The native identifiers are defined in the CUBRID server header src/base/error_code.h (release/11.4; release/10.2 has the same values except -1284) and its official CCI copy. Every code in the driver's tables is one of these server codes. Client-side driver codes, such as CCI CCI_ER_* or JDBC UErrorCode, are never sent by the broker and are not listed. Protocol error responses with the generic failure code -1 (ER_FAILED) retain the existing message-based fallback below; per-statement batch dispatch remains code-based:

Keywords in error message Exception raised
unique, duplicate, foreign key, constraint violation IntegrityError
syntax, unknown class, does not exist, not found ProgrammingError
All others DatabaseError

get_error_description()

pycubrid.get_error_description(code: int) -> str | None

Returns the driver's short English description of a native CUBRID server error code, or None when the code is not in the driver's table. The table is a fixed subset built into the driver and the lookup does not query the server. A missing entry only means there is no description: the lookup never raises, and a server error with an unlisted code is still raised as DatabaseError.

import pycubrid

pycubrid.get_error_description(-493)    # 'Syntax error'
pycubrid.get_error_description(-4)      # 'Interrupted'
pycubrid.get_error_description(-99999)  # None

The same text appears in the repr() of an exception that carries a known code, for example ProgrammingError('...', errno=-493, description='Syntax error'). It is a diagnostic aid: classify errors by exception class and by code/errno, as described above, not by this text.


Type Objects

PEP 249 type objects for comparing cursor.description type codes:

Type Object Description Matching CUBRID types
STRING String types CHAR, STRING, NCHAR, VARNCHAR, ENUM, CLOB, JSON
BINARY Binary types BIT, VARBIT, BLOB
NUMBER Numeric types SHORT, INT, BIGINT, FLOAT, DOUBLE, NUMERIC, MONETARY
DATETIME Date/time types DATE, TIME, DATETIME, TIMESTAMP
ROWID Row ID types OBJECT
import pycubrid

cur.execute("SELECT name FROM users")
type_code = cur.description[0][1]

if type_code == pycubrid.STRING:
    print("It's a string column")

Type Constructors

PEP 249 constructor functions:

Constructor Return Type Description
Date(y, m, d) datetime.date Construct a date
Time(h, m, s) datetime.time Construct a time
Timestamp(y,m,d,h,m,s) datetime.datetime Construct a timestamp
DateFromTicks(t) datetime.date Date from Unix ticks
TimeFromTicks(t) datetime.time Time from Unix ticks
TimestampFromTicks(t) datetime.datetime Timestamp from Unix ticks
Binary(s) bytes Binary string from bytes
import pycubrid

d = pycubrid.Date(2025, 1, 15)
t = pycubrid.Time(14, 30, 0)
ts = pycubrid.Timestamp(2025, 1, 15, 14, 30, 0)
b = pycubrid.Binary(b"\x00\x01\x02")

Typed Collection Parameters

class Set(elements: Iterable[Any] = ())
class Multiset(elements: Iterable[Any] = ())
class Sequence(elements: Iterable[Any] = ())

Defined in pycubrid.types and exported from pycubrid (added in #567). Each wraps its elements in an immutable tuple and binds as one typed CUBRID collection literal through execute()/executemany() on ordinary sync and async cursors. Plain set/list/tuple parameters stay rejected.

Class Literal Server semantics
Set SET{...} duplicates removed, order not kept
Multiset MULTISET{...} duplicates kept, order not kept
Sequence SEQUENCE{...} (same type as LIST{...}) duplicates and order kept
Member Description
.elements The stored tuple of elements
iter(), len() Iterate over / count the elements
==, hash() Equal only to the same class with equal elements (order-sensitive for all three)
  • Elements take the scalar parameter types (None, bool, int, float, Decimal, str, bytes, bytearray, date, time, datetime) and are rendered by the same hardened renderer; anything else, including a nested collection, raises ProgrammingError.
  • A single str/bytes/bytearray argument raises TypeError; subclassing raises TypeError; setting an attribute raises AttributeError.
  • A dict argument raises TypeError for all three classes (its keys would be used silently and its values dropped). Sequence additionally raises TypeError for a set/frozenset argument, since its iteration order is not guaranteed; Set and Multiset accept a set/frozenset.
  • The instances are safe to copy.copy() (always returns the same object), copy.deepcopy() (the same object when every element is itself immutable; an independent copy, with independently copied elements, when an element such as bytearray is mutable) and pickle (round-trips to an equal instance). Re-invoking __init__ on an existing instance is a no-op and cannot mutate it.
  • Fetched collections are not returned as these classes: with decode_collections=True they stay frozenset (SET) and list (MULTISET/SEQUENCE).
  • Sequence is also a name in typing/collections.abc; from pycubrid import * shadows it (and Set) with these classes. Prefer an explicit import, e.g. from pycubrid.types import Sequence as CubridSequence, when both are needed in the same module.
from pycubrid import Multiset, Sequence, Set

cur.execute(
    "INSERT INTO t (tags, words, steps) VALUES (?, ?, ?)",
    (Set([1, 2, 3]), Multiset(["a", "a"]), Sequence([3, 1, 2])),
)

See Parameter Binding.