Skip to content

Troubleshooting Guide

Comprehensive solutions for common pycubrid issues — connection errors, query problems, type mismatches, LOB handling, performance tuning, and Docker setup.


Table of Contents


Troubleshooting Decision Tree

flowchart TD
    A["Start: pycubrid error observed"] --> B{Connection established?}
    B -->|No| C[Check broker status and port reachability]
    C --> D{Authentication error?}
    D -->|Yes| E[Verify user/password and database]
    D -->|No| F[Set connect_timeout and inspect network/firewall]
    B -->|Yes| G{Query or transaction failure?}
    G -->|Query| H[Validate SQL syntax and placeholder count]
    G -->|Transaction| I[Confirm autocommit and explicit commit/rollback]
    G -->|LOB| J[Validate LOB type and read/write flow]
    H --> K[Catch specific pycubrid exceptions]
    I --> K
    J --> K
    E --> K
    F --> K
    K --> L[Use debug script and logs, then escalate with reproducible case]

Tip

Start from the first failing operation (connect, execute, fetch, or commit) and isolate one variable at a time.

Note

Most production failures can be classified quickly into one of four buckets: connectivity, authentication, SQL/binding, or transaction state.


Connection Issues

ConnectionRefusedError on Port 33000

Symptom:

ConnectionRefusedError: [Errno 111] Connection refused

Causes and fixes:

Warning

CUBRID startup is asynchronous in Docker-based setups. A successful container start does not always mean the broker is already accepting connections.

  1. CUBRID broker is not running
# Check if broker is running
cubrid broker status

# Start the broker
cubrid broker start
  1. Wrong port — The broker may be configured on a different port.
# Check broker port in configuration
cat $CUBRID/conf/cubrid_broker.conf | grep BROKER_PORT
  1. Docker container not ready — The CUBRID container takes a few seconds to initialize.
# Check container status
docker compose ps

# Wait for health check
docker compose up -d --wait

# If the wait times out, inspect the service state and logs
docker compose ps
docker compose logs cubrid | tail -20
  1. Firewall or network — Port 33000 may be blocked.
# Test port connectivity
nc -zv localhost 33000

# On macOS/Linux
telnet localhost 33000

Authentication Failed

Symptom:

OperationalError: Incorrect or missing password. (errno=-171, description='Invalid password', sqlstate='28000')
OperationalError: User "app" is invalid. (errno=-165, description='Invalid user', sqlstate='28000')

Fixes:

CUBRID's default dba user has no password. If you set a password, make sure it matches:

# Default — no password
conn = pycubrid.connect(
    host="localhost",
    port=33000,
    database="testdb",
    user="dba",
)

# With password
conn = pycubrid.connect(
    host="localhost",
    port=33000,
    database="testdb",
    user="dba",
    password="your_password",
)

Common mistakes: - Passing password="" when the user has a password set - Passing a password when the user has no password (some CUBRID versions reject this) - Wrong username — CUBRID usernames are case-insensitive but must exist


TimeoutError or socket.timeout

Symptom:

TimeoutError: [Errno 110] Connection timed out
socket.timeout: timed out

For request I/O, sync read_timeout applies to each socket operation, so a reply arriving slowly in pieces can take longer than that duration if no individual receive times out. Async read_timeout bounds the whole request round trip. See What the timeouts bound for details.

Fixes:

  1. Increase timeout for slow networks:
conn = pycubrid.connect(
    host="remote-server.example.com",
    port=33000,
    database="testdb",
    user="dba",
    connect_timeout=30.0,  # 30-second timeout
)
  1. Verify server is reachable:
ping remote-server.example.com
nc -zv remote-server.example.com 33000
  1. Check for network firewalls between client and server.

Connection Closed Unexpectedly

Symptom:

InterfaceError: connection is closed
OperationalError: socket communication failed
OperationalError: connection lost during receive

The first message is raised by calls made after a connection has already closed. The other two are raised by the call that lost the transport.

Causes:

  • Server-side session timeout — CUBRID broker has a SESSION_TIMEOUT setting. Default is 300 seconds (5 minutes) of inactivity.
  • Broker restart — If the broker restarts, all existing connections are terminated.
  • Network interruption — Temporary network failure drops the TCP connection.
  • Idle connection cleanup — The broker may close idle connections to free resources.
  • Malformed broker reply — the call itself raised OperationalError: malformed response from broker because the reply could not be read as a whole: a length field (a BIT/VARBIT, string, NUMERIC, collection or LOB byte count) that is negative or runs past the end of the reply, a row cell whose value does not use exactly its declared size (#523), or collection elements that do not fill their declared size. The driver closes the connection, because the next reply boundary is unknown, and later calls raise InterfaceError. Bytes after the last value a reply declares are not an error. A complete reply with a value Python cannot represent raises DataError and keeps the connection instead (see Invalid UTF-8 and Zero Date). Earlier releases returned a shortened value for a cut-off field and kept the connection (#383).

Fix: Create a new connection when this error occurs:

import pycubrid

def get_connection():
    return pycubrid.connect(
        host="localhost",
        port=33000,
        database="testdb",
        user="dba",
    )

conn = get_connection()
try:
    cur = conn.cursor()
    cur.execute("SELECT 1")
except (pycubrid.OperationalError, pycubrid.InterfaceError):
    # Reconnect on connection loss
    conn = get_connection()
    cur = conn.cursor()
    cur.execute("SELECT 1")

For long-running applications, use SQLAlchemy with connection pooling — it handles reconnection automatically:

from sqlalchemy import create_engine

engine = create_engine(
    "cubrid+pycubrid://dba@localhost:33000/testdb",
    pool_pre_ping=True,  # Test connection before use
    pool_recycle=1800,    # Recycle connections every 30 minutes
)

Broker Port Redirect Failure

Symptom:

OperationalError: ... (during connection handshake)

Background: When pycubrid connects to port 33000, the CUBRID broker may redirect the connection to a different CAS (CUBRID Application Server) port. If the redirect port is unreachable, the connection fails.

Fix:

  1. Check CAS processes are running:
cubrid broker status -b  # Shows broker and CAS process details
  1. Ensure all CAS ports are reachable — If using Docker with port forwarding, only port 33000 may be exposed. When the broker redirects to a different port, the connection fails if that port is not forwarded.

Docker fix — Expose a range of ports, or configure the broker to reuse the connection (port 0 mode):

# docker-compose.yml
services:
  cubrid:
    image: cubrid/cubrid:11.2
    ports:
      - "127.0.0.1:33000:33000"
    environment:
      CUBRID_DB: testdb
The default Docker image is configured correctly for single-port access. If you see this error with Docker, check that you're not overriding broker configuration.

Async TLS Connect Hangs After a Stalled or Reset Handshake

Symptom (pycubrid releases before the fix for #513): on Python 3.11+, await pycubrid.aio.connect(..., ssl=...) never returns, even with read_timeout set, when the broker (or a proxy or middlebox in front of it) accepts the plaintext CUBRS handshake but then stalls or resets the connection before the TLS handshake completes.

Cause: the TLS handshake timed out or failed as intended, but asyncio's SSLProtocol does not report the lost connection to the stream while it is still handshaking, so the connect cleanup waited forever for the stream to close.

Fix: upgrade pycubrid. The async driver now raises OperationalError within read_timeout (or the 10-second ssl_handshake_timeout when read_timeout is unset) and closes the socket. connect_timeout only bounds the TCP connect, so set read_timeout to bound the TLS handshake. The sync driver was not affected.


Connection Option Has No Effect

Symptom:

A connection option is accepted but does nothing — the timeout never changes, timing stats stay empty, the fetch batch size is unchanged — and a warning like this appears:

UnknownConnectionOptionWarning: Unknown connection option ignored by pycubrid:
'read_timout' (did you mean 'read_timeout'?). Supported options: autocommit,
connect_timeout, database, decode_collections, enable_timing, fetch_size, host,
json_deserializer, no_backslash_escapes, password, port, read_timeout, ssl, user.

Cause:

The keyword is not a supported connection option, so it lands in **kwargs and is discarded. Usually a typo (read_timout), a camelCase spelling (connectTimeout), or an option borrowed from a different driver.

Fixes:

  1. Use the spelling the warning suggests, or pick from the supported set it lists. The full reference is in Connection Options.

  2. Catch typos automatically in development by making the warning fatal:

import warnings
import pycubrid

warnings.simplefilter("error", pycubrid.UnknownConnectionOptionWarning)

(Install this filter from Python. python -W and PYTHONWARNINGS are parsed at interpreter startup, before site-packages is importable, so -W error::pycubrid.UnknownConnectionOptionWarning is rejected with Invalid -W option ignored: invalid module name: 'pycubrid' even when pycubrid is installed. python -W error::UserWarning works, but it escalates every UserWarning, not just this one.)

  1. If a wrapper legitimately forwards extra keywords (a connection pool or an ORM dialect), silence the category instead of chasing each one:
warnings.simplefilter("ignore", pycubrid.UnknownConnectionOptionWarning)

Note: If you see no warning at all, check that warnings are not globally suppressed — python -W default restores the default display. Warnings are also hidden by default in some test runners.


Query Issues

ProgrammingError: SQL Syntax

Symptom:

ProgrammingError: Syntax error ...

Common causes:

  1. Using MySQL/PostgreSQL-specific syntax — CUBRID has its own SQL dialect:
# WRONG — CUBRID doesn't support LIMIT with comma syntax
cur.execute("SELECT * FROM users LIMIT 0, 10")

# CORRECT — use LIMIT with OFFSET
cur.execute("SELECT * FROM users LIMIT 10 OFFSET 0")
  1. Using reserved words as identifiers — Quote them with double quotes:
# WRONG — 'value' is a reserved word
cur.execute("SELECT value FROM config")

# CORRECT — quote the identifier
cur.execute('SELECT "value" FROM config')

# BETTER — avoid reserved words
cur.execute("SELECT val FROM config")
  1. Missing semicolons are fine — pycubrid does not require trailing semicolons (and they may cause errors in some contexts).

Parameter Binding Errors

Symptom:

ProgrammingError: Cannot convert parameter ...

Danger

pycubrid supports qmark placeholders (?) only. Mixing %s, :name, or f-string SQL construction often causes subtle runtime errors or SQL injection risk.

pycubrid uses qmark paramstyle (question marks). Do not use named parameters or format strings:

# CORRECT — qmark style
cur.execute("SELECT * FROM users WHERE name = ? AND age > ?", ("Alice", 25))

# WRONG — named parameters (not supported)
cur.execute("SELECT * FROM users WHERE name = :name", {"name": "Alice"})

# WRONG — format string (SQL injection risk!)
cur.execute(f"SELECT * FROM users WHERE name = '{name}'")

# WRONG — %s style (not supported)
cur.execute("SELECT * FROM users WHERE name = %s", ("Alice",))

Supported Python types for parameters:

Python Type SQL Result
None NULL
bool 1 or 0
int, float Numeric literal
Decimal Numeric literal
str 'escaped_string'
bytes X'hex_string'
datetime.date DATE'YYYY-MM-DD'
datetime.time TIME'HH:MM:SS'
datetime.datetime DATETIME'YYYY-MM-DD HH:MM:SS.mmm'

Wrong Number of Parameters

Symptom:

ProgrammingError: Incorrect number of bindings supplied

Fix: Ensure the number of ? placeholders matches the number of parameters:

# WRONG — 2 placeholders, 1 parameter
cur.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Alice",))

# CORRECT — 2 placeholders, 2 parameters
cur.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Alice", 30))

For single parameters, pass a tuple (not a bare value):

# WRONG — string is iterable, each character becomes a parameter
cur.execute("SELECT * FROM users WHERE name = ?", "Alice")

# CORRECT — wrap in a tuple
cur.execute("SELECT * FROM users WHERE name = ?", ("Alice",))

Reserved Word Conflicts

The token reported after unexpected marks the server's diagnostic position; it may follow the offending identifier. For example, CREATE TABLE t (key VARCHAR(50)) reports unexpected 'VARCHAR' on captured CUBRID 10.2, 11.2 and 11.4 brokers. pycubrid's appended hint therefore says an identifier at or before that position may be reserved. It does not identify the offending name or tell you to quote the datatype VARCHAR; inspect the identifier near that position and quote or rename it. The original server message and error metadata are preserved.

Common CUBRID reserved words that often clash with column/table names:

Reserved Word Safe Alternative
value val, item_value
count cnt, item_count
data file_data, raw_data
level user_level, access_level
name Usually OK, but check if issues occur
status item_status
type item_type
action user_action

To use reserved words as identifiers, quote them with double quotes:

cur.execute('CREATE TABLE "order" (id INT, "value" VARCHAR(100))')
cur.execute('SELECT "value" FROM "order"')

Empty Result Set

Symptom: fetchone() returns None or fetchall() returns [] when you expect data.

Common causes:

  1. Uncommitted INSERT — data was inserted but not committed:
cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
conn.commit()  # Don't forget this!
cur.execute("SELECT * FROM users WHERE name = ?", ("Alice",))
print(cur.fetchall())
  1. Different connection — each connection has its own transaction view. Uncommitted data in one connection is not visible in another.

  2. Case sensitivity — CUBRID string comparison is case-sensitive by default:

# These return different results
cur.execute("SELECT * FROM users WHERE name = ?", ("alice",))
cur.execute("SELECT * FROM users WHERE name = ?", ("Alice",))

Transaction Issues

Data Not Persisted After Insert

Symptom: Data is inserted successfully (no error), but a subsequent query from a different connection or after reconnection shows no data.

Cause: autocommit is False by default in pycubrid when using the constructor directly. You must call conn.commit() explicitly.

conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")

cur = conn.cursor()
cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
conn.commit()  # Required! Without this, data is lost on close
conn.close()

Or use the context manager which auto-commits on success:

with pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba") as conn:
    cur = conn.cursor()
    cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
    # Auto-commits on successful exit

Autocommit Behavior

Symptom: Unexpected commit or rollback behavior.

Key facts:

Scenario autocommit Behavior
Default constructor False (driver default) Explicit commit() required
Via SQLAlchemy False (dialect sets it) SQLAlchemy manages transactions
Context manager exit N/A Commits on success, rollbacks on exception

To switch modes:

# Check current mode
print(conn.autocommit)  # False

# Disable for manual transaction control
conn.autocommit = False

cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
cur.execute("INSERT INTO users (name) VALUES (?)", ("Bob",))
conn.commit()  # Both inserts committed together

Deadlocks

Symptom:

OperationalError: Deadlock detected ...

CUBRID uses row-level locking. Deadlocks occur when two connections hold locks that each other needs.

Prevention:

  1. Keep transactions short
  2. Access tables in a consistent order
  3. Use SELECT ... FOR UPDATE to lock rows upfront
  4. Set appropriate isolation levels
# Lock rows before updating to prevent deadlocks
cur.execute("SELECT * FROM accounts WHERE id = ? FOR UPDATE", (1,))
cur.execute("UPDATE accounts SET balance = balance - 100 WHERE id = ?", (1,))
conn.commit()

Type Mapping Issues

Date/Time Handling

CUBRID type → Python type mapping:

CUBRID Type Python Type Example
DATE datetime.date date(2025, 1, 15)
TIME datetime.time time(14, 30, 0)
DATETIME datetime.datetime datetime(2025, 1, 15, 14, 30, 0)
TIMESTAMP datetime.datetime datetime(2025, 1, 15, 14, 30, 0)

Common issue — inserting date strings:

# CORRECT — use Python datetime objects
from datetime import date, datetime

cur.execute("INSERT INTO events (event_date) VALUES (?)", (date(2025, 1, 15),))
cur.execute("INSERT INTO events (event_time) VALUES (?)", (datetime(2025, 1, 15, 14, 30, 0),))

# ALSO CORRECT — CUBRID accepts date literal strings in SQL
cur.execute("INSERT INTO events (event_date) VALUES (DATE'2025-01-15')")

Decimal Precision Loss

Symptom: Decimal values lose precision when inserted or retrieved.

Fix: Use decimal.Decimal for exact numeric values:

from decimal import Decimal

# CORRECT — preserves precision
cur.execute("INSERT INTO products (price) VALUES (?)", (Decimal("19.99"),))

# RISKY — float has inherent precision issues
cur.execute("INSERT INTO products (price) VALUES (?)", (19.99,))

NULL Handling

Inserting NULL:

cur.execute("INSERT INTO users (name, email) VALUES (?, ?)", ("Alice", None))

Checking for NULL in results:

cur.execute("SELECT email FROM users")
row = cur.fetchone()
if row[0] is None:
    print("Email is NULL")

Boolean Values

CUBRID has no native BOOLEAN type. Use SMALLINT (0/1):

# Insert boolean-like values
cur.execute("INSERT INTO settings (is_active) VALUES (?)", (True,))   # Stored as 1
cur.execute("INSERT INTO settings (is_active) VALUES (?)", (False,))  # Stored as 0

# Read boolean-like values
cur.execute("SELECT is_active FROM settings")
row = cur.fetchone()
is_active = bool(row[0])  # Convert SMALLINT back to bool

Unicode / NCHAR Encoding

CUBRID supports Unicode through NCHAR and NCHAR VARYING types. pycubrid handles UTF-8 encoding transparently:

# Unicode strings work directly
cur.execute("INSERT INTO users (name) VALUES (?)", ("김영선",))
cur.execute("INSERT INTO users (name) VALUES (?)", ("日本語テスト",))

cur.execute("SELECT name FROM users")
for row in cur:
    print(row[0])  # Prints correctly: 김영선, 日本語テスト

This assumes a UTF-8 database (the default). For a database created with another charset, connect with that charset, for example charset="euckr" for ko_KR.euckr; the broker does no conversion, so a mismatched client either cannot encode a value or cannot decode a reply and raises DataError. See Character Encoding.

Invalid UTF-8 in a Value or Error Message

CUBRID counts VARCHAR(n) sizes and some echoed error text in bytes, so it can cut a string in the middle of a multi-byte character. CUBRID 10.2, for example, stores '\U00010000' * 13 in a VARCHAR(50) as 50 bytes, ending with half a character.

  • Error messages: invalid bytes are replaced with U+FFFD, and the real CUBRID error is raised with its errno and sqlstate.
  • Column values: a CHAR/VARCHAR/NCHAR/ENUM/JSON value that is not valid UTF-8 raises DataError; the original UnicodeDecodeError is its __cause__. The connection stays usable. To inspect the stored bytes, select HEX(col) instead, then fix the stored value.
  • Charset mismatch: the same DataError names the connection codec, for example column value is not valid UTF-8 from a default client on an EUC-KR database, or column value is not valid euc_kr for a CHARSET utf8 column read with charset="euckr". Connect with the database charset, or convert the column in SQL: CAST(col AS VARCHAR(n) CHARSET euckr). Undecodable column names raise DataError the same way (#86). JSON is always UTF-8.

Earlier releases raised OperationalError: malformed response from broker and closed the connection.

Unresolved Time Zone in a TZ Value

pycubrid.exceptions.DataError: cannot resolve CUBRID timezone 'Asia/Seoul': it is
not in the client's IANA time zone database (install the 'tzdata' package or
update the system zoneinfo)

A TIMESTAMPTZ, TIMESTAMPLTZ, DATETIMETZ or DATETIMELTZ value named a region that Python's zoneinfo cannot find. The LTZ types report the session zone, so even UTC needs the database. Usual causes:

  • No time zone database on the client — minimal container images (python:*-slim, distroless, Alpine without tzdata). Run pip install tzdata, or install the OS tzdata package. pycubrid already depends on tzdata on Windows.
  • The client database is older than the server's — update tzdata or the OS package.
  • PYTHONTZPATH points somewhere empty — unset it or fix the path.

The connection stays usable. Offsets such as +09:00 need no database; an offset outside ±24 hours raises DataError: cannot resolve CUBRID timezone offset .... The explicit prepared API (pycubrid.compat.native) stays fail-closed, as for invalid UTF-8: it raises OperationalError and retires the session. To read the values without zone resolution, select them as text, e.g. SELECT TO_CHAR(col).

Earlier releases logged Unknown timezone token and returned a naive datetime, silently dropping the zone (#413).

Zero Date or Datetime Value

pycubrid.exceptions.DataError: CUBRID DATE value (0, 0, 0) cannot be represented
in Python: year 0 is out of range

CUBRID accepts zero values such as DATE'0000-00-00' and DATETIME'0000-00-00 00:00:00' (also for TIMESTAMP and the TZ/LTZ types), for example from CAST('0000-00-00' AS DATE) or data loaded from another system. Python's datetime has no year 0, so pycubrid raises DataError when such a value is fetched, whether it is in the first page returned by execute() or in a later fetch page. The reply was read in full, so the connection stays usable; after a failed execute() the cursor has no result set (description is None) but still owns and releases the server handle, as for invalid UTF-8.

On a later fetch page, the fetch call that reaches the page raises and the whole page is withheld, but rows that call had already collected are kept: the next fetchmany()/fetchall() returns them, and every fetch after that raises the same DataError without asking the server again, until you execute a new query (#507). For example:

cur.execute("SELECT id, d FROM t ORDER BY id")
try:
    rows = cur.fetchall()
except pycubrid.DataError:
    rows = cur.fetchall()  # rows before the failing page
    # cur.fetchone() now raises the same DataError; re-execute with a
    # converted column (below) to read the rest.

The same applies to invalid text (#492) and unresolved zones (#413). Earlier releases dropped the rows collected by that call and requested the page again on every retry, which in autocommit mode could fail with CAS error -1012 because the broker had already closed the result.

pycubrid has no option to return zero dates as None or text. Convert them in SQL instead:

SELECT id, NULLIF(d, DATE'0000-00-00') AS d FROM t;              -- zero -> NULL
SELECT id, CASE WHEN d = DATE'0000-00-00' THEN NULL ELSE d END FROM t;
SELECT id, TO_CHAR(d, 'YYYY-MM-DD') AS d FROM t;                 -- '0000-00-00'
SELECT id FROM t WHERE d = DATE'0000-00-00';                     -- find them

Use DATETIME'0000-00-00 00:00:00' (or the matching type) for the other types. The explicit prepared API (pycubrid.compat.native) stays fail-closed, as for invalid UTF-8: it raises OperationalError and retires the session. A reply that is cut short is still OperationalError: malformed response from broker, even when it also contains a zero date.

Earlier releases raised OperationalError: malformed response from broker and closed the connection (#512).

Invalid JSON Text in a Value

pycubrid.exceptions.DataError: JSON value is not valid JSON: Expecting value:
line 1 column 1 (char 0)

With json_deserializer=json.loads (see JSON Columns), a JSON column value that is not valid JSON text raises DataError; the original json.JSONDecodeError is its __cause__. Valid JSON text that json.loads cannot decode raises DataError: JSON value cannot be decoded in the same way: an integer longer than Python's integer string conversion limit (ValueError) or nesting deeper than the recursion limit (RecursionError) (#900). The reply was read in full, so the connection stays usable, whether the value is in the first page returned by execute() or in a later fetch page. After a failed execute() the cursor has no result set (description is None), but, exactly as for invalid UTF-8 (#492) and zero dates (#512), it still owns and releases its server handle. On a later fetch page the same #507 rules apply: the fetch call that reaches the page raises, rows it had already collected are kept, and every fetch after that raises the same DataError without asking the server again, until you execute a new query.

Without json_deserializer (the default), a JSON column is returned as its raw str, so this does not apply: str.__new__ never fails on the text CUBRID sends. Only the built-in json.loads path above is reclassified: a caller-supplied json_deserializer callable is invoked as-is, and whatever it raises is not turned into DataError.

This is ordinary-cursor behavior. The explicit prepared API (pycubrid.compat.native), which threads the same json_deserializer, stays fail-closed as for invalid UTF-8 and zero dates: it raises OperationalError and retires the session.

Earlier releases raised OperationalError: malformed response from broker and closed the connection (#543).


LOB (CLOB/BLOB) Issues

LOB Columns Return a Dict, Not Data

Symptom: Fetching a CLOB/BLOB column returns a dictionary instead of the actual data.

cur.execute("SELECT clob_col FROM my_table")
row = cur.fetchone()
print(row[0])
# {'lob_type': 24, 'lob_length': 1234, 'file_locator': '...', 'packed_lob_handle': b'...'}

This is expected behavior. CUBRID's CAS protocol returns LOB metadata, not the LOB content inline. To read LOB content, you need to use the LOB handle separately.

Workaround — insert and retrieve as regular strings/bytes:

# Insert string directly into CLOB column
cur.execute("INSERT INTO docs (content) VALUES (?)", ("Large text content here...",))
conn.commit()

# Fetches return the metadata dict for CLOB/BLOB columns; use the packed handle to read content

Cannot Pass Lob Object as Parameter

Symptom:

ProgrammingError: Cannot convert parameter of type Lob

Lob objects cannot be used as query parameters of execute() or the default executemany(). Insert strings/bytes directly (or, with autocommit off on the sync cursor, use executemany(..., prepared=True), which binds a Lob of the same connection by its handle):

# WRONG — Lob objects cannot be passed as parameters
lob = conn.create_lob(24)  # CLOB
lob.write(b"data")
cur.execute("INSERT INTO docs (content) VALUES (?)", (lob,))  # ERROR!

# CORRECT — pass string directly
cur.execute("INSERT INTO docs (content) VALUES (?)", ("Large text content",))

# CORRECT — pass bytes for BLOB
cur.execute("INSERT INTO docs (binary_data) VALUES (?)", (b"\x89PNG\r\n...",))

LOB Size Limits

CUBRID LOB size limits depend on the server configuration. The default maximum is typically sufficient for most use cases, but extremely large objects may need server-side configuration adjustments.

For files larger than a few megabytes, consider:

  1. Storing file paths in the database instead of file content
  2. Breaking large content into chunks
  3. Using CUBRID's file storage configuration options

Cursor Issues

InterfaceError: Cursor is Closed

Symptom:

InterfaceError: Cursor is closed

Causes:

  1. Explicitly closed cursor — you called cur.close() then tried to use it again
  2. Connection closed — closing a connection closes all its cursors
  3. Context manager exited — with conn.cursor() as cur: closes the cursor on exit

Fix: Create a new cursor:

cur = conn.cursor()
cur.execute("SELECT 1")

fetchone() Returns None Unexpectedly

Possible causes:

  1. No rows in result set — the query returned 0 rows
  2. Already consumed — previous fetchone() or fetchall() consumed all rows
  3. Non-SELECT statement — INSERT, UPDATE, DELETE don't produce rows
cur.execute("SELECT * FROM users")
row1 = cur.fetchone()  # First row or None
row2 = cur.fetchone()  # Second row or None
# ... continues until None (no more rows)

To re-read results, execute the query again:

cur.execute("SELECT * FROM users")
all_rows = cur.fetchall()  # Get all at once
# cur.fetchone() would now return None — results already consumed

rowcount Is -1 After SELECT

This is correct PEP 249 behavior. rowcount is only meaningful for INSERT, UPDATE, DELETE statements:

cur.execute("SELECT * FROM users")
print(cur.rowcount)  # -1 (undefined for SELECT)

cur.execute("UPDATE users SET name = 'Bob' WHERE id = 1")
print(cur.rowcount)  # 1 (one row affected)

cur.execute("DELETE FROM users WHERE id > 100")
print(cur.rowcount)  # Number of deleted rows

executemany() Performance

For bulk inserts and other non-SELECT DML, executemany() does not execute each parameter set as a separate round trip. It renders each bound statement and sends the full batch in one BatchExecutePacket. Only SELECT statements fall back to the per-parameter loop to preserve result-set semantics. Use executemany_batch() when you already have distinct SQL strings and want to send them in one batch request:

# Standard executemany — non-SELECT DML batches into one request
data = [("Alice", 30), ("Bob", 25), ("Charlie", 35)]
cur.executemany("INSERT INTO users (name, age) VALUES (?, ?)", data)

# executemany_batch — sends multiple statements in one request
sql_list = [
    "INSERT INTO users (name, age) VALUES ('Alice', 30)",
    "INSERT INTO users (name, age) VALUES ('Bob', 25)",
    "INSERT INTO users (name, age) VALUES ('Charlie', 35)",
]
cur.executemany_batch(sql_list)

Performance comparison:

Method Round Trips Best For
execute() in loop N Few rows
executemany() for INSERT/UPDATE/DELETE 1 Parameterized bulk DML
executemany() for SELECT N Repeated SELECTs with separate parameter sets
executemany_batch() 1 Many distinct SQL statements

Parameterized Query Issues

execute(sql, params) Pattern

Correct pattern:

sql = "SELECT * FROM users WHERE department = ?"

# Execute with SQL + parameters
cur.execute(sql, ("Engineering",))
engineers = cur.fetchall()

cur.execute(sql, ("Marketing",))
marketers = cur.fetchall()

Key points:

  • Always pass the SQL string as the first argument to execute()
  • Pass parameter values in the second argument
  • The driver renders the parameters into SQL literals on the client and sends the complete SQL with CAS PREPARE_AND_EXECUTE on each call; there is no cursor.prepare(), typed value payload or driver-side statement-handle cache (see Parameter Binding)

Mixing Parameterized and Direct Execution

You can safely mix parameterized and direct SQL execution on one cursor:

cur.execute("SELECT * FROM users WHERE id = ?", (1,))

cur.execute("SELECT * FROM departments")

cur.execute("SELECT * FROM users WHERE id = ?", (2,))

Best practice: Keep SQL explicit at each call site:

sql = "SELECT * FROM users WHERE id = ?"

cur.execute(sql, (1,))
cur.execute(sql, (2,))
cur.execute("SELECT COUNT(*) FROM users")

Docker Issues

Container Starts but Cannot Connect

Check 1: Container is actually running:

docker compose ps
# Should show "running" status

Check 2: Wait for initialization — CUBRID takes a few seconds to start:

docker compose up -d --wait

# If the wait times out, inspect the service state and logs
docker compose ps
docker compose logs cubrid

# Test connection
python3 -c "
import pycubrid
conn = pycubrid.connect(host='localhost', port=33000, database='testdb', user='dba')
print('Connected!')
print('Version:', conn.get_server_version())
conn.close()
"

Check 3: Port mapping is correct:

docker compose ps
# Verify 33000->33000/tcp is shown

Database Server Host Not Found After Recreating the Container

Symptom: after docker compose down and up (or a second make integration), 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): ee2bfa1105f9 (errno=-353, sqlstate='HY000')

Inside the container, csql -u dba testdb -c "SELECT 1" reports Cannot find ("ee2bfa1105f9") in /etc/hosts, 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. The repository's 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 reports this case at its first -353 instead of retrying 36 times.

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 first run saves the previous file next to it as databases.txt.before-fix-db-host; a later run keeps that copy.

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.


Database Not Found

Symptom:

OperationalError: ... database 'mydb' not found

The Docker image creates only the database specified in CUBRID_DB:

# docker-compose.yml
services:
  cubrid:
    image: cubrid/cubrid:11.2
    environment:
      CUBRID_DB: testdb  # Only this database is created

Fix: Either: 1. Set CUBRID_DB to match your connection's database name 2. Create the database manually inside the container:

docker compose exec cubrid cubrid createdb mydb
docker compose exec cubrid cubrid server start mydb

Container Health Check

Add a health check that logs in to the database, as the repository's docker-compose.yml does. A cubrid broker status check is not enough: the broker reports healthy before testdb accepts logins. The 127.0.0.1 binding keeps the empty-password dba account off the network.

services:
  cubrid:
    image: cubrid/cubrid:11.2
    environment:
      CUBRID_DB: testdb
      CUBRID_DB_HOST: localhost
    ports:
      - "127.0.0.1:33000:33000"
    healthcheck:
      test: ["CMD", "csql", "-u", "dba", "testdb", "-c", "SELECT 1"]
      interval: 15s
      timeout: 10s
      retries: 10
      start_period: 30s

Wait for health check in tests:

docker compose up -d --wait
# Proceeds only after health check passes

SQLAlchemy Integration Issues

Wrong Connection URL Format

Correct URL format for pycubrid:

# pycubrid driver
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")

# With password
engine = create_engine("cubrid+pycubrid://dba:password@localhost:33000/testdb")

Common mistakes:

# WRONG — missing driver specification (defaults to C-extension driver)
engine = create_engine("cubrid://dba@localhost:33000/testdb")

# WRONG — wrong port format
engine = create_engine("cubrid+pycubrid://dba@localhost/testdb?port=33000")

# WRONG — wrong scheme
engine = create_engine("pycubrid://dba@localhost:33000/testdb")

Autocommit Conflicts

Symptom: Data is committed even though you haven't called session.commit().

Cause: The CUBRID server default is autocommit=True. SQLAlchemy's pycubrid dialect sets autocommit=False on each new connection, but if the dialect is misconfigured, the server default takes effect.

Fix: Ensure you're using cubrid+pycubrid:// in the connection URL, which loads the correct dialect that manages autocommit properly.


Connection Pool Exhaustion

Symptom:

TimeoutError: QueuePool limit of size 5 overflow 10 reached

Fix: Tune the connection pool:

from sqlalchemy import create_engine

engine = create_engine(
    "cubrid+pycubrid://dba@localhost:33000/testdb",
    pool_size=10,        # Maximum persistent connections
    max_overflow=20,     # Additional connections beyond pool_size
    pool_timeout=30,     # Seconds to wait for available connection
    pool_pre_ping=True,  # Test connections before use
    pool_recycle=1800,   # Recycle connections every 30 minutes
)

Ensure connections are returned to the pool:

# CORRECT — context manager returns connection
with engine.connect() as conn:
    result = conn.execute(text("SELECT 1"))

# WRONG — connection never returned
conn = engine.connect()
result = conn.execute(text("SELECT 1"))
# conn.close() is never called!

Performance Issues

Slow Queries

Diagnostic steps:

  1. Check query execution time in your application code:
import time

start = time.perf_counter()
cur.execute("SELECT * FROM large_table WHERE status = ?", ("active",))
rows = cur.fetchall()
elapsed = time.perf_counter() - start
print(f"Query took {elapsed:.3f}s, returned {len(rows)} rows")
  1. Add indexes for frequently queried columns:
CREATE INDEX idx_status ON large_table (status);
  1. Use LIMIT to restrict result set size:
cur.execute("SELECT * FROM large_table LIMIT 100")

High Memory Usage

Symptom: Python process consumes excessive memory with large result sets.

Cause: fetchall() loads all rows into memory at once.

Fix: Use fetchone() or fetchmany() for large result sets:

# WRONG — loads all 1 million rows into memory
cur.execute("SELECT * FROM large_table")
rows = cur.fetchall()  # 1M rows in memory!

# CORRECT — process one row at a time
cur.execute("SELECT * FROM large_table")
for row in cur:  # Iterator protocol — fetches in batches
    process(row)

# ALSO CORRECT — fetch in chunks
cur.execute("SELECT * FROM large_table")
while True:
    batch = cur.fetchmany(1000)
    if not batch:
        break
    for row in batch:
        process(row)

Connection Overhead

Symptom: Opening connections is slow.

Cause: Each pycubrid.connect() performs a TCP handshake + CAS broker handshake + database open (3+ round trips).

Fix for applications: Use SQLAlchemy connection pooling:

from sqlalchemy import create_engine

# Connection pool reuses existing connections
engine = create_engine(
    "cubrid+pycubrid://dba@localhost:33000/testdb",
    pool_size=5,
    pool_pre_ping=True,
)

Fix for scripts: Reuse a single connection instead of opening/closing repeatedly.


Debugging Techniques

Enable Verbose Logging

pycubrid includes opt-in DEBUG logging in pycubrid.connection, pycubrid.cursor, pycubrid.lob, and the async modules. Enable it through Python's logging configuration:

import logging
import pycubrid

logging.basicConfig(level=logging.DEBUG)

conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
try:
    print(f"Server version: {conn.get_server_version()}")
    print(f"Autocommit: {conn.autocommit}")

    cur = conn.cursor()
    try:
        cur.execute("SELECT COUNT(*) FROM db_class")
        print(f"Description: {cur.description}")
        print(f"Row count: {cur.rowcount}")
    finally:
        cur.close()
finally:
    conn.close()

The driver's debug logs intentionally avoid printing bound parameter values.

Inspect Server Version

conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
version = conn.get_server_version()
print(f"CUBRID version: {version}")  # e.g., "11.2.0.0378"
conn.close()

Test Connection Script

Save this as test_connection.py for quick verification:

#!/usr/bin/env python3
"""Quick pycubrid connection test."""
import sys
import pycubrid

try:
    conn = pycubrid.connect(
        host="localhost",
        port=33000,
        database="testdb",
        user="dba",
    )
    print(f"✅ Connected to CUBRID {conn.get_server_version()}")

    cur = conn.cursor()
    cur.execute("SELECT 1 + 1")
    result = cur.fetchone()
    print(f"✅ Query result: {result[0]}")

    cur.execute("SELECT COUNT(*) FROM db_class")
    count = cur.fetchone()[0]
    print(f"✅ System tables: {count}")

    cur.close()
    conn.close()
    print("✅ All checks passed")

except pycubrid.OperationalError as e:
    print(f"❌ Connection failed: {e}")
    sys.exit(1)
except pycubrid.ProgrammingError as e:
    print(f"❌ Query failed: {e}")
    sys.exit(1)

SQLAlchemy Debug Logging

import logging

logging.basicConfig()
logging.getLogger("sqlalchemy.engine").setLevel(logging.DEBUG)

engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb", echo=True)

This shows all SQL statements, parameters, and execution times.


See also: Connection Guide · API Reference · Examples · Development