Complete API documentation for pycubrid — a pure Python DB-API 2.0 driver for CUBRID.
- Module-Level Attributes
- Module-Level Constructor
- Async Module Constructor
- Explicit Native Compatibility Subset
- Connection Class
- Cursor Class
- AsyncConnection Class
- AsyncCursor Class
- Lob Class
- TimingStats Class
- Exception Hierarchy
- Type Objects
- Type Constructors
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"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,
) -> ConnectionCreate 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 |
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.
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_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 (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,euckrandiso88591, and a CUBRID locale such as"ko_KR.euckr"(the part after the dot is used), normalized to the Python codec name ("euc_kr");Nonemeans the default. Validated before any socket work: a non-string raisesTypeError; an unknown codec, CUBRIDbinaryand codecs that are not ASCII-transparent (UTF-16/32, UTF-7, Shift_JIS, Big5, GBK, GB18030, CP949, ISO-2022, ...) raiseValueError; credentials the codec cannot encode raiseDataError. - Text that cannot be encoded raises
DataErrorbefore any byte of that request is sent, on every path (ordinary andcompat.nativecursors,get_schema_info()), and the session stays usable. Witheuc_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
DataErrornaming 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 raisesOperationalError. Error text and LOB file locators are decoded witherrors="replace". JSONvalues are read back as UTF-8, but a JSON parameter is SQL text and is encoded with the connection codec (an emoji in JSON undereuckrraisesDataErroron insert).NUMERIC, timezone names, the version string and LOB contents are not affected (CLOBbytes are in the column charset);pycubrid.Binary(str)always encodes as UTF-8.- The broker does no conversion, so a
CHARSET utf8column in an EUC-KR database raisesDataErrorundercharset="euckr"; convert it in SQL withCAST(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,
)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()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.
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.
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()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.
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.
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()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.
datamust be atuple(anything else raisesInterfaceError, as in the official driver). Elements arestr,Nonefor a NULL element, or, whentypeis INT,intin signed 64-bit range (others raiseDataError);intand 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 raiseProgrammingErrororDataError, and the set keeps its previous value. Astrsubclass is copied as plain text first.typeis a CCI element type code such as CHAR (1), STRING/VARCHAR (2), NUMERIC (7), INT (8) or DATE (13), for exampleCUBRIDDataType.NUMERICfrompycubrid.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, raiseNotSupportedError, and a non-intcode raisesInterfaceError.- As in the official driver, every element is sent as a STRING (
2) element whatevertypeis, and the server converts it to the column's element type. Anintelement is sent as its decimal text, soimports((1, 2), INT)sends the same bytes as the officialimports(('1', '2'), INT). A value the column cannot hold fails on the server atexecute(), and the prepared handle stays usable. Because the elements are strings, an untypedSETcolumn stores them as strings. kindis 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. Passkind=MULTISETto keep duplicates orkind=SEQUENCEto keep order and duplicates. CUBRID 10.2 and 11.4 brokers reject the MULTISET bind kind (error -454), sokind=MULTISETis 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 laterimports()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()raisesInterfaceErrorfor anything that is not anative.set, andProgrammingErrorfor a bad index or a set imported under another charset. Stale-session and closed-cursor rules are the same as forbind_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()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
Noneelement binds a NULL element; the official wrapper sends the text'None', or raisesUnboundLocalErrorwhen inference meetsNonefirst. - BIT/VARBIT collection elements are not supported (
NotSupportedError); the official wrapper convertsbytesto bit strings. - Only
list,tuple,setandfrozensetarguments 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(officialIntegrityError). - 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 isProgrammingError,byteswith a non-VARBIT code isNotSupportedError); the official wrapper sends theirstr()text. - The scalar subset is otherwise unchanged: the official wrapper also binds
bool,float,Decimal, dates andbytesscalars and skipsNonearguments, while this wrapper rejects those types and bindsNoneas SQL NULL. Since #840 itsintarguments past signed 32-bit bind BIGINT or NUMERIC, as nativebind_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()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, likefetch_row()(including FETCH paging), and puts the handle at the one-based columncolintolob. The type of that column decides BLOB or CLOB. Acolthat is not anintraisesTypeErrorfirst, as the official argument parser does. At the end of the result it returnsNoneand 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 raisesProgrammingErrorwithout consuming the row, and a closedlobor alobcreated by another connection raisesInterfaceError. It returnsNone, as in the official driver. A NULL cell consumes the row and leaveslobwithout a value. If the server's cell atcolis not a handle of the column's LOB type,DataErroris raised without consuming the row and the connection stays usable; a handle with damaged framing raisesOperationalErrorand the physical session is retired.bind_lob(index, lob)binds the handlelobholds at that moment and sends nothing untilexecute(). 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 anative.lobraisesTypeError(as official; a non-int index is reported first, as the official argument parser does), and an out-of-range index raisesProgrammingError.InterfaceErroris raised, before any request, for a closedlob, alobwithout a value (never filled, or filled from a NULL cell), and a closed cursor or connection. Bind SQL NULL withbind_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 beforeclose()stays valid. A closedlobcannot be filled or bound again; create a new one withconn.lob().lob.write(data, type="B", /) -> Noneacceptsstr(encoded as UTF-8, independent of the connection charset) orbytes. 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 raisesNotSupportedErrorbefore 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, /) -> strreads 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 ordinaryLob.read()for arbitrary binary bytes. A split UTF-8 character raisesUnicodeDecodeErrorafter 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, /) -> intchanges the byte position.native.SEEK_SET,SEEK_CURandSEEK_ENDare exported; as in the official driver, SEEK_END computessize - offset(a positive offset moves backward).seek(0, SEEK_CUR)reports the current position; there is no separatetell(). 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; useseek(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 latercommit(). 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 freshlobfor 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 officialSEEK_ENDdirection, and replace a closed holder withconn.lob()rather than writing after close. Ordinarypycubrid.lob.Lob.read(length, offset=0)andwrite(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()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 raisesValueError; invalid argument types raiseTypeError. Import accepts only"B"/"b"or"C"/"c"; other strings raiseInterfaceErrorwith.code == -30006before 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 raisesInterfaceError(-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
InterfaceErrorcode-30009, input read/close failures-30016, and output write/flush/close/replace failures-30017. The originalOSError, when present, is the cause. These errors keep pycubrid's message-onlyargs, not the official integer/message pair; no newerrnoor 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 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,
) -> AsyncConnectionCreate and open an async connection.
- Returns a connected
AsyncConnection. - Accepts the same collection / JSON decoding kwargs and the same
charsetoption aspycubrid.connect(). - Supports
autocommit=True, applied automatically once the connection is established.AsyncConnectionitself now acceptsautocommitdirectly 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 throughawait conn.set_autocommit(...)instead of a property setter. - Accepts the same
sslparameter aspycubrid.connect():True,False/None, or a customSSLContext; whenTrue, the default verified context enforces a TLS 1.2 minimum. Async TLS uses CUBRID's STARTTLS-style upgrade — theCUBRShandshake is sent in plaintext, then the transport is upgraded viaasyncio.AbstractEventLoop.start_tls()(bounded byssl_handshake_timeout) beforeOPEN_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())pycubrid.connection.Connection
Represents a single connection to a CUBRID database via the CAS broker protocol.
class Connection:
def __init__(
self,
host: str,
port: int,
database: str,
user: str,
password: str,
autocommit: bool = False,
**kwargs: Any,
) -> NoneNote: Do not instantiate
Connectiondirectly. Usepycubrid.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. |
def connect(self) -> NoneEstablish 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.
def close(self) -> NoneClose 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 closeddef commit(self) -> NoneCommit 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.
def rollback(self) -> NoneRoll 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.
def cursor(self) -> CursorCreate 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()def get_server_version(self) -> strReturn 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"def get_last_insert_id(self) -> str | NoneReturn 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 NoneSuccessful 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.
def ping(self, reconnect: bool = True) -> boolOn 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
Truewhen the CAS connection is alive.CAS_INFO[0]=0means OUT_TRAN, not a disconnected session, and does not itself cause a reconnect. Ordinary requests after an OUT_TRAN reply are preceded by an automaticCHECK_CASand reconnect only when it fails (#485); a healthyping()also counts as that check. - With
reconnect=False, checks an open socket without reconnecting and returnsFalsewhen 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 ifCHECK_CASreturns a negative code (broken CAS-to-DB link).reconnect=Falsereports the negative response asFalsewithout reconnecting and closes that broken session (later calls raiseInterfaceError), 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 detectedno_backslash_escapesmode is probed again on a new physical session before use; an explicitTrue/Falseremains pinned. A healthy same-session ping does not probe. Probe failure retires the replacement and returnsFalse, without guessing a mode or replaying SQL. - A failed reconnect returns
Falsewhatever the error, including a server rejection ofOPEN_DATABASEsuch as a wrong password (-171) or an unknown user (-165); before #896 the sync driver raised thatDatabaseError. The async driver behaves the same.
if not conn.ping():
raise RuntimeError("database connection is unavailable")def create_lob(self, lob_type: int) -> LobCreate 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:
Lobobjects cannot be passed as query parameters toexecute()or the defaultexecutemany(). Insert strings/bytes directly into CLOB/BLOB columns instead. See TYPES.md for details. The one exception isexecutemany(..., prepared=True)on the sync cursor with autocommit off, which binds aLobof the same connection by its handle.
def get_schema_info(
self,
schema_type: int,
table_name: str = "",
pattern_match_flag: int = 1,
*,
arg2: str | None = None,
) -> GetSchemaPacketCreate 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) -> 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.
@property
def autocommit(self) -> bool
@autocommit.setter
def autocommit(self, value: bool) -> NoneGet 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@property
def timing_stats(self) -> TimingStats | NoneReturn 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 987000The 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 countersSee Timing & Profiling Hooks for the full guide.
Connection implements the context manager protocol (__enter__ / __exit__).
- On successful exit: calls
commit()thenclose(). - On exception: calls
rollback()thenclose(). - 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 herepycubrid.cursor.Cursor
Represents a database cursor for executing SQL statements and fetching results.
class Cursor:
def __init__(self, connection: Connection) -> NoneNote: Do not instantiate
Cursordirectly. Useconnection.cursor()instead.
def execute(
self,
operation: str,
parameters: Sequence[Any] | None = None,
) -> CursorPrepare 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:
InterfaceErrorif the cursor is closedProgrammingErroron 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
dictare not supported for parameter binding. Passing a mapping raisesProgrammingError("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{...} |
def executemany(
self,
operation: str,
seq_of_parameters: Sequence[Sequence[Any]],
*,
prepared: bool = False,
) -> CursorExecute 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) # 3When 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) # -1Exceptions 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).
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,DELETEorMERGEare accepted (a comment right after the verb, as inINSERT/*x*/INTO, ends the keyword, #898); anything else (includingSELECT,CALLandWITH ... INSERT) raisesNotSupportedError. There is no fallback to the literal batch. Every placeholder must be a value the server can type: in aMERGEsource, writeCAST(? 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-emptybytes/bytearray,date,timeanddatetime. Each value carries its own type byte, so one column may mix types from row to row.pycubrid.types.Set,MultisetandSequenceare sent as collections of string elements (str,intorNone) that the server converts; aMultisetis sent as a SEQUENCE, because the broker rejects the MULTISET bind kind.pycubrid.Lobvalues are accepted on the sync cursor with autocommit off (see below). - Checks before send.
seq_of_parametersmay be any iterable of rows (a generator is consumed once; the annotation staysSequence[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,DataErrororNotSupportedError) with its position, for examplerows[2][1]: nan and inf are not supported by CUBRID; empty rows raiseNotSupportedError. The value itself never appears in the message. AfterPREPARE, a server marker count different from the row length raisesProgrammingErrorand 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 androllback()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) androwcountstays-1. On successrowcountis the sum of the per-row counts.lastrowidisNone. 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. Itsbatch_indexis the interrupted row,batch_errorsstill starts with the first failing row, andbatch_rowcountsends 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
Lobfrom this connection'screate_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 aLobraisesNotSupportedErrorbefore anything is sent: turn autocommit off and callcommit()after the batch. ALobthat is closed, belongs to another connection or was created before a reconnect raisesInterfaceError. The async cursor rejectsLobvalues withNotSupportedError. - Handles. The prepared statement handle is released like any other result:
with the next statement on the cursor, by
commit()/rollback(), or byclose(). With autocommit on and broker statement pooling, the release rides on the next statement's request (aPREPARE_AND_EXECUTEor the next batch'sPREPARE) instead of a separateCLOSE_REQ. With autocommit on, a broker that closes its connection right afterPREPAREmakes the batch fail withOperationalError; 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
PREPAREwas sent: the session is retired (the reply could arrive later), andEXECUTE_ARRAYis 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.
def nextset(self) -> NoneDB-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 Nonedef 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 = INSERTNote:
executemany_batchis 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, leavesdescription=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 carriesbatch_index,batch_errorsandbatch_rowcountswith positions insql_list(see Partial failure). If closing the previous handle fails, no batch is sent and the handle remains tracked. SQL passed directly toexecutemany_batchis 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 thatexecute()/executemany()render from parameters is rejected when the session changed after binding.
def fetchone(self) -> tuple[Any, ...] | NoneFetch 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 = rowNote on explicit connection recovery: A normal
CAS_INFO[0]=0response does not replace the session or invalidate a cursor. If an actual connection failure is followed by successfulping(reconnect=True)recovery while a result is only partially buffered, rows already in the buffer remain readable. Once exhausted, subsequentfetchone()/fetchmany()/fetchall()calls raiseOperationalErrorwith the existing messageresult set lost due to broker reconnect mid-fetchbecause the former server-side handle is no longer valid. Re-execute the query explicitly; it is never replayed automatically.execute()andclose()reset the invalidation flag.
Transaction-boundary fetching:
commit()androllback()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 asyncfetchone()/fetchmany()/fetchall()raiseInterfaceErrorrather than silently reporting EOF. Afetchmany()orfetchall()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 separateOperationalError.
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()orfetchall()call (or iteration step) that reaches the page raisesDataError. 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 afetchmany()orfetchall()after the error returns those rows (possibly fewer than requested). After that, every fetch raises the sameDataErroragain without requesting the page, untilexecute()orclose(); 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.
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 rowsDeprecated:
fetchmany(size)withsize <= 0. Starting with pycubrid 1.13.0, sync and asyncfetchmany()emit aDeprecationWarningthat points at the calling line when an explicit integersizeis 0 or negative. The result is unchanged in 1.x ([], no row consumed, no FETCH sent). pycubrid 2.0 will raiseProgrammingError("size must be a positive integer")instead, the same rule thearraysizesetter already applies (#678). Omitted/Nonesizes and positive integers do not warn, and thepycubrid.compatwrapper keeps returning[]forsize <= 0without 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 []
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)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()def setinputsizes(self, sizes: Any) -> NoneDB-API no-op. Accepted for compatibility.
def setoutputsize(self, size: int, column: int | None = None) -> NoneDB-API no-op. Accepted for compatibility.
def close(self) -> NoneClose the cursor and release the active query handle. Calling on an already-closed cursor is a no-op.
@property
def description(self) -> tuple[DescriptionItem, ...] | NoneReturn 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]}")@property
def rowcount(self) -> intNumber 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.
@property
def lastrowid(self) -> int | NoneLast 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@property
def arraysize(self) -> int
@arraysize.setter
def arraysize(self, value: int) -> NoneDefault 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.
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.
with conn.cursor() as cur:
cur.execute("SELECT 1")
print(cur.fetchone())
# cur is automatically closedpycubrid.aio.connection.AsyncConnection
Async counterpart to Connection for use with asyncio, with a similar surface rather than full method-for-method parity.
| 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()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).
async def ping(self, reconnect: bool = True) -> boolOn a healthy session, perform a lightweight native CHECK_CAS health check
without SQL. Recovery can execute the read-only escape-mode probe.
- Returns
Truewhen the CAS connection is alive. - Issues the native
CHECK_CASround-trip when the socket is open.CAS_INFO[0]=0denotes OUT_TRAN after a transaction boundary, not a disconnected session. Ordinary requests after an OUT_TRAN reply are preceded by an automaticCHECK_CASand reconnect only when it fails (#485). - With
reconnect=False, does not reconnect; returnsFalsewhen 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 whenCHECK_CASreturns a negative code (broken CAS-to-DB link).reconnect=Falsereports the negative response asFalsewithout reconnecting and closes that broken session (later calls raiseInterfaceError), as the sync driver does. Only explicitly set autocommit is restored; arbitrary SQL is never replayed. Automaticno_backslash_escapesdetection runs on each new physical session, not on healthy same-session checks; explicitTrue/Falseremains pinned. Probe failure retires the replacement and returnsFalse. 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
Falsewhatever the error, including a server rejection ofOPEN_DATABASEsuch as a wrong password (-171), as in the sync driver.
if not await conn.ping(reconnect=False):
await conn.ping(reconnect=True)pycubrid.aio.cursor.AsyncCursor
Async counterpart to Cursor.
| 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 Nonepycubrid.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.
@classmethod
def create(cls, connection: Connection, lob_type: int) -> LobCreate 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.
def write(self, data: bytes | bytearray | memoryview, offset: int = 0) -> intWrite 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.
def read(self, length: int, offset: int = 0) -> bytesRead 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.
@property
def lob_handle(self) -> bytesThe 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).
@property
def lob_type(self) -> intThe LOB type code (23 = BLOB, 24 = CLOB).
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"])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.
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. |
def reset(self) -> NoneReset every counter to zero atomically. Useful for measuring a specific code section after a warm-up phase.
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)
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.
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
All exceptions have msg (str) and code (int) attributes. DatabaseError and its subclasses additionally have errno (int | None) and sqlstate (str | None).
class Warning(Exception):
def __init__(self, msg: str = "", code: int = 0) -> NoneRaised for important warnings (e.g., data truncation during insertion).
class Error(Exception):
def __init__(self, msg: str = "", code: int = 0) -> NoneBase class for all pycubrid errors.
class InterfaceError(Error)Raised for errors related to the database interface — calling methods on closed connections/cursors, invalid arguments, etc.
class DatabaseError(Error):
def __init__(
self,
msg: str = "",
code: int = 0,
errno: int | None = None,
sqlstate: str | None = None,
) -> NoneBase 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().
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.
class OperationalError(DatabaseError)Raised for database operation errors (unexpected disconnect, memory errors, transaction failures, connection lost).
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.
class InternalError(DatabaseError)Raised for internal database errors (invalid cursor state, out-of-sync transactions).
class ProgrammingError(DatabaseError)Raised for programming errors (SQL syntax errors, table not found, wrong number of parameters, unsupported parameter types).
class NotSupportedError(DatabaseError)Raised when an unsupported method or API is called.
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.
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 |
pycubrid.get_error_description(code: int) -> str | NoneReturns 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) # NoneThe 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.
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")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")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, raisesProgrammingError. - A single
str/bytes/bytearrayargument raisesTypeError; subclassing raisesTypeError; setting an attribute raisesAttributeError. - A
dictargument raisesTypeErrorfor all three classes (its keys would be used silently and its values dropped).Sequenceadditionally raisesTypeErrorfor aset/frozensetargument, since its iteration order is not guaranteed;SetandMultisetaccept aset/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 asbytearrayis mutable) andpickle(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=Truethey stayfrozenset(SET) andlist(MULTISET/SEQUENCE). Sequenceis also a name intyping/collections.abc;from pycubrid import *shadows it (andSet) 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.