Skip to content

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

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_params skips None, and executemany prepares the statement once. A None parameter therefore keeps the previous row's bound value, so [(1, 'a'), (2, None)] stores (2, 'a').
  • Wrong rowcount. cursor.rowcount reports 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 CASE whose THEN/ELSE values are all bound parameters returns a string on CUBRIDdb. select(case({True: 10}, value=..., else_=20)) and the searched form case((cond, 10), else_=20) return '10' and '20'; on pycubrid they return 10 and 20. The server types such a CASE as a string. Wrap it in cast(..., Integer) (CAST(CASE ... END AS INTEGER) returns 10 on both drivers), or use literal_column("10") for the values.
  • A GROUP_CONCAT argument and aggregate_order_by() item that contain bound parameters fail on CUBRIDdb (-494). CUBRID requires the ORDER BY item to be the same expression as the argument, and with CUBRIDdb func.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 with literal_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:// or cubrid+cubriddb:// engine reads the loaded driver's version (_cubrid.__version__) and emits a sqlalchemy.exc.SAWarning if 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