Skip to content

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

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:

pip install "sqlalchemy-cubrid[cubriddb]"

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):

pip install "sqlalchemy-cubrid[pycubrid]"

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:

ModuleNotFoundError: No module named 'pycubrid'

Fix:

pip install pycubrid
# Or install both together
pip install "sqlalchemy-cubrid[pycubrid]"

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:

pip install "sqlalchemy-cubrid[pycubrid]"

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:

  1. 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"
  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.

  1. Wrong port: Check cubrid_broker.conf for the actual broker port.

  2. 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:

make docker-up
make docker-fix-db-host

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:

User "..." is invalid
Incorrect or missing password

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:

OperationalError: Connection is closed
OperationalError: broker is not available

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:

TimeoutError: QueuePool limit of size 5 overflow 10 reached

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:

CompileError: RETURNING is not supported by this dialect

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:

ProgrammingError: CAST to this type is not supported

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:

ProgrammingError: Syntax error near 'value'

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.

stmt = select(config.c.data["theme"])

If you see this error:

CompileError: JSON is not supported by this dialect

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:

INSERT INTO products (tags) VALUES ({'red', 'blue', 'green'});

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:

NoSuchTableError: table_name

Causes:

  1. Table doesn't exist — verify in CUBRID directly
  2. 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
  1. Wrong database — ensure your connection URL points to the correct database

Note: Reflection raises NoSuchTableError only for a missing object: the db_class lookup finds no such table or view, the server reports Unknown 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 example sqlalchemy.exc.ProgrammingError), even though CUBRID uses native error -493 for both (and pycubrid before 1.8.0 reports SQLSTATE 42S02, Table not found, for every -493 error) (#454).

Note: get_foreign_keys() and get_unique_constraints() no longer turn a failed SHOW CREATE TABLE into 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 proposing add_fk for foreign keys that already exist. Retry on a fresh connection (pool_pre_ping=True replaces 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:

CompileError: sequences are not supported by this dialect

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:

  1. Import from the correct module:
from sqlalchemy_cubrid.dml import merge
  1. 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(...)
)
  1. when_matched_then_delete() requires a prior when_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:

KeyError: 'cubrid'   (raised from alembic/ddl/impl.py, DefaultImpl.get_by_dialect)

Fix: Install with the alembic extra:

pip install "sqlalchemy-cubrid[alembic]"

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:

Error: cannot coerce value ... (incompatible type change)

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):

def upgrade():
    op.alter_column("users", "name", type_=sa.String(500))

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):

def upgrade():
    op.alter_column("users", "old_name", new_column_name="new_name")

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 without csql --no-auto-commit --no-single-line: in csql's default single-line mode, csql continues past a failing statement, still runs the trailing COMMIT; and exits 0;
  • a revision issued COMMIT itself, for example through op.execute().

Prevention:

  1. 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)
  2. Use transaction_per_migration=True for long or large-table migrations: uncommitted DDL holds schema locks, so other sessions wait until the transaction ends
  3. Test migrations against a staging database first
  4. 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:

  1. Models not imported — Alembic's autogenerate only sees models that are imported when env.py runs:
# In env.py — import all models
from myapp.models import Base
target_metadata = Base.metadata
  1. Table already exists with different schema — CUBRID reflection may not perfectly match your model definition (e.g., VARCHAR(4096) vs String())

  2. Collection types — changes to SET, MULTISET, SEQUENCE types 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