CUBRID-Python Driver Compatibility¶
This document describes the tested compatibility matrix between sqlalchemy-cubrid,
the CUBRID Python driver (CUBRIDdb), and CUBRID server versions.
Table of Contents¶
- Server, driver, and distribution versions
- Driver Overview
- Version Compatibility Matrix
- Exception Hierarchy
- Driver API Usage
- Known Issues
- Installation Notes
Server, driver, and distribution versions¶
Some CUBRID Python driver release numbers resemble CUBRID Database product versions. The names reflect related release families, but the numbers identify different components. Matching major/minor numbers is not a requirement for connecting, nor proof that every cross-version combination works.
| Identifier | Example | What the version identifies |
|---|---|---|
| CUBRID database server | 11.4.1 |
The running database; check with SELECT VERSION() |
| CUBRIDdb upstream source release | v11.3.0.51 |
A published GitHub source release of the Python C-extension driver, not a CUBRID server release or a PyPI 11.3 package |
| CCI client library | 11.2.0 |
Native C client interface used by that specific upstream driver release |
| PyPI distribution release | CUBRID-Python 9.3.0 |
The version listed on PyPI for the C-extension driver package |
| PyPI source archive revision | 9.3.0.0001, 9.3.0.0002 |
Filenames for two source archives within the same PyPI 9.3.0 release, uploaded in 2014 and 2015 respectively |
| Installed C-extension version | For example, 11.3.0.0001 |
_cubrid.__version__ in a particular build; its fourth field can be a Git commit count rather than the upstream source tag suffix |
| SQLAlchemy dialect version | For example, sqlalchemy-cubrid 1.x |
This project's independently published Python package version |
The official CUBRID Python v11.3.0.51 release notes (published 2025-09-08) state that it was built with CCI 11.2.0 and conforms to CUBRID server 11.4.1. That is an explicit example of different version numbers in one working release context. Its GitHub release has source archives but no attached binary release assets; this is not a CUBRID-Python 11.3 release on PyPI.
The PyPI CUBRID-Python project describes its 9.3.0 package as a connector for CUBRID Database 9.3.0. This is historical naming context, not proof that it cannot connect to another server version. An earlier 9.3.0.1 driver / CUBRID 11.4 test did connect but exposed a BIGINT conversion difference; support policy, successful connection, tested combinations, and known limitations must be kept separate.
Connection path¶
flowchart TD
app["Application / SQLAlchemy Core & ORM"] --> dialect["sqlalchemy-cubrid dialect"]
dialect --> pure["pycubrid / pycubrid.aio"]
dialect --> native["CUBRIDdb (_cubrid C extension)"]
native --> cci["CCI client library"]
cci --> cas["CUBRID Broker / CAS"]
pure --> cas
cas --> database["CUBRID database server"]
The [cubrid], [cubriddb] and [pycubrid] extras are dependency installation choices, not server versions. cubrid:// and cubrid+cubriddb:// select CUBRIDdb; cubrid+pycubrid:// and cubrid+aiopycubrid:// select the respective pycubrid drivers. Neither URL embeds a driver release number.
Check the versions in an environment¶
from importlib import import_module
from importlib.metadata import PackageNotFoundError, version
for name in ("sqlalchemy-cubrid", "pycubrid", "CUBRID-Python"):
try:
print(f"{name}: {version(name)}")
except PackageNotFoundError:
print(f"{name}: not installed")
try:
cubriddb = import_module("CUBRIDdb")
except ImportError:
print("CUBRIDdb C extension: not importable")
else:
extension = getattr(cubriddb, "_cubrid", None)
print("CUBRIDdb C extension:", getattr(extension, "__version__", "unknown"))
CUBRID-Python and cubrid_python are spelling variants of the normalized distribution name; they do not mean two independent drivers. To inspect the running server instead, execute SELECT VERSION() through a live connection. Record the upstream Git tag or source commit separately when you built CUBRIDdb yourself. Do not infer server version, source tag, or full feature compatibility from the installed package version alone.
Driver Overview¶
| Property | Value |
|---|---|
| Supported install | Built from source, cubrid-python v11.3.0.51 or later (how) |
| PyPI package | CUBRID-Python, newest release 9.3.x (2015): untested, not supported (details) |
| Import name | CUBRIDdb |
| Type | C extension (CPython only) |
| DBAPI level | DB-API 2.0 (PEP 249) |
| Parameter style | qmark |
| Source | github.com/CUBRID/cubrid-python |
The driver wraps the CUBRID CCI (C Client Interface) library. It is not a pure
Python driver and requires compilation against the CCI headers.
The source-built CUBRIDdb driver above is supported by this dialect; the deprecated
[cubrid] / [cubriddb] installation extras select the untested PyPI package,
not a deprecated driver interface. For new projects, pycubrid is recommended
because it needs no CUBRID native libraries and avoids the CUBRIDdb limitations below.
Version Compatibility Matrix¶
Tested Configurations¶
| sqlalchemy-cubrid | CUBRID-Python Driver | CUBRID Server | Python | Status |
|---|---|---|---|---|
| 0.4.0 | v11.3.0.51 | 11.4 | 3.10 – 3.14 | ✅ Tested in CI |
| 0.4.0 | v11.3.0.51 | 11.2 | 3.10 – 3.14 | ✅ Tested in CI |
| 0.4.0 | v11.3.0.51 | 11.0 | 3.10 – 3.14 | ✅ Tested in CI |
| 0.4.0 | v11.3.0.51 | 10.2 | 3.10 – 3.14 | ✅ Tested in CI |
| 0.3.x | v11.3.0.51 | 10.2 – 11.4 | 3.10 – 3.13 | ✅ Tested |
| any | PyPI CUBRID-Python 9.3.x |
any | any | ❌ Not tested, not supported (details) |
CUBRID Server Version Support¶
| Server Version | Driver Version | Notes |
|---|---|---|
| 11.4 | v11.3.0.51 | Latest stable |
| 11.2 | v11.3.0.51 | LTS release |
| 11.0 | v11.3.0.51 | Legacy support |
| 10.2 | v11.3.0.51 | Minimum supported |
| 12.x | — | Not yet released; will be tested when available |
Python Version Support¶
| Python | Status | Notes |
|---|---|---|
| 3.11 | ✅ Fully supported | Minimum version |
| 3.12 | ✅ Fully supported | |
| 3.13 | ✅ Fully supported | |
| 3.14 | ✅ Fully supported | |
| 3.15 | ✅ Fully supported | Standard GIL builds. The stock cubrid-python build fails with GCC 14 on Python 3.14 and 3.15 alike (see below) |
| 3.10 | ❌ Not supported from 1.10.0 | Last supported in 1.9.x |
GCC 14 and the official driver build: cubrid-python v11.3.0.51 builds with
the CI recipe on ubuntu-latest, where the compiler reports
-Wincompatible-pointer-types and -Wint-conversion diagnostics in
python_cubrid.c as warnings. GCC 14 makes them errors by default, so the same
recipe fails to compile there on Python 3.14 and 3.15 alike; it is a toolchain
change, not a Python 3.15 one. A contributor build with
CFLAGS="-Wno-error=incompatible-pointer-types -Wno-error=int-conversion" and no
source change produced a working 3.15 wheel (recorded in #666). This project does
not add those flags to CI; a runner image that moves to GCC 14 needs that decision
first.
Exception Hierarchy¶
CUBRIDdb 11.3.0.51 (module _cubrid) provides every PEP 249 exception class
except Warning:
graph TD
base["BaseException"] --> exc["Exception"]
exc --> err["CUBRIDdb.Error (Base DBAPI error)"]
err --> iface["CUBRIDdb.InterfaceError (Driver-level errors)"]
err --> db["CUBRIDdb.DatabaseError (Server-level errors)"]
db --> data["CUBRIDdb.DataError"]
db --> op["CUBRIDdb.OperationalError"]
db --> integ["CUBRIDdb.IntegrityError"]
db --> internal["CUBRIDdb.InternalError"]
db --> prog["CUBRIDdb.ProgrammingError"]
db --> ns["CUBRIDdb.NotSupportedError (Unsupported operations)"]
The class a server error gets comes from the driver's error-code mapping. Observed on CUBRID 11.4:
| Error (native code) | CUBRIDdb class |
|---|---|
| Syntax error or unknown table (-493) | ProgrammingError |
| NOT NULL (-631), foreign key (-922), unique (-670) | IntegrityError |
| Division by zero (-494) | IntegrityError |
Parent DELETE/UPDATE restricted by a foreign key (-924) |
IntegrityError |
TRUNCATE of a referenced table (-1284; CUBRID 10.2 and 11.0 report -924, IntegrityError) |
DatabaseError |
Failed CAST (-181) |
DatabaseError |
A zero DATE/DATETIME ('0000-00-00') in a result |
SystemError, not a DB-API error: SQLAlchemy does not wrap it, and the connection stays usable |
Reading a result after rollback() (CCI -20040) |
InterfaceError |
SQLAlchemy wraps the class it receives, so cubrid:// raises
sqlalchemy.exc.IntegrityError for constraint violations; see
Known Issue 9 for
pycubrid. The sqlalchemy-cubrid dialect uses string-based message matching
to distinguish disconnect errors from other failures.
Driver API Usage¶
The dialect relies on these driver-specific APIs:
Connection Methods¶
| Method | Purpose | Used by |
|---|---|---|
conn.ping() |
Check connection liveness | CubridDialect.do_ping() |
conn.get_last_insert_id() |
Get auto-increment value | CubridExecutionContext.get_lastrowid() |
conn.set_autocommit(bool) / conn.autocommit |
Control autocommit | CubridDialect.on_connect(), set_isolation_level() (AUTOCOMMIT and back), detect_autocommit_setting() |
conn.cursor() |
Create cursor | Standard DB-API |
Error Code Extraction¶
# Error codes are in exception.args[0]
try:
cursor.execute("invalid sql")
except CUBRIDdb.DatabaseError as e:
code = e.args[0] # int
The dialect's _extract_error_code() reads only an integer args[0]. A string
args[0] never carries a code, even when it starts with a number (e.g.
"-20004 rows rejected ..." quoted from application data); releases up to 1.8.0
parsed such a leading number as a code and could invalidate a working connection
(#608).
pycubrid keeps only the message in args and the server error code in errno
(and code). is_disconnect() reads errno for the server codes listed in
Known Issue 1. pycubrid's
str() also appends a description of errno (for example Communication error
for -671; pycubrid 1.13.0 describes -4 as Interrupted with SQLSTATE HY008, while
earlier releases reported it as Communication error with 08S01), so the message
patterns are matched against args[0], the driver's own message, instead.
Known Issues¶
1. Disconnect detection does not use OperationalError¶
CUBRIDdb 11.3.0.51 defines OperationalError (see
Exception Hierarchy), but the dialect's
is_disconnect() does not classify disconnects by exception class. Instead, it uses:
- String pattern matching against 17 known disconnect messages (14 shared, 3 from pycubrid)
- Numeric error code matching, on args[0] (CUBRIDdb only), for the CCI and CAS
codes of a dead or unusable connection: -20004 (CCI_ER_COMMUNICATION), -10003
(CAS_ER_COMMUNICATION; CCI treats both as communication errors), -20002
(CCI_ER_CON_HANDLE), -20016 (CCI_ER_CONNECT) and -10002
(CAS_ER_NO_MORE_MEMORY; the CAS closes the connection after sending it). Releases
up to 1.8.0 listed -4, -10005, -10007, -21003 and -21005 instead (#572): -4 is the
server's ER_INTERRUPTED (a query interrupted by KILL QUERY), so CUBRIDdb
invalidated a working connection; -10005 and -10007 are CAS_ER_TRAN_TYPE and
CAS_ER_NUM_BIND; -21003 and -21005 are CUBRID JDBC codes that neither Python
driver raises. -20004, which CUBRIDdb raises when its CAS dies mid-transaction, and
-10002 were missing (#578).
- Numeric error code matching, on both drivers, for the server errors that make the
broker reset the CAS because its session with cub_server is gone: -111
(ER_TM_SERVER_DOWN_UNILATERALLY_ABORTED), -199 (ER_NET_SERVER_CRASHED), -224
(ER_OBJ_NO_CONNECT) and -677 (ER_BO_CONNECT_FAILED). After cub_server stops or
crashes, a connection in a transaction gets -111 and then -224 on every statement
until the transaction ends, even after the server is back (#565). -671
(ER_CSS_RECV_OR_SEND) is not included: the broker does not reset the CAS for it.
CUBRID's src/broker/cas_error.c sets the CAS's reset_flag for these four codes only, on release/11.4 (3ea1d8d) and release/10.2 (ed716d0); the other reset paths (a failed rollback when the session ends, the HA fail-back status after a commit or abort, the reconnect time) do not depend on -671 (#873).
See Troubleshooting.
- Numeric error code matching, on pycubrid's errno only, for CAS codes as pycubrid
actually receives them: CUBRID's CAS legacy-renumbers cas_error.h codes (adds 9000)
for a driver, like pycubrid, that never advertises understanding the renewed
error-code protocol. -1002 (legacy CAS_ER_NO_MORE_MEMORY, i.e. -10002 + 9000) is
matched this way (#578).
- Origin of the overlapping codes (#916): legacy CAS codes arrive as -1000 to -1200,
the same numbers as some server errors (CUBRID 11.4.6 ER_JSON_INVALID_PATH is -1200).
pycubrid 1.13 reports who sent an error in error_source ("cas" or "server").
With "server", the code is the server's: a server -1002 is not
CAS_ER_NO_MORE_MEMORY, and the message patterns apply only to the server
connect-failure codes (-190, -191, -368), because server text can echo application
data. With "cas", with None, or with pycubrid before 1.13 (which has no
error_source), these codes are classified as before.
2. CCI Library Dependency¶
The driver requires the CCI library to be compiled from source. In CI, this is handled by:
git clone --branch v11.3.0.51 --depth 1 --recurse-submodules https://github.com/CUBRID/cubrid-python.git
cd cubrid-python/cci-src && mkdir build_x86_64_release && cd build_x86_64_release
cmake ../ && make -j$(nproc)
3. cursor.lastrowid Not Available¶
The standard DB-API cursor.lastrowid attribute is not implemented. The dialect
uses connection.get_last_insert_id() instead, with a SELECT LAST_INSERT_ID()
SQL fallback.
The fallback opens a regular cursor on the active DBAPI connection and closes it
after fetching the ID, including when execution, fetching, or integer conversion
raises. It does not require server-side cursor support. The pycubrid execution
context uses the same fallback when cursor.lastrowid is unavailable. A native
driver result of None is returned directly without running the fallback query.
4. CUBRID 12.x Compatibility¶
CUBRID 12 has not yet been released. When available, the driver and dialect will be tested and the CI matrix updated. Potential concerns: - CCI API changes may require driver recompilation - New SQL features may need compiler updates - New data types may need type mapping additions
5. NUMERIC / DECIMAL fractional precision is lost¶
The CUBRIDdb C-extension driver truncates the fractional part of
NUMERIC / DECIMAL values below the DB-API layer: a column storing
15.7563 comes back as 15. Because the digits are discarded inside the
driver before SQLAlchemy receives the value, no dialect result processor can
recover them. This is an upstream driver limitation, not a dialect bug.
Use the pure-Python cubrid+pycubrid:// driver for correct Decimal
round-trips — it returns Decimal values natively with full precision
(verified on CUBRID 11.4). This is one of the reasons pycubrid is the
recommended driver for new projects.
6. BLOB / CLOB reads return a LOB locator¶
Verified live on CUBRID 10.2 and 11.4, Core and ORM, sync and async, with
pycubrid 1.8.0 (the supported floor) and pycubrid main (#485;
test/test_lob_value_contract.py, re-run whenever this is re-verified). With
every released driver, binding bytes / str into a BLOB / CLOB column
stores the full value and NULL round-trips as None. Selecting a non-NULL
BLOB / CLOB column, however, returns the driver's LOB locator instead of
bytes / str:
| Driver URL | Non-NULL BLOB / CLOB read returns |
|---|---|
cubrid:// (CUBRIDdb 11.3) |
server file-locator str ('file:...') |
cubrid+pycubrid:// (pycubrid 1.8.0 and main) |
LOB-handle dict (lob_type, lob_length, file_locator, ...) |
cubrid+aiopycubrid:// (pycubrid 1.8.0 and main) |
the file_locator string (binding LargeBinary / BLOB values, including None, works since #500) |
For LargeBinary / BLOB, SQLAlchemy's result processor then raises TypeError.
To read content, convert on the server (CLOB_TO_CHAR(col), BLOB_TO_BIT(col)),
or store large text in sqlalchemy.Text (CUBRID STRING), which round-trips as
str. pycubrid's LOB handles (cubrid-lab/pycubrid#441/#442, completed) are
explicit calls on pycubrid.compat.native (connection.lob(),
cursor.fetch_lob(), cursor.bind_lob(), sync only). The dialect does not use
that module, and ordinary fetch on pycubrid main (checked at 9fbbc05) still
returns the locator, so test_lob_value_contract.py xfails the same cases on
both the released driver and pycubrid main, strictly — a pycubrid release that
changes ordinary fetch turns the xfail into a failing XPASS instead of silently
staying green. The dialect does not convert on the server for you: wrapping
every BLOB / CLOB select in BLOB_TO_BIT / CLOB_TO_CHAR would change the
emitted SQL, would not apply to text() statements and would load each whole
value into memory. See also Types.
7. executemany reuses the previous row's value for None (dialect guard)¶
CUBRIDdb 11.3.0.51 (the cubrid:// driver) has two executemany() bugs:
- Wrong data. Its
_bind_paramsskipsNone, andexecutemanyprepares the statement once. ANoneparameter therefore keeps the previous row's bound value, so[(1, 'a'), (2, None)]stores(2, 'a'). - Wrong rowcount.
cursor.rowcountreports only the last row's count.
Through SQLAlchemy, these bugs made text() and Core UPDATE/DELETE
executemany store wrong data. Batched ORM UPDATEs raised StaleDataError
(expected to update 3 row(s); 1 were matched). The driver cannot be worked
around from the outside because bind_param(i, None) raises SystemError.
Dialect guard (#502). CubridDialect.do_executemany runs each parameter set
with cursor.execute() and sets cursor.rowcount to the total. Each
execute() prepares the statement again, so an unbound None becomes NULL. The
guard always applies, not only when a row contains None, because the
last-row rowcount is wrong for every statement. With the guard,
supports_sane_multi_rowcount is accurate on both drivers. The cost is one
statement prepare per row on plain executemany. A multi-row Core insert()
that uses insertmanyvalues goes through do_execute and does not use the
guard. An INSERT into a table with a column whose type defines
bind_expression() falls back to executemany (#421), so on cubrid:// it
does use the per-row guard. Code that calls CUBRIDdb's cursor.executemany() directly, without
SQLAlchemy, is still affected.
cubrid+pycubrid:// and cubrid+aiopycubrid:// bind None correctly and sum
the rowcount, so they keep the driver's prepare-once executemany.
The guard will be removed once a fixed CUBRIDdb release is the minimum
supported version. An upstream report to CUBRID/cubrid-python is pending, and
#502 tracks it.
8. Unfinished results after commit() / rollback()¶
Verified live on CUBRID 10.2 and 11.4 (#481) with a 500-row result that needs
several FETCH round trips (pycubrid fetches 100 rows per batch; the broker's
first response held 16 of these 1000-byte rows). Reading the rest of a sync
Result after Connection.commit() / rollback() behaves differently per
driver:
| Driver | After commit() |
After rollback() |
|---|---|---|
cubrid:// (CUBRIDdb 11.3) |
all rows | InterfaceError (CCI -20040) |
cubrid+pycubrid:// (pycubrid 1.7.1) |
silently returns only the rows already buffered on a connection that completed an earlier query (the normal state of a pooled connection); OperationalError otherwise |
same as commit |
cubrid+aiopycubrid:// |
all rows: AsyncConnection.execute() buffers the whole result before it returns |
all rows |
raw pycubrid.aio cursor (pycubrid 1.7.1) |
silently returns only the buffered rows | same as commit |
Fixed in pycubrid 1.8.0: cubrid-lab/pycubrid#395 makes pycubrid raise
InterfaceError instead of returning a partial result, and the contract tests
require that behavior. With pycubrid 1.7.1 or earlier, fully consume a result
before ending its transaction. AsyncConnection.stream() is not
available because the dialect does not support server-side cursors.
9. NOT NULL / foreign-key violations on pycubrid 1.7.1 and earlier¶
Verified live on CUBRID 10.2 and 11.4 (#480). SQLAlchemy wraps the DB-API exception class it receives, so the SQLAlchemy exception class depends on the driver:
| Violation (native code) | cubrid:// (CUBRIDdb 11.3) |
cubrid+pycubrid:// / cubrid+aiopycubrid:// (pycubrid 1.7.1) |
|---|---|---|
| NOT NULL (-631) | IntegrityError |
DatabaseError |
| Foreign key (-922) | IntegrityError |
DatabaseError |
| Unique / primary key (-670) | IntegrityError |
IntegrityError |
Fixed in pycubrid 1.8.0: cubrid-lab/pycubrid#390 raises IntegrityError
for NOT NULL and foreign-key violations, matching CUBRIDdb, and the contract
tests require that behavior. With pycubrid 1.7.1 or earlier, catch
sqlalchemy.exc.DatabaseError (the base of IntegrityError) for NOT NULL and
foreign-key failures through pycubrid. The dialect deliberately does not
reclassify exceptions by message. On every driver the connection or Session
remains usable after rollback().
pycubrid 1.8.x (supported floor), fixed in pycubrid 1.9.0 (verified on CUBRID 10.2 and 11.4, #479):
| Case | pycubrid 1.8.x | pycubrid >= 1.9.0 |
|---|---|---|
Parent DELETE/UPDATE restricted by a foreign key (-924), TRUNCATE of a referenced table (-1284) |
DatabaseError |
IntegrityError (cubrid-lab/pycubrid#493) |
A value pycubrid cannot represent in a complete reply, such as a zero DATE/DATETIME ('0000-00-00') |
OperationalError ("malformed response from broker"), reported as a disconnect: the connection is invalidated |
DataError (cubrid-lab/pycubrid#492, #512, #543): the same connection runs the next statement |
The contract tests require the 1.9.0 behavior and are strict expected failures on pycubrid 1.8.x.
10. pycubrid replaces the session after a CAS restart (dialect re-applies the isolation level)¶
Fixed in pycubrid 1.8.0 (cubrid-lab/pycubrid#468, #472), the minimum
supported version. Before 1.8.0, pycubrid opened a new broker session after
every driver commit() / rollback() (the broker reports the CAS as out of
transaction then), so the level dropped to the server default (READ COMMITTED)
and session variables were lost (#505). pycubrid 1.8.0 keeps the CAS session
across commit() / rollback() and in autocommit mode: before the next request
it probes an out-of-transaction CAS with CHECK_CAS and reconnects only when
the CAS is gone. CUBRIDdb has always kept the same session.
Residual case: a replaced CAS. When the CAS does go away (for example a CAS
restart after a transaction, such as the APPL_SERVER_MAX_SIZE memory restart,
or a broker reset), pycubrid 1.8.0 opens a new session. That session starts at
the server default isolation level, pycubrid restores only autocommit, and
session state set with raw SQL (SET @var, a SET TRANSACTION statement you
run yourself) is lost. Set the level server-side (isolation_level in
cubrid.conf) if it must survive this without the dialect.
Dialect re-apply. cubrid+pycubrid:// and cubrid+aiopycubrid://
remember the isolation level they set on each connection and re-apply it
(SET TRANSACTION ISOLATION LEVEL + COMMIT) after every commit and rollback,
only on connections with a configured level. An engine-level
isolation_level survives commits, rollbacks and pool checkins. A
connection-level execution_options(isolation_level=...) survives commits and
rollbacks while that Connection stays open; on checkin SQLAlchemy restores
the engine level (or the server default when none is configured), and on an
engine without a configured level checkin stops the re-apply for that
connection. The re-apply is the first request after the end of the
transaction, so when the CAS is replaced at that point, it runs on the new
session and the level is kept (verified with pycubrid 1.8.0 on CUBRID 11.4 by
killing the CAS right after commit(): the level stays SERIALIZABLE, and drops
to READ COMMITTED without the re-apply). If the re-apply itself fails, the
commit or rollback still reports success (or the rollback's original exception
still propagates): the failure is logged as a warning and retried at the start
of the next transaction, where a second failure raises before any statement
runs. The connection stays marked until a re-apply succeeds (or a new level is
set on it), so every later transaction start retries and raises again rather
than running at an unconfirmed level (#937); the prior commit is never replayed.
Idle connections with a configured level. The re-apply's SQL COMMIT
leaves the CAS marked as in transaction, so the broker keeps that CAS bound to
the pooled connection while it sits idle (CLIENT_WAIT in
cubrid broker status; CLOSE_WAIT without a configured level). Size the
broker's MAX_NUM_APPL_SERVER for the pool. It also means pycubrid does not
reconnect such a connection by itself if its CAS is killed while idle: the next
statement raises OperationalError, which the dialect reports as a disconnect,
so SQLAlchemy discards the connection instead of silently running at the server
default level. With create_engine(..., pool_pre_ping=True) the pool replaces
it at checkout, and the new connection gets the engine level.
AUTOCOMMIT on pycubrid. pycubrid 1.8.0 keeps the session in autocommit
mode, so statements keep the server level that was in effect and session
variables survive between statements, as on CUBRIDdb. The dialect stops
re-applying a level once a connection is switched to AUTOCOMMIT.
11. Collection parameters (SET, MULTISET, SEQUENCE)¶
The drivers bind a Python list, tuple or set for a collection column
differently (#484; verified on CUBRID 10.2 and 11.4):
| Driver | Binding a list / tuple / set |
Reading a collection |
|---|---|---|
| pycubrid >= 1.9.0 (typed collection parameters, cubrid-lab/pycubrid#567) | The dialect wraps the value in pycubrid.types.Set, Multiset or Sequence to match the column type. SET, MULTISET and SEQUENCE semantics are kept. A SEQUENCE takes only a list or tuple: a set/frozenset raises TypeError ("SEQUENCE is ordered; pass a list or tuple"). |
frozenset (SET) or list (MULTISET, SEQUENCE) with ?decode_collections=true; raw bytes without it |
| pycubrid 1.8.x (supported floor) | ProgrammingError: pycubrid rejects collection parameters. A set/frozenset for a SEQUENCE raises the same TypeError as above, from the dialect. |
Same as above |
| CUBRIDdb 11.3.0.51 | CUBRIDdb binds the value itself, always as a SET host variable. A MULTISET loses its duplicates and a SEQUENCE its order ([3, 1, 2, 1] is stored as {1, 2, 3}). None elements are not supported: [None] fails inside the driver with an UnboundLocalError, and [1, None] with -494 Cannot coerce host var to type sequence. |
set (SET) or list (MULTISET, SEQUENCE) of str elements, whatever the element type |
The dialect does not change what CUBRIDdb does. On CUBRIDdb, keep collections
that need duplicates or order out of bound parameters: write the collection
literal in SQL (MULTISET{1, 1}, SEQUENCE{3, 1, 2}), or use
cubrid+pycubrid://. See Type Mapping, Collection values.
12. Ordinary scalar execution never uses pycubrid's compat.native prepared core¶
pycubrid 1.8.0 (the supported floor) added an additive, opt-in sync prepared/typed
scalar-binding cursor under pycubrid.compat.native (INT32, UTF-8 CHAR and SQL
NULL only; cubrid-lab/pycubrid#439). Neither cubrid+pycubrid:// nor
cubrid+aiopycubrid:// import or call it: SQLAlchemy keeps using the ordinary
PEP 249 execute() / executemany() path (FC41) for every scalar bind — integer,
UTF-8/CJK string, NULL, repeated execution, executemany, Core and ORM — on
every driver tested, with no behavior difference and no dialect change (#483;
test/test_scalar_execution_contract.py, which also asserts the dialect source
never references pycubrid.compat and that pycubrid.compat.native.connect() is
never called by ordinary use). A SQLAlchemy/DB-API integration that uses typed
prepared execution would be a separate, explicitly designed and released feature,
not an automatic consequence of compat.native existing in the installed driver.
13. CUBRIDdb binds values on the server¶
pycubrid writes each bound value into the SQL text before sending it; CUBRIDdb
sends the statement with ? markers and binds the values on the server. Two
results differ because of it (#1003; verified on CUBRID 10.2 and 11.4 with
CUBRIDdb 11.3.0.51 and pycubrid 1.13.1):
- A
CASEwhoseTHEN/ELSEvalues are all bound parameters returns a string on CUBRIDdb.select(case({True: 10}, value=..., else_=20))and the searched formcase((cond, 10), else_=20)return'10'and'20'; on pycubrid they return10and20. The server types such aCASEas a string. Wrap it incast(..., Integer)(CAST(CASE ... END AS INTEGER)returns10on both drivers), or useliteral_column("10")for the values. - A
GROUP_CONCATargument andaggregate_order_by()item that contain bound parameters fail on CUBRIDdb (-494). CUBRID requires theORDER BYitem to be the same expression as the argument, and with CUBRIDdbfunc.group_concat(and_(t.c.a == 1, t.c.b == 2)).aggregate_order_by(...)sends the two copies with their own markers, which the server does not treat as the same expression. pycubrid sends the same literal SQL twice and succeeds. On CUBRIDdb, order by the argument position,aggregate_order_by(literal_column("1")), or write the constants withliteral_column(). An argument without bound parameters, such as a column, works on both drivers.
The dialect does not change either result.
Installation Notes¶
For the pure Python pycubrid dialect variants, install sqlalchemy-cubrid[pycubrid] with pycubrid>=1.8.0,<2.0. pycubrid 1.8.0 is the minimum because it keeps the CAS session across commit() / rollback() and ships the fixes the contract tests require (Known Issues 8 to 10); it also provides the native sync and async ping(False) used by pool_pre_ping.
The [pycubrid] extra supports both sync and async connections. It includes
SQLAlchemy[asyncio], which supplies greenlet on SQLAlchemy 2.0 and 2.1. The
contributor test dependency group (part of dev) also includes this bridge for async test imports. Bare installation
keeps its existing SQLAlchemy dependency. The pycubrid driver remains pure Python,
but greenlet may require build tools when no compatible wheel is available.
PyPI CUBRID-Python 9.3.x is not supported¶
The newest CUBRID-Python release on PyPI is 9.3.0.2 (sdist only, uploaded in 2015).
There is no 11.x release on PyPI. The [cubrid] and [cubriddb] extras depend on
CUBRID-Python without a version bound, so they install 9.3.x. That driver is not
tested with this dialect. On CUBRID 11.4 it returns BIGINT values (including
COUNT(*)) as str (#583) and fails parts of the integration suite: autocommit,
large-object round-trips, fetch shapes, ping and recursive CTEs (#585).
- The
[cubrid]and[cubriddb]extras are deprecated. They are kept so that existing installs keep resolving, but they cannot install a supported driver. - At the first connection, a
cubrid://orcubrid+cubriddb://engine reads the loaded driver's version (_cubrid.__version__) and emits asqlalchemy.exc.SAWarningif it is older than 11.3. It warns rather than refusing to connect, so existing deployments keep working. To fail fast instead, turn the warning into an error:warnings.filterwarnings("error", message="CUBRIDdb .* is older", category=SAWarning). - Supported paths: use the recommended pure-Python driver
(
pip install "sqlalchemy-cubrid[pycubrid]",cubrid+pycubrid://), or build CUBRIDdb from source as shown below.
Building CUBRIDdb from Source¶
This is the recipe CI uses:
# Clone the driver with its CCI submodule
git clone --branch v11.3.0.51 --depth 1 --recurse-submodules \
https://github.com/CUBRID/cubrid-python.git
cd cubrid-python
# Build the CCI library
mkdir -p cci-src/build_x86_64_release && cd cci-src/build_x86_64_release
cmake ../ && make -j$(nproc)
cd ../..
# Skip setup.py's own CCI rebuild, then install
printf '#!/bin/bash\nexit 0\n' > build_cci.sh
pip install .
A build from source installs the cubrid_python distribution. Its fourth version
component is a git commit count, not the release tag, so a shallow clone of v11.3.0.51
reports 11.3.0.0001.
Verify Installation¶
import CUBRIDdb
print(CUBRIDdb._cubrid.__version__) # b'11.3.0.0001' for a v11.3.0.51 build; b'9.3.0.0001' is the PyPI release
See also: Connection Guide · Development · Feature Support