Troubleshooting Guide¶
Comprehensive solutions for common sqlalchemy-cubrid issues — connection setup, SQL compilation, type mapping, schema reflection, Alembic migrations, ORM patterns, and performance tuning.
Table of Contents¶
- Installation Issues
- ImportError: Could not import CUBRIDdb
- ModuleNotFoundError: No module named 'pycubrid'
- C Extension Build Failure
- Missing greenlet for async engines
- Connection Issues
- Connection Refused on Port 33000
- Database Server Host Not Found After Recreating the Container
- Authentication Failed
- Stale Connections / Disconnections
- Errors After a cub_server Restart or Crash
- Interrupted Query (-4)
- Connection Pool Exhaustion
- Wrong URL Format
- SQL Compilation Issues
- Unsupported RETURNING Clause
- Boolean Column Behavior
- LIMIT / OFFSET Syntax
- CAST Type Limitations
- Reserved Word Conflicts
- JSON Type Usage
- Type Mapping Issues
- Boolean Mapped to SMALLINT
- Text Mapped to STRING
- Missing ARRAY Type
- LOB Column Behavior
- Collection Types (SET, MULTISET, SEQUENCE)
- Decimal Precision
- Schema Reflection Issues
- Table Not Found During Reflection
- Case Sensitivity in Table Names
- View Reflection
- Missing Schema Support
- ORM Issues
- autoincrement and lastrowid
- No Sequences — Use AUTO_INCREMENT
- Relationship Cascade Behavior
- Bulk Insert Performance
- DML Extension Issues
- ON DUPLICATE KEY UPDATE Not Working
- MERGE Statement Errors
- REPLACE INTO Behavior
- Alembic Migration Issues
- No Implementation Found for Dialect 'cubrid'
- ALTER COLUMN TYPE Rejected (lossy conversion)
- RENAME COLUMN
- Partial Migration After a Failure
- Autogenerate Not Detecting Changes
- Isolation Level Issues
- Setting Isolation Levels
- DDL Runs in the Current Transaction
- Transaction Issues
- Data Not Persisted
- Autocommit Conflicts
- Savepoint Limitations
- Performance Issues
- Slow Schema Reflection
- Connection Overhead
- Statement Caching
- Docker Issues
- Container Starts but Cannot Connect
- Database Not Created
- Version-Specific Behavior
- Debugging Techniques
Installation Issues¶
ImportError: Could not import CUBRIDdb¶
Symptom:
ImportError: Could not import CUBRIDdb. The cubrid:// and cubrid+cubriddb:// URLs use the CUBRIDdb C-extension driver. Install it with pip install "sqlalchemy-cubrid[cubriddb]" (the CUBRID-Python distribution on PyPI), or build CUBRIDdb from the official CUBRID/cubrid-python source. Alternatively, use the pure-Python pycubrid driver with pip install "sqlalchemy-cubrid[pycubrid]" and a cubrid+pycubrid:// URL. Check driver and server version compatibility separately.
Cause: The CUBRID C-extension Python driver is not installed.
Fix — Option A: Install the CUBRIDdb C-extension driver: install the PyPI CUBRID-Python package using the installation extra, or build the driver from official CUBRID/cubrid-python source:
Source builds require appropriate CUBRID CCI headers and a C compiler; see Building CUBRIDdb from Source. Check driver, Python, and CUBRID server version compatibility separately. The availability of an installation method does not prove every combination was tested.
Fix — Option B: Use the pure Python driver instead (recommended):
Then change your connection URL:
# Before (C-extension)
engine = create_engine("cubrid://dba@localhost:33000/testdb")
# After (pure Python driver — no CUBRID native libraries)
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")
ModuleNotFoundError: No module named 'pycubrid'¶
Symptom:
Fix:
C Extension Build Failure¶
Symptom: building CUBRIDdb (cubrid-python) fails with compilation errors.
Common causes:
- Missing C compiler (gcc / cl.exe)
- Missing CUBRID CCI headers
- Incompatible platform
Fix: Use the pure-Python pycubrid driver instead; it requires no CUBRID native libraries. The [pycubrid] extra also installs greenlet, which may need build tools if no compatible wheel is available:
Missing greenlet for async engines¶
Symptom: creating an async SQLAlchemy engine fails before connecting:
ImportError: The SQLAlchemy asyncio module requires that the Python 'greenlet' library is installed.
Cause: In SQLAlchemy 2.1, installing the base SQLAlchemy package alone
does not necessarily install greenlet. An environment with pycubrid but
without SQLAlchemy's asyncio dependencies cannot use create_async_engine().
Fix: install the supported dialect extra, which includes the async dependencies, or install SQLAlchemy's asyncio extra explicitly:
python -m pip install "sqlalchemy-cubrid[pycubrid]"
# If the dialect and driver are already installed:
python -m pip install "sqlalchemy[asyncio]"
Some Python/platform combinations do not have a compatible greenlet wheel
and require a compiler/build tools. Check that the install command succeeded
in the same virtual environment used by the async application. Use
cubrid+aiopycubrid:// for the async dialect, not cubrid://.
Connection Issues¶
Connection Refused on Port 33000¶
Symptom:
OperationalError: (CUBRIDdb.DatabaseError) Connection refused
OperationalError: (pycubrid.exceptions.OperationalError) failed to connect to CUBRID broker
Fixes:
- Check the broker inside the container (not on the host, unless CUBRID is installed there):
docker compose ps
docker compose exec -T cubrid cubrid broker status
docker compose exec -T cubrid csql -u dba testdb -c "SELECT 1"
- Wait for database login readiness, not an arbitrary delay:
docker compose up -d
docker compose ps
docker compose exec -T cubrid csql -u dba testdb -c "SELECT 1"
For the Lesson 1 docker run lab instead of Compose:
docker ps --filter name=sqlalchemy-cubrid-quickstart
docker logs sqlalchemy-cubrid-quickstart
docker exec sqlalchemy-cubrid-quickstart cubrid broker status
docker exec sqlalchemy-cubrid-quickstart csql -u dba testdb -c "SELECT 1"
If SELECT 1 is not ready yet, use the up-to-three-minute readiness loop in
Lesson 1 rather than repeatedly restarting the container.
A running container or an available broker alone does not prove database login readiness.
-
Wrong port: Check
cubrid_broker.conffor the actual broker port. -
Firewall: Ensure port 33000 is not blocked.
Database Server Host Not Found After Recreating the Container¶
Symptom: after docker compose down and up with this repository's
docker-compose.yml, the broker answers but every connection fails, naming a host that
looks like an old container ID:
OperationalError: ... Failed to connect to database server, 'testdb', on the following host(s): 43134de458f9
Inside the container, csql -u dba testdb -c "SELECT 1" fails the same way, although
cubrid server status lists testdb.
Cause: the image registers the database in databases.txt, for CUBRID_DB_HOST
or, when that is unset, for the hostname of the container that created it, which is that
container's ID. The file is on the cubrid-data volume, or on cubrid-databases for 11.4. down keeps
the volume, and a recreated container has a new ID, so nothing resolves the recorded
host. docker-compose.yml sets CUBRID_DB_HOST: localhost, so a volume it creates keeps
working; a volume created by an older copy still records the old ID. make integration
is not affected: it uses its own Compose project and removes it with down -v.
Fix, keeping the data: with the container running, rewrite only the host field of
testdb to localhost:
The previous file is saved next to it as databases.txt.before-fix-db-host.
Fix, deleting the data: make docker-clean runs docker compose down -v, which
removes the volume and every table in it; the next make docker-up creates a fresh
database. Use it only for a volume whose data you do not need.
Authentication Failed¶
Symptom: Depending on driver and SQLAlchemy's exception wrapper, the underlying CUBRID error can contain either of these messages:
Fix: Check the username, database and password in the URL. The local
disposable Docker lab creates a dba user without a password; do not
assume that a production server allows an empty DBA password:
# Disposable local lab only — empty password
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")
# Server with an explicitly configured password
engine = create_engine("cubrid+pycubrid://dba:mypassword@localhost:33000/testdb")
Stale Connections / Disconnections¶
Symptom:
Cause: The CUBRID broker has a SESSION_TIMEOUT (default ~300 seconds). Idle pooled connections expire server-side.
Fix: Configure pool settings:
engine = create_engine(
"cubrid://dba@localhost:33000/testdb",
pool_pre_ping=True, # Native driver ping before checkout
pool_recycle=240, # Recycle before SESSION_TIMEOUT (300s)
)
For cubrid+pycubrid:// and cubrid+aiopycubrid://, pool_pre_ping=True now uses pycubrid's native CHECK_CAS ping instead of issuing SELECT 1.
See Connection Guide — Pool Tuning for detailed recommendations.
Errors After a cub_server Restart or Crash¶
Symptom: A statement in an open transaction fails after cub_server was stopped, restarted or crashed:
DatabaseError: (-111) Your transaction has been aborted by the system due to server failure or mode change.
DatabaseError: (-224) A database has not been restarted.
Cause: The broker's CAS lost its session with cub_server. It reconnects only after the client ends the transaction, so until then the same connection fails every statement with -224, even after cub_server is back. pool_pre_ping cannot catch this: the connection is already checked out, and the CAS still answers the ping.
Behavior: The dialect treats the codes for which the broker resets the CAS as disconnects on both drivers: -111 (ER_TM_SERVER_DOWN_UNILATERALLY_ABORTED), -199 (ER_NET_SERVER_CRASHED), -224 (ER_OBJ_NO_CONNECT) and -677 (ER_BO_CONNECT_FAILED). SQLAlchemy invalidates the connection (exc.connection_invalidated is True) and the pool opens a new one. A pycubrid connection that fails to reconnect while cub_server is down (CAS did not answer CHECK_CAS out of transaction and reconnecting failed) is also a disconnect. Releases up to 1.8.0 did not classify these errors, so the broken connection stayed in the pool.
Fix: Nothing to configure. Roll back (leaving a with engine.connect() or Session block does this) and retry the transaction once cub_server accepts connections again.
Interrupted Query (-4)¶
Symptom: A running statement fails. The exact text is driver-specific:
# CUBRIDdb
DatabaseError: (-4, 'ERROR: DBMS, -4, Has been interrupted.[CAS INFO-127.0.0.1:33000,1,44].')
# pycubrid 1.13.0 and later
OperationalError: Has been interrupted. (errno=-4, description='Interrupted', sqlstate='HY008')
# pycubrid before 1.13.0
OperationalError: Has been interrupted. (errno=-4, description='Communication error', sqlstate='08S01')
Cause: Another session ran KILL QUERY <tran_index> on it. -4 is the server's ER_INTERRUPTED; the connection is still usable. pycubrid 1.13.0 and later describe it as Interrupted with SQLSTATE HY008. Earlier releases reported it as a communication error (Communication error, 08S01), which was a pycubrid label, not the meaning of the server code.
Behavior: The error is not a disconnect: SQLAlchemy keeps the connection (exc.connection_invalidated is False). Releases up to 1.8.0 treated -4 from CUBRIDdb as a disconnect and replaced the connection (#572).
Fix: Roll back and retry the statement if it should run to completion.
Connection Pool Exhaustion¶
Symptom:
Causes:
- Connections not being returned to the pool (missing close() or context manager)
- Pool too small for application concurrency
Fix:
engine = create_engine(
"cubrid://dba@localhost:33000/testdb",
pool_size=10,
max_overflow=20,
pool_timeout=30,
)
# Always use context managers to return connections
with engine.connect() as conn:
result = conn.execute(text("SELECT 1"))
Wrong URL Format¶
Symptom:
ArgumentError: Could not parse rfc1738 URL
NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:pycubrid
Correct URL formats:
# C-extension driver (CUBRIDdb)
engine = create_engine("cubrid://dba@localhost:33000/testdb")
engine = create_engine("cubrid+cubrid://dba@localhost:33000/testdb")
# Pure Python driver (pycubrid)
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")
Common mistakes:
# WRONG — 'pycubrid://' is not a valid scheme
engine = create_engine("pycubrid://dba@localhost:33000/testdb")
# WRONG — port should be in the URL, not as a query parameter
engine = create_engine("cubrid://dba@localhost/testdb?port=33000")
SQL Compilation Issues¶
Unsupported RETURNING Clause¶
Symptom:
Cause: CUBRID does not support INSERT ... RETURNING or UPDATE ... RETURNING.
Fix: The dialect handles this automatically via get_last_insert_id() on the driver connection. You can also use SELECT LAST_INSERT_ID():
from sqlalchemy import insert, select, text
# Insert and get the generated ID
with engine.begin() as conn:
result = conn.execute(
insert(users).values(name="Alice", email="alice@example.com")
)
new_id = result.inserted_primary_key[0]
print(f"New user ID: {new_id}")
# Or use LAST_INSERT_ID()
with engine.begin() as conn:
conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
last_id = conn.execute(text("SELECT LAST_INSERT_ID()")).scalar()
ORM pattern:
with Session(engine) as session:
user = User(name="Alice", email="alice@example.com")
session.add(user)
session.flush() # Sends INSERT, populates user.id
print(user.id) # Available after flush
session.commit()
Boolean Column Behavior¶
Symptom: Boolean columns store/return 0 and 1 instead of True/False.
Cause: CUBRID has no native BOOLEAN type. The dialect maps Boolean to SMALLINT.
Fix: This is expected. SQLAlchemy automatically converts between Python bool and SMALLINT(0/1):
from sqlalchemy import Boolean
from sqlalchemy.orm import mapped_column, Mapped
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Stored as SMALLINT: True→1, False→0
# Retrieved as bool automatically
In raw SQL:
# Insert
conn.execute(text("INSERT INTO users (is_active) VALUES (:val)"), {"val": 1})
# Query — compare against 0/1
conn.execute(text("SELECT * FROM users WHERE is_active = 1"))
LIMIT / OFFSET Syntax¶
Symptom: Unexpected query behavior with pagination.
The dialect emits MySQL-style LIMIT [offset,] row_count. CUBRID accepts the standard
LIMIT row_count OFFSET offset form as well, but the dialect never generates it — the OFFSET
keyword does not appear in its output at all:
select(users).limit(3) # → SELECT ... FROM users LIMIT 3
select(users).limit(10).offset(20) # → SELECT ... FROM users LIMIT 20, 10
select(users).offset(5) # → SELECT ... FROM users LIMIT 5, 4611686018427387904
The comma form appears only when there is an offset, and the offset comes first. LIMIT 20, 10
skips 20 rows and returns 10; it does not limit to 20 starting at 10.
Offset without limit. CUBRID has no bare OFFSET (SELECT ... OFFSET 5 is a syntax error), so
.offset(n) with no .limit() still has to name a row count, and the dialect supplies an
effectively unbounded one — the third line above. A very large second operand in a query log is
that sentinel, not a bug. See Feature Support for the exact value and why it
is deliberately not the signed BIGINT maximum.
CAST Type Limitations¶
Symptom:
Cause: CUBRID's CAST() supports a limited set of target types.
Supported CAST targets:
| Target Type | Example |
|---|---|
CHAR(n) |
CAST(col AS CHAR(10)) |
VARCHAR(n) |
CAST(col AS VARCHAR(100)) |
NCHAR(n) |
CAST(col AS NCHAR(10)) |
INTEGER |
CAST(col AS INTEGER) |
BIGINT |
CAST(col AS BIGINT) |
FLOAT |
CAST(col AS FLOAT) |
DOUBLE |
CAST(col AS DOUBLE) |
NUMERIC(p,s) |
CAST(col AS NUMERIC(10,2)) |
DATE |
CAST(col AS DATE) |
TIME |
CAST(col AS TIME) |
DATETIME |
CAST(col AS DATETIME) |
TIMESTAMP |
CAST(col AS TIMESTAMP) |
Unsupported: CAST(... AS BOOLEAN), CAST(... AS BLOB), CAST(... AS SET).
Reserved Word Conflicts¶
Symptom:
Cause: CUBRID has many reserved words that may conflict with column or table names.
Common CUBRID reserved words:
| Reserved Word | Safe Alternative |
|---|---|
value |
val, item_value |
count |
cnt, item_count |
data |
file_data, raw_data |
level |
user_level |
action |
user_action |
status |
item_status |
type |
item_type |
Fix — The dialect auto-quotes identifiers, but if you're using text() for raw SQL, quote manually:
# Auto-quoting works for ORM and Core constructs
class Config(Base):
__tablename__ = "config"
value: Mapped[str] = mapped_column("val", String(100)) # Rename the column
# For raw SQL, use double quotes
conn.execute(text('SELECT "value" FROM config'))
The CubridIdentifierPreparer handles quoting automatically for reserved words. The full reserved word list is maintained in base.py.
JSON Type Usage¶
Status: JSON is supported in sqlalchemy-cubrid since v1.2.0 and requires CUBRID ≥ 10.2.
Basic usage:
from sqlalchemy import Column, MetaData, Table, select
from sqlalchemy_cubrid import JSON
metadata = MetaData()
config = Table(
"config",
metadata,
Column("data", JSON),
)
Path access: col["key"] compiles to JSON_EXTRACT(...) on CUBRID.
If you see this error:
upgrade to sqlalchemy-cubrid>=1.2.0.
Workaround for CUBRID < 10.2: Store JSON as VARCHAR or STRING and serialize/deserialize in Python:
import json
from sqlalchemy import String, TypeDecorator
class JSONType(TypeDecorator):
"""Store JSON as VARCHAR in CUBRID."""
impl = String(4096)
cache_ok = True
def process_bind_param(self, value, dialect):
if value is not None:
return json.dumps(value)
return None
def process_result_value(self, value, dialect):
if value is not None:
return json.loads(value)
return None
class Config(Base):
__tablename__ = "config"
id: Mapped[int] = mapped_column(primary_key=True)
settings: Mapped[dict] = mapped_column(JSONType)
Type Mapping Issues¶
Boolean Mapped to SMALLINT¶
See Boolean Column Behavior above.
Text Mapped to STRING¶
Behavior: SQLAlchemy's Text type maps to CUBRID's STRING type, which is equivalent to VARCHAR(1,073,741,823) — a very large variable-length string.
from sqlalchemy import Text
class Article(Base):
__tablename__ = "articles"
content: Mapped[str] = mapped_column(Text)
# → STRING in DDL (equivalent to VARCHAR(1073741823))
This is correct behavior. CUBRID's STRING type serves the same purpose as TEXT in MySQL/PostgreSQL.
Missing ARRAY Type¶
Cause: CUBRID does not have a standard ARRAY type. Instead, it offers collection types: SET, MULTISET, and SEQUENCE.
from sqlalchemy_cubrid import SET, MULTISET, SEQUENCE
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
tags: Mapped[str] = mapped_column(SET(String(50))) # Unique, unordered
categories: Mapped[str] = mapped_column(MULTISET(String(50))) # Allows duplicates
colors: Mapped[str] = mapped_column(SEQUENCE(String(50))) # Ordered
SQL equivalents:
CREATE TABLE products (
id INTEGER AUTO_INCREMENT PRIMARY KEY,
tags SET(VARCHAR(50)),
categories MULTISET(VARCHAR(50)),
colors SEQUENCE(VARCHAR(50))
);
LOB Column Behavior¶
CLOB (Character Large Object) and BLOB (Binary Large Object) types are supported:
from sqlalchemy import LargeBinary, Text
class Document(Base):
__tablename__ = "documents"
content: Mapped[str] = mapped_column(Text) # Uses STRING (CLOB-like)
binary_data: Mapped[bytes] = mapped_column(LargeBinary) # Uses BLOB
Note: LOB behavior depends on the driver:
- CUBRIDdb (C-extension): LOB columns may return raw bytes or LOB handles
- pycubrid (pure Python): LOB columns return a dictionary with metadata (lob_type, lob_length, file_locator, packed_lob_handle)
For simple use cases, insert strings/bytes directly and they will be stored in the LOB column.
Collection Types (SET, MULTISET, SEQUENCE)¶
Reflection: When reflecting tables with collection columns, the dialect maps them back to the appropriate SQLAlchemy type:
from sqlalchemy import inspect
inspector = inspect(engine)
columns = inspector.get_columns("products")
for col in columns:
print(f"{col['name']}: {col['type']}")
# tags: SET(VARCHAR(50))
# categories: MULTISET(VARCHAR(50))
Inserting collection data: Use CUBRID's collection literal syntax in raw SQL:
Decimal Precision¶
Symptom: Decimal values lose precision.
Fix: Use NUMERIC(precision, scale) for exact decimal arithmetic:
from sqlalchemy_cubrid import NUMERIC
class Product(Base):
__tablename__ = "products"
price: Mapped[Decimal] = mapped_column(NUMERIC(10, 2))
# Stores exactly 10 digits with 2 decimal places
CUBRID supports precision up to 38 digits.
Schema Reflection Issues¶
Table Not Found During Reflection¶
Symptom:
Causes:
- Table doesn't exist — verify in CUBRID directly
- Case sensitivity — CUBRID folds identifiers to lowercase (unlike SQL standard which folds to uppercase):
# CUBRID stores table names in lowercase
inspector = inspect(engine)
tables = inspector.get_table_names()
print(tables) # ['users', 'products'] — all lowercase
- Wrong database — ensure your connection URL points to the correct database
Note: Reflection raises
NoSuchTableErroronly for a missing object: thedb_classlookup finds no such table or view, the server reportsUnknown class "<owner>.<name>", or a query that returns a row for every existing object (SHOW CREATE TABLE,SHOW CREATE VIEW) returns none (#589). A syntax error or any other failure of a reflection query propagates as the original exception (for examplesqlalchemy.exc.ProgrammingError), even though CUBRID uses native error -493 for both (and pycubrid before 1.8.0 reports SQLSTATE42S02,Table not found, for every -493 error) (#454).Note:
get_foreign_keys()andget_unique_constraints()no longer turn a failedSHOW CREATE TABLEinto an empty list (#589). A disconnect, an authorization error or a driver error during reflection now raises, so Alembic autogenerate stops with that error instead of proposingadd_fkfor foreign keys that already exist. Retry on a fresh connection (pool_pre_ping=Truereplaces stale pooled connections).
Case Sensitivity in Table Names¶
CUBRID folds unquoted identifiers to lowercase. This differs from PostgreSQL (lowercase) and Oracle (uppercase).
# These all refer to the same table
conn.execute(text("CREATE TABLE MyTable (id INT)")) # Stored as 'mytable'
conn.execute(text("SELECT * FROM MYTABLE")) # Finds 'mytable'
conn.execute(text("SELECT * FROM mytable")) # Finds 'mytable'
# To preserve case, use double quotes
conn.execute(text('CREATE TABLE "MyTable" (id INT)')) # Stored as 'MyTable'
conn.execute(text('SELECT * FROM "MyTable"')) # Must use quotes
The dialect's CubridIdentifierPreparer quotes case-sensitive names with initial_quote = '"'. Because CUBRID already folds unquoted names to lowercase, which matches SQLAlchemy's convention, the dialect sets requires_name_normalize = False.
View Reflection¶
Supported. Use inspector.get_view_names():
inspector = inspect(engine)
views = inspector.get_view_names()
print(views) # ['my_view', 'active_users_view']
Missing Schema Support¶
CUBRID uses a single-schema model. Unlike PostgreSQL, there is no concept of multiple schemas within a database.
# WRONG — CUBRID doesn't support schema parameter
inspector.get_table_names(schema="public")
# CORRECT — omit schema
inspector.get_table_names()
If your code needs to work across databases (PostgreSQL + CUBRID), handle this with a conditional:
schema = None if dialect_name == "cubrid" else "public"
tables = inspector.get_table_names(schema=schema)
ORM Issues¶
autoincrement and lastrowid¶
CUBRID uses AUTO_INCREMENT (not sequences) for auto-generated primary keys:
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(100))
After insert, the ID is available via flush():
with Session(engine) as session:
user = User(name="Alice")
session.add(user)
session.flush()
print(user.id) # Auto-generated ID
session.commit()
The dialect implements get_lastrowid() in CubridExecutionContext which calls raw_conn.get_last_insert_id() (falling back to SELECT LAST_INSERT_ID() via SQL if the driver method is unavailable).
No Sequences — Use AUTO_INCREMENT¶
Symptom:
Cause: CUBRID does not support SQL sequences. Use AUTO_INCREMENT columns instead:
# WRONG — sequences not supported
from sqlalchemy import Sequence
id = Column(Integer, Sequence("user_id_seq"), primary_key=True)
# CORRECT — use autoincrement
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
Relationship Cascade Behavior¶
Foreign key cascades work normally in CUBRID:
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
posts: Mapped[list["Post"]] = relationship(back_populates="author", cascade="all, delete-orphan")
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
author: Mapped["User"] = relationship(back_populates="posts")
Bulk Insert Performance¶
For inserting many rows, use insert().values() with a list of dicts:
from sqlalchemy import insert
with engine.begin() as conn:
conn.execute(
insert(users),
[
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"},
{"name": "Charlie", "email": "charlie@example.com"},
],
)
For very large datasets, batch inserts with executemany() or use executemany_batch() (pycubrid-specific).
DML Extension Issues¶
ON DUPLICATE KEY UPDATE Not Working¶
Symptom: on_duplicate_key_update() raises AttributeError.
Cause: You're using SQLAlchemy's built-in insert() instead of the dialect's custom insert():
# WRONG — standard SQLAlchemy insert has no on_duplicate_key_update
from sqlalchemy import insert
stmt = insert(users).values(name="Alice")
stmt = stmt.on_duplicate_key_update(name="Alice Updated") # AttributeError!
# CORRECT — use the dialect's insert
from sqlalchemy_cubrid import insert
stmt = insert(users).values(name="Alice")
stmt = stmt.on_duplicate_key_update(name="Alice Updated")
The table must have a UNIQUE or PRIMARY KEY constraint for the duplicate detection to work.
MERGE Statement Errors¶
Symptom: MERGE statement compilation fails.
Checklist:
- Import from the correct module:
- All required clauses must be present:
stmt = (
merge(target)
.using(source) # Required
.on(target.c.id == source.c.id) # Required
.when_matched_then_update(...) # At least one WHEN clause
.when_not_matched_then_insert(...)
)
when_matched_then_delete()requires a priorwhen_matched_then_update():
# WRONG — delete without update
merge(t).using(s).on(condition).when_matched_then_delete()
# CORRECT — delete after update
merge(t).using(s).on(condition).when_matched_then_update({...}).when_matched_then_delete(where=...)
REPLACE INTO Behavior¶
REPLACE INTO deletes the existing row and inserts a new one (unlike ON DUPLICATE KEY UPDATE which updates in place):
from sqlalchemy_cubrid import replace
# This DELETES the existing row with the conflicting key,
# then INSERTs the new row
stmt = replace(users).values(id=1, name="Alice", email="alice@new.com")
Warning: REPLACE INTO causes the row to get a new auto-increment ID if the table uses AUTO_INCREMENT. If you want to preserve the existing row's ID, use ON DUPLICATE KEY UPDATE instead.
Alembic Migration Issues¶
No Implementation Found for Dialect 'cubrid'¶
Symptom:
Fix: Install with the alembic extra:
CubridImpl registers itself when the CUBRID dialect loads, provided Alembic is installed in the same environment. No manual configuration or env.py import is needed.
ALTER COLUMN TYPE Rejected (lossy conversion)¶
Symptom:
CUBRID supports changing a column's data type via native
ALTER TABLE ... MODIFY, which the dialect emits automatically. A change is
only rejected when it is lossy/incompatible and the
alter_table_change_type_strict system parameter is yes (when no, CUBRID
may silently truncate).
Normal usage (emits native MODIFY):
Workaround for genuinely lossy conversions — use batch_alter_table (table recreate):
def upgrade():
with op.batch_alter_table("users") as batch_op:
batch_op.alter_column("name", type_=sa.String(500))
def downgrade():
with op.batch_alter_table("users") as batch_op:
batch_op.alter_column("name", type_=sa.String(100))
batch_alter_table creates a new table, copies data, drops the original, and renames.
RENAME COLUMN¶
CUBRID supports ALTER TABLE ... RENAME COLUMN. The dialect emits it
natively; alter_column(new_column_name=...) no longer raises. A combined
rename + type change is emitted as a single CHANGE statement.
Normal usage (emits native RENAME COLUMN):
Partial Migration After a Failure¶
Symptom: A migration fails partway through, leaving the database in an inconsistent state.
Background: CUBRID DDL is transactional: with client autocommit off, which the dialect always sets, CREATE, ALTER, DROP, TRUNCATE, CREATE INDEX, RENAME and the rest are undone by ROLLBACK. CubridImpl sets transactional_ddl = True, so by default a failed alembic upgrade rolls back every revision of that run, including the alembic_version update. With transaction_per_migration=True, the revisions that finished stay committed and only the failing one is rolled back.
Cause: Something committed before the failure:
- client autocommit — driver-level autocommit, csql's default auto-commit mode, or
isolation_level="AUTOCOMMIT"where the dialect accepts it (#501) — commits every statement; - an offline (
--sql) script ran withoutcsql --no-auto-commit --no-single-line: in csql's default single-line mode, csql continues past a failing statement, still runs the trailingCOMMIT;and exits 0; - a revision issued
COMMITitself, for example throughop.execute().
Prevention:
- Run migrations without client autocommit, and run offline scripts with
csql --no-auto-commit --no-single-line(it stops at the first error and exits 1, and the open transaction is rolled back) - Use
transaction_per_migration=Truefor long or large-table migrations: uncommitted DDL holds schema locks, so other sessions wait until the transaction ends - Test migrations against a staging database first
- Back up the database before running migrations
Recovery:
# Check current state
alembic current
# Manually fix the database, then stamp to correct revision
alembic stamp <revision_id>
Autogenerate Not Detecting Changes¶
Possible causes:
- Models not imported — Alembic's autogenerate only sees models that are imported when
env.pyruns:
-
Table already exists with different schema — CUBRID reflection may not perfectly match your model definition (e.g.,
VARCHAR(4096)vsString()) -
Collection types — changes to
SET,MULTISET,SEQUENCEtypes may not be detected by autogenerate
Isolation Level Issues¶
Setting Isolation Levels¶
CUBRID's MVCC engine (10.0+) supports three isolation levels:
# Engine-level (applies to all connections)
engine = create_engine(
"cubrid://dba@localhost:33000/testdb",
isolation_level="REPEATABLE READ",
)
# Connection-level
with engine.connect().execution_options(
isolation_level="SERIALIZABLE"
) as conn:
result = conn.execute(text("SELECT * FROM accounts"))
Available levels:
| SQLAlchemy Name | CUBRID Numeric Level |
|---|---|
"SERIALIZABLE" |
6 |
"REPEATABLE READ" |
5 |
"READ COMMITTED" (default) |
4 |
"AUTOCOMMIT" |
driver autocommit mode (see Autocommit Conflicts) |
The legacy pre-MVCC levels (READ UNCOMMITTED and the granular
class/instance combinations that resolved to numeric codes 1–3) were
removed in CUBRID 10.0 and are no longer accepted; passing them to
create_engine(isolation_level=...) or execution_options(isolation_level=...)
raises sqlalchemy.exc.ArgumentError. See Isolation Levels.
DDL Runs in the Current Transaction¶
CUBRID does not implicitly commit DDL. With client autocommit off — the dialect turns it off on every connection — DDL is part of the current transaction, whatever the isolation level:
with engine.begin() as conn:
conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
conn.execute(text("CREATE TABLE temp (id INT)")) # not committed yet
conn.execute(text("INSERT INTO users (name) VALUES ('Bob')"))
raise RuntimeError("abort") # rolls back both INSERTs and the CREATE TABLE
The only auto-commit behavior is client autocommit (CCI_DEFAULT_AUTOCOMMIT / driver-level autocommit, csql's default mode, or isolation_level="AUTOCOMMIT" where the dialect accepts it (#501)), which commits after every statement, DDL or DML.
Watch out for schema locks: uncommitted DDL holds a schema lock on its table until the transaction ends, so other sessions that use the table wait (SCH_S_LOCK waits; CUBRID's default lock_timeout is unlimited, so they wait until the transaction ends unless a lock timeout is set). Commit DDL promptly, and keep long DDL transactions out of busy periods.
Transaction Issues¶
Data Not Persisted¶
Symptom: Data is inserted without errors but disappears after reconnecting.
Cause: Missing commit() or not using a transaction context.
Fix:
# Option 1: Explicit commit
with engine.connect() as conn:
conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
conn.commit()
# Option 2: begin() auto-commits on success
with engine.begin() as conn:
conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
# Auto-commits on successful exit
# Option 3: ORM session
with Session(engine) as session:
session.add(User(name="Alice"))
session.commit()
Autocommit Conflicts¶
Symptom: Statements commit unexpectedly.
Background: Both CUBRID drivers default to autocommit=True, but the dialect sets autocommit=False on each new connection so SQLAlchemy can manage transactions.
If you need true autocommit (each statement commits immediately), use
SQLAlchemy's AUTOCOMMIT isolation level. It works on all three drivers
(cubrid://, cubrid+pycubrid://, cubrid+aiopycubrid://):
with engine.connect().execution_options(
isolation_level="AUTOCOMMIT"
) as conn:
conn.execute(text("INSERT INTO logs (msg) VALUES ('event')"))
# Committed immediately; conn.rollback() does not undo it
create_engine(..., isolation_level="AUTOCOMMIT") and
engine.execution_options(isolation_level="AUTOCOMMIT") work too. A connection
switched to AUTOCOMMIT with execution_options() is transactional again when
it goes back to the pool. Releases before #501 raised ArgumentError
(execution_options) or ValueError (create_engine) for AUTOCOMMIT. See
Isolation Levels.
Savepoint Limitations¶
CUBRID supports SAVEPOINT and ROLLBACK TO SAVEPOINT, but does not support RELEASE SAVEPOINT. The dialect implements do_release_savepoint() as a no-op.
with engine.begin() as conn:
conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
savepoint = conn.begin_nested()
try:
conn.execute(text("INSERT INTO users (name) VALUES ('duplicate')"))
savepoint.commit()
except Exception:
savepoint.rollback()
# Alice's insert is still pending
conn.commit()
Performance Issues¶
Slow Schema Reflection¶
Symptom: inspector.get_table_names() or metadata.reflect() is slow.
Cause: Schema reflection queries system catalogs, which can be slow with many tables.
Fix: Reflect only the tables you need:
# SLOW — reflects ALL tables
metadata.reflect(bind=engine)
# FAST — reflect specific tables
metadata.reflect(bind=engine, only=["users", "products", "orders"])
Connection Overhead¶
Each connection performs a multi-step CAS handshake. Use connection pooling:
# Default pool (recommended for web apps)
engine = create_engine(
"cubrid://dba@localhost:33000/testdb",
pool_size=5,
pool_pre_ping=True,
)
# NullPool for scripts (no pooling)
from sqlalchemy.pool import NullPool
engine = create_engine(
"cubrid://dba@localhost:33000/testdb",
poolclass=NullPool,
)
Statement Caching¶
The dialect supports SQLAlchemy's statement caching (supports_statement_cache = True). This is enabled by default in SQLAlchemy 2.0 and significantly reduces compilation overhead for repeated queries.
No configuration needed — it works automatically.
Docker Issues¶
Apple silicon / arm64 Docker image: The course's
cubrid/cubrid:11.4 lab image is documented for amd64, not native arm64.
On a Mac with Apple silicon, try adding --platform linux/amd64 to the
Lesson 1 docker run example
to request Docker emulation; then use the existing three-minute database
readiness loop and inspect the container logs. This is unverified and can
be slower. If emulation does not work, use a reachable pre-existing CUBRID
server (Lesson 1 Option A). Do not replace the loopback port binding or
assume the image has native arm64 support.
Container Starts but Cannot Connect¶
Fix:
# 1. Check container is actually running
docker compose ps
# 2. Wait until the Compose healthcheck / actual database login succeeds
docker compose up -d
docker compose ps
docker compose exec -T cubrid csql -u dba testdb -c "SELECT 1"
# For the separate Lesson 1 lab use:
# docker logs sqlalchemy-cubrid-quickstart
# docker exec sqlalchemy-cubrid-quickstart csql -u dba testdb -c "SELECT 1"
# See Lesson 1's up-to-three-minute readiness loop before troubleshooting further.
# 3. Test connection
python3 -c "
from sqlalchemy import create_engine, text
engine = create_engine('cubrid+pycubrid://dba@localhost:33000/testdb')
with engine.connect() as conn:
print(conn.execute(text('SELECT 1')).scalar())
"
Database Not Created¶
The Docker image only creates the database specified in CUBRID_DB:
services:
cubrid:
image: cubrid/cubrid:11.4
environment:
CUBRID_DB: testdb # Only this database is created
ports:
- "127.0.0.1:33000:33000"
If your connection URL uses a different database name, it will fail. Match CUBRID_DB with your connection URL.
Version-Specific Behavior¶
Specify the CUBRID version explicitly:
# Default (11.2)
docker compose up -d
# Specific version
CUBRID_VERSION=11.4 docker compose up -d
CUBRID_VERSION=10.2 docker compose up -d
Version-specific checks:
| Feature | CUBRID 10.2 | CUBRID 11.0–11.4 |
|---|---|---|
LIMIT in UPDATE |
Not verified | Not verified |
CTE (WITH / WITH RECURSIVE) |
✅ Verified | ✅ Verified |
MERGE |
Not verified by version | Not verified by version |
| Index comments | Not verified | Not verified |
WITH and WITH RECURSIVE were checked live on 10.2, 11.0, 11.2 and 11.4; see CTE tests and boundaries and the support matrix. “Not verified” denotes missing per-version evidence in this guide, not a claim that the database lacks the feature.
Debugging Techniques¶
Troubleshooting Decision Flow¶
flowchart TD
start[Issue observed] --> kind{Primary symptom}
kind -->|Import / Module error| install[Check driver install]
kind -->|Connection failure| conn[Validate URL, host, port, credentials]
kind -->|SQL compile/runtime error| sql[Inspect generated SQL and unsupported feature]
kind -->|Migration failure| mig[Check Alembic limitations and client autocommit]
kind -->|Performance issue| perf[Check pool settings, indexes, statement patterns]
install --> i1{CUBRIDdb or pycubrid?}
i1 -->|CUBRIDdb| i2[Build CUBRIDdb from cubrid-python v11.3.0.51+ or switch to cubrid+pycubrid]
i1 -->|pycubrid| i3[Install pycubrid and use cubrid+pycubrid URL]
conn --> c1{Broker reachable?}
c1 -->|No| c2[Start broker / docker service and verify port 33000]
c1 -->|Yes| c3[Enable pool_pre_ping and review SESSION_TIMEOUT]
sql --> s1{Using unsupported feature?}
s1 -->|RETURNING / ARRAY| s2[Refactor using supported CUBRID patterns]
s1 -->|No| s3[Enable SQLAlchemy echo and inspect raw SQL]
mig --> m1[Split migration into atomic DDL steps]
perf --> p1[Profile query plan, batch size, lock scope]
Start with reproducibility
Reduce the failing case to a minimal script with one engine, one table, and one query. This quickly identifies whether the issue is driver, dialect, or application logic.
Distinguish compilation errors from runtime errors
- Compilation errors happen before SQL is sent.
- Runtime errors come from CUBRID or the driver. They need different debugging paths.
Capture environment details in issue reports
Include dialect version, SQLAlchemy version, driver (CUBRIDdb or pycubrid),
CUBRID server version, and the exact connection URL format used.
Enable SQL Logging¶
# Method 1: echo=True
engine = create_engine("cubrid://dba@localhost:33000/testdb", echo=True)
# Method 2: Python logging
import logging
logging.basicConfig()
logging.getLogger("sqlalchemy.engine").setLevel(logging.DEBUG)
Output includes SQL statements, parameters, and execution times.
Check Dialect Version¶
import sqlalchemy_cubrid
print(sqlalchemy_cubrid.__version__) # e.g., "1.8.0"
from sqlalchemy import create_engine
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")
print(engine.dialect.name) # "cubrid"
print(engine.dialect.server_version_info) # (11, 2, 0, 378)
Inspect Compiled SQL¶
from sqlalchemy import column, select, table
# Compile a statement for CUBRID without executing
from sqlalchemy_cubrid.dialect import CubridDialect
users = table("users", column("id"), column("name"))
stmt = select(users).where(users.c.name == "Alice")
compiled = stmt.compile(dialect=CubridDialect())
print(str(compiled))
print(compiled.params)
Test Connection Script¶
#!/usr/bin/env python3
"""Quick sqlalchemy-cubrid connection test."""
import sys
from sqlalchemy import create_engine, text, inspect
url = "cubrid+pycubrid://dba@localhost:33000/testdb"
try:
engine = create_engine(url)
with engine.connect() as conn:
version = conn.execute(text("SELECT VERSION()")).scalar()
print(f"✅ Connected to CUBRID {version}")
inspector = inspect(engine)
tables = inspector.get_table_names()
print(f"✅ Found {len(tables)} tables: {tables[:5]}{'...' if len(tables) > 5 else ''}")
print("✅ All checks passed")
except Exception as e:
print(f"❌ Failed: {e}")
sys.exit(1)
See also: Connection Guide · Type Mapping · DML Extensions · Alembic · Feature Support