Feature Support Comparison¶
A comprehensive comparison of sqlalchemy-cubrid capabilities against the mature SQLAlchemy dialects for MySQL, PostgreSQL, and SQLite.
Use this document to understand what the CUBRID dialect supports, what it doesn't, and how it compares to other database backends when choosing a dialect for your project.
Table of Contents¶
- Legend
- Summary
- DML — Data Manipulation Language
- DDL — Data Definition Language
- Query Features
- Type System
- Schema Reflection
- Transactions & Connections
- Dialect Engine Features
- CUBRID-Specific Features
- CUBRID-Specific DML Constructs
- Index Hints
- Known Limitations & Roadmap
Legend¶
| Symbol | Meaning |
|---|---|
| ✅ | Fully supported |
| ⚠️ | Partial or emulated support |
| ❌ | Not supported |
Summary¶
High-level overview by feature category.
| Category | CUBRID | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
| DML | ✅ | ✅ | ✅ | ⚠️ |
| DDL | ✅ | ✅ | ✅ | ⚠️ |
| Query features | ✅ | ✅ | ✅ | ✅ |
| Type system | ⚠️ | ✅ | ✅ | ⚠️ |
| Schema reflection | ✅ | ✅ | ✅ | ⚠️ |
| Transactions | ✅ | ✅ | ✅ | ⚠️ |
| Engine features | ⚠️ | ✅ | ✅ | ⚠️ |
DML — Data Manipulation Language¶
| Feature | CUBRID | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
| INSERT … RETURNING | ❌ | ❌ | ✅ | ✅ |
| UPDATE … RETURNING | ❌ | ❌ | ✅ | ✅ |
| DELETE … RETURNING | ❌ | ❌ | ✅ | ✅ |
| INSERT … DEFAULT VALUES | ✅ | ❌ | ✅ | ✅ |
| Empty INSERT | ✅ | ⚠️ | ✅ | ✅ |
| Multi-row INSERT | ✅ | ✅ | ✅ | ✅ |
| INSERT FROM SELECT | ✅ | ✅ | ✅ | ✅ |
| ON DUPLICATE KEY UPDATE | ✅ | ✅ | ❌ | ❌ |
| MERGE statement | ✅ | ❌ | ❌ | ❌ |
| REPLACE INTO | ✅ | ✅ | ❌ | ✅ |
| FOR UPDATE (row locking) | ✅ | ✅ | ✅ | ❌ |
| UPDATE with LIMIT | ✅ | ✅ | ❌ | ❌ |
| TRUNCATE TABLE | ✅ | ✅ | ✅ | ❌ |
| IS DISTINCT FROM | ✅ | ❌ | ✅ | ❌ |
| Postfetch LASTROWID | ✅ | ✅ | ❌ | ✅ |
Notes¶
- RETURNING: CUBRID has no
RETURNINGclause. Auto-generated keys cannot be fetched in the same round-trip as the INSERT, so the dialect relies onpostfetch_lastrowid = Trueinstead (get_last_insert_id()/ SQL fallback for the C driver;cursor.lastrowidfor pycubrid). - DEFAULT VALUES: CUBRID supports
INSERT INTO t DEFAULT VALUES. The dialect setssupports_default_values = True. - ON DUPLICATE KEY UPDATE: CUBRID supports
INSERT … ON DUPLICATE KEY UPDATE. The dialect handlesstmt.insertedreferences by re-emitting INSERT bind parameters (CUBRID does not support theVALUES()function). Usesqlalchemy_cubrid.insert(table).on_duplicate_key_update(col=value). See CUBRID-Specific DML Constructs for usage examples. - MERGE: CUBRID supports the full SQL MERGE statement. Use
sqlalchemy_cubrid.dml.merge(target)with.using(),.on(),.when_matched_then_update(), and.when_not_matched_then_insert(). See CUBRID-Specific DML Constructs. - FOR UPDATE: CUBRID supports
SELECT … FOR UPDATE [OF col1, col2]. NOWAIT and SKIP LOCKED are not supported. - UPDATE with LIMIT: CUBRID and MySQL both support
UPDATE … LIMIT n. PostgreSQL and SQLite do not. - Multi-table UPDATE: SQLAlchemy's multi-table UPDATE pattern compiles to
UPDATE t1, t2 SET ... WHERE ..., which matches the MySQL-style syntax accepted by CUBRID. The dialect intentionally keepsupdate_from_clause()disabled because no extraFROMclause is required. - TRUNCATE: CUBRID supports
TRUNCATE TABLE. Like other statements, it runs inside the connection's transaction and takes effect only when committed (see No Statement-Text Autocommit). - IS DISTINCT FROM: CUBRID lacks the SQL-standard syntax but supports the null-safe equal
<=>. The dialect emulatesa IS DISTINCT FROM bas(a <=> b) = 0anda IS NOT DISTINCT FROM basa <=> b, preserving NULL-safe semantics in predicates and SELECT projections (#344, #377).IS/IS NOTagainst a value, e.g. Booleancol.is_(True), is rendered the same way (col <=> 1,(col <=> 1) = 0), because CUBRID'sISaccepts onlyNULL,TRUEandFALSE(#465); see Boolean predicates.
DDL — Data Definition Language¶
| Feature | CUBRID | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
| ALTER TABLE | ✅ | ✅ | ✅ | ⚠️ |
| Table comments | ✅ | ✅ | ✅ | ❌ |
| Column comments | ✅ | ✅ | ✅ | ❌ |
| CREATE … IF NOT EXISTS | ✅ | ✅ | ✅ | ✅ |
| DROP … IF EXISTS | ✅ | ✅ | ✅ | ✅ |
| Temporary tables | ❌ | ✅ | ✅ | ✅ |
| Multiple schemas | ❌ | ✅ | ✅ | ⚠️ |
Notes¶
- ALTER TABLE: CUBRID supports standard
ALTER TABLEfor adding/dropping columns and constraints. SQLite has limited ALTER support (add column only; no drop/rename column before 3.35). - Comments: CUBRID supports inline
COMMENTsyntax for both tables (e.g.,CREATE TABLE t (...) COMMENT = 'text') and columns (e.g.,col TYPE COMMENT 'text'). The dialect implementsSetTableComment,DropTableComment, andSetColumnCommentDDL constructs. Comment reflection is supported viaget_table_comment()and column comments inget_columns(). - IF NOT EXISTS / IF EXISTS: CUBRID supports
CREATE TABLE IF NOT EXISTSandDROP TABLE IF EXISTS. The base SA compiler handles these natively. CUBRID has noCREATE INDEX IF NOT EXISTSorDROP INDEX IF EXISTS, soCreateIndex(..., if_not_exists=True)andDropIndex(..., if_exists=True)raiseCompileError(#540, #533); usecheckfirst=Trueorinspect(conn).has_index()instead. - Temporary tables: CUBRID does not support
CREATE TEMPORARY TABLEor session-scoped tables. - Multiple schemas: CUBRID operates in a single-schema model. MySQL uses databases as schemas. SQLite can attach databases but does not have true schema support.
Query Features¶
| Feature | CUBRID | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
| Common Table Expressions (WITH) | ✅ | ✅ | ✅ | ✅ |
| Recursive CTEs (WITH RECURSIVE) | ✅ | ✅ | ✅ | ✅ |
| CTEs on DML | ❌ | ❌ | ✅ | ❌ |
| Window functions | ✅ | ✅ | ✅ | ✅ |
| NULLS FIRST / NULLS LAST | ✅ | ❌ | ✅ | ✅ |
| GROUP_CONCAT | ✅ | ✅ | ❌ | ✅ |
| INTERSECT | ✅ | ✅ | ✅ | ✅ |
| EXCEPT | ✅ | ✅ | ✅ | ✅ |
| DISTINCT | ✅ | ✅ | ✅ | ✅ |
| LIMIT / OFFSET | ✅ | ✅ | ✅ | ✅ |
| Lateral joins | ❌ | ❌ | ✅ | ❌ |
| Full-text search (MATCH … AGAINST) | ❌ | ✅ | ✅ | ✅ |
| Query trace / EXPLAIN | ⚠️ | ✅ | ✅ | ✅ |
| ### Notes |
- CTEs: CUBRID supports
WITHclauses for read queries;TestIsolatedCTE849intest/test_integration.pychecks them live on CUBRID 10.2, 11.0, 11.2 and 11.4 (#849): non-recursive and recursiveSELECT, bound parameters inside the CTE,values()via a CTE, andUPDATE/DELETE ... WHERE col = (SELECT ... FROM cte)return the expected rows. A data-modifying statement inside a CTE body (a PostgreSQL-style writable CTE,WITH x AS (INSERT … RETURNING …)) is not supported; anUPDATEorDELETEcan still read a CTE through a subquery, as above.UPDATE ... FROM cteandDELETE ... FROM cteraise, because CUBRID has no multi-tableUPDATE/DELETE.insert().from_select(..., select(cte))andreplace().from_select(..., select(cte)), including a recursive CTE, render theWITHclause after the column list (INSERT INTO t (...) WITH ... SELECT), the only order CUBRID accepts;WITH ... INSERTis a syntax error (-493) on CUBRID 10.2 and 11.4 (#897). SQLAlchemy's ownCTETestnodes stay baselined intest/known_failures.txt, because their fixture cleanup fails on CUBRID's per-row foreign-key check, not because of these queries. - Recursive CTEs: CUBRID supports
WITH RECURSIVEfor recursive queries, verified live on 10.2, 11.0, 11.2 and 11.4 (#849). SQLAlchemy's base compiler generates correct syntax — no dialect-specific compilation needed. - Window functions: CUBRID supports
ROW_NUMBER(),RANK(),DENSE_RANK(), and other window functions withOVER(PARTITION BY … ORDER BY …). The SA base compiler handles these natively. - NULLS FIRST / NULLS LAST: CUBRID supports
ORDER BY col ASC NULLS FIRSTandORDER BY col DESC NULLS LAST. The SA base compiler handles these natively. - GROUP_CONCAT: CUBRID supports
GROUP_CONCAT([DISTINCT] expr [ORDER BY …] [SEPARATOR '…']). Usesa.func.group_concat(column),sa.func.group_concat(column.distinct()), or aliteral_column()argument forORDER BY/SEPARATOR; see GROUP_CONCAT. - LIMIT / OFFSET: CUBRID uses MySQL-style
LIMIT [offset,] countsyntax. When only an offset is given, the dialect emitsLIMIT offset, 4611686018427387904(2^62) as the "all remaining rows" sentinel — effectively unbounded, and small enough that CUBRID's internaloffset + countaddition never overflows the signed BIGINT range. CUBRID has noFETCH FIRST n ROWS ONLY, soSelect.fetch(n)also renders asLIMIT n(LIMIT offset, nwith.offset(), #867);fetch(..., percent=True)andfetch(..., with_ties=True)have noLIMITequivalent and raiseCompileError. - Boolean SELECT-list items: CUBRID rejects a bare
EXISTS (...),NOT (...)ora AND b/a OR bas a SELECT-list item (SELECT EXISTS (SELECT 1)is a syntax error) but accepts it inside parentheses. The dialect groups such a column, soselect(exists().where(...)),select(~exists()...),select(and_(...))and the ORMsession.query(q.exists()).scalar()work (#966). CUBRID also rejects these expressions as a bare CAST or function argument and as an ORDER BY item, so the dialect groups them there too (also insidetype_coerce(), labels and sort modifiers).select(type_coerce(exists(), Boolean)),select(cast(and_(...), Integer)),select(func.coalesce(exists(), False)), a BooleanTypeDecoratorwhosecolumn_expression()wraps the column,order_by(and_(...))andorder_by(exists())work (#968). A CAST or function groups such an argument wherever it appears, not only in the SELECT list, for example inWHERE,HAVING,ONorUPDATE ... SET, sowhere(func.coalesce(and_(...), 0) == 1)andwhere(cast(and_(...), Integer) == 1)work. AnEXISTS (...)orNOT (...)item of a window orWITHIN GROUPORDER BYorPARTITION BYis grouped too, soover(order_by=exists()),over(partition_by=exists())andwithin_group(~exists())work. A singleand_()oror_()passed as a windoworder_byorpartition_byis split by SQLAlchemy itself into separate items, on every dialect (foror_()that changes the meaning), so pass it inside a list,over(order_by=[and_(...)]), or call.self_group(); inside a list and inWITHIN GROUPit is grouped (#970). ACASETHENorELSEvalue is grouped too, in a searched and a simple (value=)case(), socase((x > 0, and_(...)), else_=literal(False))andcase((x > 0, exists()), else_=literal(False))work (#982). A boolean simple-case value operand is grouped too, socase({True: 10}, value=and_(...), else_=20)andcase({True: 10}, value=exists(), else_=20)work (#987). With CUBRIDdb, aCASEwhose results are all bound values returns a string; see Driver Compatibility. CUBRID can reject aCASEwhose results are boolean and that has noELSEor onlyELSE NULL, for example one with a singleWHEN("semantic error", -495), with or without the grouping; this is a server limitation, so pass a non-NULLelse_=, for exampleelse_=literal(False). CUBRID 11.4 also ignores anyCASEused as a windowORDER BYorPARTITION BYitem: it is evaluated as a constant, without an error, so the window is numbered as if the item were absent. Do not use aCASEthere. ACASEwith a boolean value as aGROUP BYitem fails ("search condition is not allowed as group by spec", -493); group by its label instead. The same expressions as a bareWHERE,HAVINGorONcondition, aGROUP BYitem, aCASEWHENcondition or any other operand render as before. Not supported: a boolean expression as aGROUP BYitem. CUBRID rejects any search condition there, even inside parentheses ("search condition is not allowed as group by spec", -493), sogroup_by(a | b)andgroup_by(exists())fail. Label the expression in the SELECT list and group by the label instead, for exampleselect(and_(...).label("k"), func.count()).group_by("k"). The same applies to a CAST or function inGROUP BY:group_by(func.coalesce(exists(), 0))fails, with or without the grouping. Verified on CUBRID 11.4; not verified on 10.2, 11.0 or 11.2. - Join variants: INNER JOIN and LEFT OUTER JOIN compile normally. FULL OUTER JOIN and
LATERALare rejected during compilation because CUBRID does not support them. - Lateral joins: CUBRID does not support
LATERALsubqueries. TheLATERALkeyword causes a syntax error. - Full-text search: CUBRID does not support
MATCH … AGAINSTsyntax or full-text indexes. - Query trace: CUBRID uses
SET TRACE ON/SHOW TRACEinstead of standardEXPLAIN. The dialect providestrace_query()as a utility function — see CUBRID-Specific DML Constructs.
Type System¶
Standard SQL Types¶
| Type | CUBRID | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
| SMALLINT | ✅ | ✅ | ✅ | ✅ |
| INTEGER | ✅ | ✅ | ✅ | ✅ |
| BIGINT | ✅ | ✅ | ✅ | ✅ |
| NUMERIC / DECIMAL | ✅ | ✅ | ✅ | ✅ |
| FLOAT | ✅ | ✅ | ✅ | ✅ |
| DOUBLE / REAL | ✅ | ✅ | ✅ | ✅ |
| BOOLEAN | ⚠️ | ⚠️ | ✅ | ⚠️ |
| DATE | ✅ | ✅ | ✅ | ✅ |
| TIME | ✅ | ✅ | ✅ | ✅ |
| DATETIME | ✅ | ✅ | ✅ | ✅ |
| TIMESTAMP | ✅ | ✅ | ✅ | ✅ |
| CHAR | ✅ | ✅ | ✅ | ✅ |
| VARCHAR | ✅ | ✅ | ✅ | ✅ |
| NCHAR / NVARCHAR | ✅ | ⚠️ | ❌ | ❌ |
| TEXT | ✅ | ✅ | ✅ | ✅ |
| BLOB | ⚠️ | ✅ | ✅ | ✅ |
| CLOB | ⚠️ | ✅ | ✅ | ✅ |
| BIT / BIT VARYING | ✅ | ✅ | ✅ | ❌ |
Extended Types¶
| Type | CUBRID | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
| ENUM | ✅ | ✅ | ✅ | ❌ |
| JSON | ✅ | ✅ | ✅ | ⚠️ |
| ARRAY | ❌ | ❌ | ✅ | ❌ |
| UUID | ❌ | ❌ | ✅ | ❌ |
| INTERVAL | ❌ | ❌ | ✅ | ❌ |
| HSTORE | ❌ | ❌ | ✅ | ❌ |
Notes¶
- BOOLEAN: CUBRID maps
BOOLEANtoSMALLINT. MySQL maps it toTINYINT(1). SQLite stores booleans as integers. Only PostgreSQL has a nativeBOOLEANtype. - NCHAR / NVARCHAR: CUBRID has first-class national character types. MySQL handles national characters via column charset. PostgreSQL and SQLite have no separate national character types.
- JSON: CUBRID 10.2+ has native JSON support (RFC 7159) with 25+ JSON functions. The dialect supports
JSONtype,col["key"]path expressions viaJSON_EXTRACT, and typed access (as_string(),as_integer(),as_float(), andas_numeric(p, s), which casts toNUMERIC(p,s)and returnsDecimal).as_numeric()accepts JSON numbers, plain-decimal numeric strings ("15.5") and JSONtrue/false(1.00/0.00). A string in exponent notation ("1e3") raises CUBRID error -181, and a value outsideNUMERIC(p,s)raises -427; useas_float()for those. A non-numeric string ("abc") raises -181 with bothas_numeric()andas_float(), so validate or filter such values first. MySQL (5.7+) and PostgreSQL also have native JSON support. SQLite has JSON functions but no dedicated column type. - ARRAY: CUBRID uses collection types (
SET,MULTISET,SEQUENCE) which serve a similar purpose but are not SQL-standard arrays. PostgreSQL has nativeARRAY[]support. - CLOB: CUBRID and MySQL have explicit
CLOBtypes. PostgreSQL usesTEXT(unlimited length). SQLite stores all text asTEXT. - BLOB / CLOB on CUBRID (⚠️): The types are implemented:
BLOB,CLOBandLargeBinarycompile to CUBRIDBLOB/CLOBDDL and reflect back, bindingbytes/strstores the full value, andNULLround-trips asNone. What is partial is the value returned by an ordinary Core or ORMSELECT: a non-NULLBLOB/CLOBcolumn comes back as the driver's LOB locator, not asbytes/str, oncubrid://,cubrid+pycubrid://andcubrid+aiopycubrid://(and forLargeBinary/BLOBSQLAlchemy's result processor then raisesTypeError). This is a driver fetch limitation, not a CUBRID storage limit;test/test_lob_value_contract.pykeeps those reads as strict xfails. To read content, convert on the server (CLOB_TO_CHAR(col),BLOB_TO_BIT(col)) or store large text inText(CUBRIDSTRING). See Driver Compatibility, Known Issue 6 and Types.
Schema Reflection¶
| Feature | CUBRID | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
| Table names | ✅ | ✅ | ✅ | ✅ |
| Column information | ✅ | ✅ | ✅ | ✅ |
| Primary keys | ✅ | ✅ | ✅ | ✅ |
| Foreign keys | ✅ | ✅ | ✅ | ✅ |
| Indexes | ✅ | ✅ | ✅ | ✅ |
| Unique constraints | ✅ | ✅ | ✅ | ✅ |
| Check constraints | ❌ | ✅ | ✅ | ❌ |
| Table comments | ✅ | ✅ | ✅ | ❌ |
| Column comments | ✅ | ✅ | ✅ | ❌ |
| View names | ✅ | ✅ | ✅ | ✅ |
| View definitions | ✅ | ✅ | ✅ | ✅ |
| Schema names | ❌ | ✅ | ✅ | ❌ |
| Sequences | ❌ | ❌ | ✅ | ❌ |
has_table |
✅ | ✅ | ✅ | ✅ |
has_index |
✅ | ❌ | ✅ | ✅ |
has_sequence |
❌ | ❌ | ✅ | ❌ |
Notes¶
- Check constraints: CUBRID parses CHECK constraint syntax but does not enforce it at runtime. To avoid reflecting misleading metadata, the dialect intentionally returns an empty list from
get_check_constraints(). - Table comments: Reflected via
get_table_comment()querying thedb_class.commentsystem catalog column. - Column comments: Reflected via
get_columns()querying thedb_attribute.commentcatalog view column. Returned in the"comment"key of each column dict. - has_index: The CUBRID dialect implements
has_index()by queryingdb_index. The MySQL SA dialect does not provide a dedicatedhas_index()method. CUBRID stores identifiers in lower case, even quoted ones, sohas_table()andhas_index()also match a mixed-case name against its lower-case stored form (#543);Index.drop(checkfirst=True)therefore works forIndex("IX_Mixed", Table("Users", ...)). - Index shapes:
get_indexes()reports a function-based key (CREATE INDEX ... (LOWER(a))) inexpressionswithNoneincolumn_names, aDESCkey incolumn_sorting, a filtered index (... WHERE c > 3) asdialect_options["cubrid_where"]and a key prefix (a(5)) asdialect_options["cubrid_length"](#869), from theSHOW INDEXESFunc,CollationandSub_partcolumns anddb_index.filter_expression. CUBRID prints the expression and the filter with the column qualified by its table (lower([dba.t].[a]),[dba.t].c>3); the qualifier is dropped, so they reflect aslower([a])andc>3, and a reflected table re-creates the same indexes. The same options create them:Index("ix", t.c.c, cubrid_where=t.c.c > 3)emitsCREATE INDEX ix ON t (c) WHERE c > 3, andIndex("ix", t.c.a, cubrid_length={"a": 5})(orcubrid_length=5for every column key) emitsCREATE INDEX ix ON t (a(5)). CUBRID rejects aUNIQUEfiltered index, sounique=Truewithcubrid_whereraisesCompileError, as does a prefix length on an expression key. Alembic autogenerate compares these indexes by their normalized keys, sort order, filter and prefix lengths (string literals are compared as written), so an unchanged one is not proposed again on any supported Alembic; replacing one whose sort order, filter or prefix length changed needs Alembic 1.12 or later, the first release that calls the dialect's index comparison. - Unique constraints and unique indexes: CUBRID implements a
UNIQUEconstraint as a unique index and cannot tell it apart fromCREATE UNIQUE INDEX, so, as with MySQL, each one is reported by bothget_indexes()andget_unique_constraints(), and the unique-constraint entry carriesduplicates_indexnaming the index.Tablereflection (and Alembic autogenerate) uses that key to keep only the unique index.get_indexes()returns an empty list for a view. - Missing tables and views:
get_columns(),get_pk_constraint(),get_foreign_keys(),get_indexes(),get_unique_constraints(),get_table_comment()andget_view_definition()raiseNoSuchTableErrorfor an object that does not exist, so theget_multi_*()variants leave it out;get_view_definition()also raises it for a table. For a view,get_foreign_keys()andget_unique_constraints()return an empty list without runningSHOW CREATE TABLE. - Reflection source: Reflection is split across multiple sources:
SHOW COLUMNS IN+db_attributefor columns/comments,db_index+db_index_keyfor the primary key (name and every column in key order),SHOW CREATE TABLEparsing for foreign keys,db_index+SHOW INDEXES INfor unique constraints (withSHOW CREATE TABLEparsing as the fallback only whendb_indexlists no index of the table at all or, since CUBRID 11.2, when the name resolves to another owner's class that shows no unique index (if it shows one,SHOW INDEXES INraisesNoSuchTableErrorfirst; the fallback raises it too); any other table with a primary key, a foreign key or another index but no unique one is answered from the catalog, #610),SHOW INDEXES IN+db_indexfor indexes,SHOW CREATE VIEWfor view definitions, anddb_classfor table/view names and table comments. - Foreign-key DDL grammar:
get_foreign_keys()relies on theSHOW CREATE TABLEoutput of the supported servers (10.2, 11.0, 11.2 and 11.4), recorded intest/fixtures/show_create_table/and checked against the live server bytest/test_show_create_table_server_output.py(#611): every identifier is printed in brackets with spaces, commas and parentheses kept verbatim; each foreign key prints both actions,ON DELETEbeforeON UPDATE, with the default printed asRESTRICT; the actions areCASCADE,SET NULL,NO ACTIONandRESTRICT(the servers rejectON UPDATE CASCADEand have noSET DEFAULT); the referenced table is owner-qualified ([dba.parent]) from 11.2 and unqualified on 10.2 and 11.0, and the owner is dropped fromreferred_table. AUNIQUEconstraint and aCREATE UNIQUE INDEXindex both print asUNIQUE KEY. - Table and view listings:
get_table_names(),get_view_names()andhas_table()readdb_class. Since CUBRID 11.2 that view lists the classes of every owner the user may read, while an unqualified name resolves only in the current user's schema, so these methods are limited toowner_name = CURRENT_USER(#864): another owner's table of the same name no longer makescreate_all()skip the current user's table,MetaData.reflect()no longer aborts withNoSuchTableErroron it, and two owners' same-named tables are listed once. Aschema=argument other than the default schema still returns no names, as for every reflection method. CUBRID 10.2 and 11.0 class names are global, so their listings are unchanged. - Reflected server defaults:
get_columns()reports each default as SQL thatCreateTableand Alembic can emit again (#865). A numeric column's numeric default is kept asSHOW COLUMNSprints it (5,-3,1.50). Any other default, including a string column's'5'(whichSHOW COLUMNSalso prints as5), is read fromSHOW CREATE TABLE, whichget_columns()then runs once for the table:SHOW COLUMNSprints a string default without its quotes, so'it''s',''and the string'NULL'used to reflect asit''s, an empty string andNULL, a date as01/02/2020, and both'CURRENT_USER'andUSERasCURRENT_USER. They now reflect as'it''s','','NULL',date '01/02/2020','CURRENT_USER'andCURRENT_USER; functions such asSYS_DATETIMEorTO_CHAR(SYS_DATETIME, 'YYYY'), charset-prefixed literals (_utf8'q',N'n') and bit strings (X'a0') keep the server's spelling. A column without a default still reflectsNone. Views do not runSHOW CREATE TABLE; if its output cannot be parsed, theSHOW COLUMNStext is kept. - Reflection as a non-DBA user: Reflection reads only the catalog views every user can read (
db_class,db_index,db_index_key,db_attribute), never the DBA-only_db_index,_db_index_keyor_db_attributecatalog tables, so a non-DBA user gets the same indexes, primary key, unique constraints,has_index()answer and column comments as DBA. The table name matches as given or folded to lower case, as inSHOW COLUMNS IN <name>. Since CUBRID 11.2 these views also list same-named classes of other owners, so the rows are limited to the owner of the reflected class (the current user's own class first, as fordb_class). A failing catalog query raises instead of silently falling back to incomplete metadata. SHOW CREATE TABLEfailures:get_foreign_keys()(and theSHOW CREATE TABLEfallback ofget_unique_constraints()) returns an empty list only whenSHOW CREATE TABLEsucceeds and the DDL has no such constraint.Unknown class, or no row for a table that the catalog listed, raisesNoSuchTableError; any other failure (a disconnect, an authorization error, a driver error) propagates instead of being logged and reported as "no constraints", which made Alembic autogenerate emitadd_fkfor foreign keys that already exist (#589). CUBRID lists indb_classonly the tables a user holdsSELECTon, andSHOW CREATE TABLEon such a table does not fail with an authorization error (checked as a non-DBA user on CUBRID 10.2 and 11.4), so no failure is tolerated for non-DBA users either. Since CUBRID 11.2 an unqualified name resolves in the current user's schema, so another owner's table, even one granted to the current user, raisesNoSuchTableErrorfromget_columns()as well as here;get_unique_constraints()therefore keeps readingSHOW CREATE TABLEfor such a table when the catalog lists its indexes but no unique one; when the catalog lists a unique index of it,SHOW INDEXES INraisesNoSuchTableErrorbefore the DDL is read. For a table of the current user (any visible table before 11.2) whose catalog lists indexes but no unique one,get_unique_constraints()returns[]without runningSHOW CREATE TABLE(#610). When the catalog lists a unique index thatSHOW INDEXESdoes not return key columns for,get_unique_constraints()raisesInvalidRequestErrornaming the unresolved and the resolved indexes, instead of reporting an incomplete set that Alembic would read as missing constraints (#848).
Transactions & Connections¶
| Feature | CUBRID | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
| Isolation level management | ✅ | ✅ | ✅ | ✅ |
| Savepoints | ✅ | ✅ | ✅ | ✅ |
| Two-phase commit | ❌ | ✅ | ✅ | ❌ |
| Server-side cursors | ❌ | ✅ | ✅ | ❌ |
| Autocommit detection from SQL text | ❌ | ❌ | ❌ | ❌ |
| Connection-level encoding | ❌ | ✅ | ✅ | ❌ |
CUBRID Isolation Levels¶
CUBRID's MVCC engine (10.0+) supports three isolation levels:
| Level | Description |
|---|---|
SERIALIZABLE (6) |
Full serialization |
REPEATABLE READ (5) |
Repeatable read within a transaction |
READ COMMITTED (4, default) |
Reads see only committed data; non-repeatable reads possible |
SQLAlchemy's AUTOCOMMIT level is also accepted on every driver; it switches the driver to autocommit mode. See Isolation Levels.
Notes¶
- Two-phase commit: CUBRID does not support distributed transactions via
XA. - Server-side cursors: The CUBRID Python driver does not expose server-side cursor functionality.
- Autocommit detection from SQL text: SQLAlchemy 2.x does not inspect statement text to decide when to commit, for any dialect. DML and DDL are committed only through
conn.commit(), anengine.begin()block, or aSessioncommit. See No Statement-Text Autocommit. - Savepoints: CUBRID supports
SAVEPOINTandROLLBACK TO SAVEPOINT.RELEASE SAVEPOINTis not supported — the dialect implementsdo_release_savepoint()as a no-op.
Dialect Engine Features¶
| Feature | CUBRID | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
| Statement caching | ✅ | ✅ | ✅ | ✅ |
| Native enum | ✅ | ✅ | ✅ | ❌ |
| Native boolean | ❌ | ❌ | ✅ | ❌ |
| Native decimal | ✅ | ✅ | ✅ | ❌ |
| Sequences | ❌ | ❌ | ✅ | ❌ |
| ON UPDATE CASCADE | ✅ | ✅ | ✅ | ✅ |
| ON DELETE CASCADE | ✅ | ✅ | ✅ | ✅ |
| Self-referential FKs | ✅ | ✅ | ✅ | ✅ |
| Independent connections | ✅ | ✅ | ✅ | ✅ |
| Unicode DDL | ✅ | ✅ | ✅ | ✅ |
| Name normalization | ✅ | ❌ | ✅ | ❌ |
Notes¶
- Statement caching:
supports_statement_cache = True. The dialect is fully compatible with SQLAlchemy 2.0's compiled cache. - Name normalization: CUBRID folds unquoted identifiers to lowercase, which already matches SQLAlchemy's lowercase convention for case-insensitive names. The dialect therefore sets
requires_name_normalize = Falseand does not apply SQLAlchemy's upper-case name normalization. - Max identifier length: CUBRID allows identifiers up to 254 characters — significantly longer than MySQL (64) or PostgreSQL (63).
- Result metadata (
cursor.description): the dialect passes the driver'scursor.descriptionthrough unchanged (CursorResult.cursor.description) and normalizes none of it. Verified live on CUBRID 10.2 and 11.4 for sync and async pycubrid and CUBRIDdb (#482):Result.keys()equals the description names, includingtext()aliases and expressions (1 + 1is named1+1); scalar type codes are the CUBRID codes on every driver (for exampleVARCHAR2,NUMERIC7,INTEGER8,DOUBLE12,DATE13,TIMESTAMP15,BIGINT21); sync and async pycubrid report the same values. Driver differences: collection columns areSET16 /MULTISET17 /SEQUENCE18 on pycubrid but CCI composite codes on CUBRIDdb (the kind in bits0x60plus the element type, soSET(INTEGER)is 40);null_okis aboolon pycubrid and0/1on CUBRIDdb. Released pycubrid 1.7.1 reportsnull_okinverted (cubrid-lab/pycubrid#431) and collection columns with their element type code (cubrid-lab/pycubrid#430); both are fixed in pycubrid 1.8.0, which the contract tests require. SQLAlchemy itself does not consumenull_ok, and reflection (Inspector.get_columns()) reads nullability from the catalog, so it is unaffected.
CUBRID-Specific Features¶
These types and capabilities are unique to the CUBRID dialect and have no direct equivalent in MySQL, PostgreSQL, or SQLite.
| Feature | Description |
|---|---|
MONETARY type |
Currency-aware server type; declared/compiled but not auto-reflected |
STRING type |
Alias for VARCHAR(1,073,741,823) — maximum-length variable string |
OBJECT type |
OID reference type pointing to another row by object identifier |
SET collection |
Unordered collection of unique elements |
MULTISET collection |
Unordered collection that allows duplicates |
SEQUENCE collection |
Ordered collection that allows duplicates |
| 3 MVCC isolation levels | READ COMMITTED (default), REPEATABLE READ, SERIALIZABLE |
| 254-char identifiers | Longer than MySQL (64) and PostgreSQL (63) |
Collection Types¶
CUBRID's collection types (SET, MULTISET, SEQUENCE) are typed containers that hold elements of a specified data type. They are declared in DDL as:
These are fully supported by the dialect's type compiler and can be used in Column definitions.
CUBRID-Specific DML Constructs¶
The dialect provides custom SQLAlchemy constructs for CUBRID-specific DML features that go beyond the standard SA API.
ON DUPLICATE KEY UPDATE¶
CUBRID supports INSERT … ON DUPLICATE KEY UPDATE. The dialect handles stmt.inserted references by re-emitting INSERT bind parameters (CUBRID does not support the VALUES() function).
from sqlalchemy_cubrid import insert
stmt = insert(users).values(id=1, name="alice", email="alice@example.com")
stmt = stmt.on_duplicate_key_update(name="updated_alice")
# INSERT INTO users (id, name, email) VALUES (1, 'alice', 'alice@example.com')
# ON DUPLICATE KEY UPDATE name = 'updated_alice'
# Reference the inserted value:
stmt = insert(users).values(id=1, name="alice", email="alice@example.com")
stmt = stmt.on_duplicate_key_update(name=stmt.inserted.name)
# ON DUPLICATE KEY UPDATE name = ?
Accepted argument forms:
- Keyword arguments: stmt.on_duplicate_key_update(name="value")
- Dictionary: stmt.on_duplicate_key_update({"name": "value"})
- List of tuples (ordered): stmt.on_duplicate_key_update([("name", "value"), ("email", "value")])
MERGE Statement¶
CUBRID supports the SQL MERGE statement for conditional INSERT/UPDATE in a single operation.
from sqlalchemy_cubrid.dml import merge
stmt = (
merge(target_table)
.using(source_table)
.on(target_table.c.id == source_table.c.id)
.when_matched_then_update(
{"name": source_table.c.name, "email": source_table.c.email},
where=source_table.c.name.is_not(None), # optional WHERE
delete_where=target_table.c.active == False, # optional DELETE WHERE
)
.when_not_matched_then_insert(
{
"id": source_table.c.id,
"name": source_table.c.name,
"email": source_table.c.email,
},
where=source_table.c.name.is_not(None), # optional WHERE
)
)
Generated SQL:
MERGE INTO target_table
USING source_table
ON (target_table.id = source_table.id)
WHEN MATCHED THEN UPDATE SET name = source_table.name, email = source_table.email
WHERE source_table.name IS NOT NULL
DELETE WHERE target_table.active = 0
WHEN NOT MATCHED THEN INSERT (id, name, email)
VALUES (source_table.id, source_table.name, source_table.email)
WHERE source_table.name IS NOT NULL
Builder methods:
- merge(target) — factory function, sets target table
- .using(source) — source table or subquery
- .on(condition) — join condition
- .when_matched_then_update(values, where=None, delete_where=None) — UPDATE clause
- .when_matched_then_delete(where=None) — adds DELETE WHERE to an existing WHEN MATCHED clause
- .when_not_matched_then_insert(values, where=None) — INSERT clause
At least one of when_matched_then_update or when_not_matched_then_insert must be specified.
GROUP_CONCAT¶
CUBRID supports GROUP_CONCAT as an aggregate function:
import sqlalchemy as sa
stmt = sa.select(sa.func.group_concat(users.c.name))
# SELECT GROUP_CONCAT(users.name) FROM users
stmt = sa.select(sa.func.group_concat(users.c.name.distinct()))
# SELECT GROUP_CONCAT(DISTINCT users.name) FROM users
# SQLAlchemy 2.1: ORDER BY with aggregate_order_by()
stmt = sa.select(sa.func.group_concat(users.c.name).aggregate_order_by(users.c.name.desc()))
# SELECT GROUP_CONCAT(users.name ORDER BY users.name DESC) FROM users
# SEPARATOR (and ORDER BY on SQLAlchemy 2.0) has no SQLAlchemy construct; write it as text
stmt = sa.select(
sa.func.group_concat(sa.literal_column("name ORDER BY name DESC SEPARATOR ';'"))
).select_from(users)
# SELECT GROUP_CONCAT(name ORDER BY name DESC SEPARATOR ';') FROM users
stmt = sa.select(sa.func.group_concat(sa.and_(users.c.id > 1, users.c.name != "")))
# SELECT GROUP_CONCAT((users.id > ? AND users.name != ?)) FROM users
NULL values are skipped, and a group with only NULLs returns NULL. Without ORDER BY
the order of the concatenated values is unspecified. A boolean EXISTS, NOT, AND or
OR argument, or aggregate_order_by() item, is grouped like any function argument.
CUBRID takes one ORDER BY item, and it must be the GROUP_CONCAT argument itself or a
position such as literal_column("1"); another expression fails on the server (-494).
With CUBRIDdb, an argument and ORDER BY item that contain bound parameters also fail
(-494); see Driver Compatibility.
Verified on CUBRID 11.4; not verified on 10.2, 11.0 or 11.2. Not supported:
sa.func.group_concat(column, sa.literal_column("SEPARATOR ','"))rendersGROUP_CONCAT(users.name, SEPARATOR ','), which CUBRID rejects (-493). PutSEPARATORin the sameliteral_column()text as the expression.- More than one argument: CUBRID takes a single expression ("invalid use of group_concat").
- More than one
aggregate_order_by()item (-493), oraggregate_order_by()with aliteral_column()argument that containsSEPARATOR(theORDER BYwould follow it, -493). Write the wholeORDER BY ... SEPARATORtext instead. aggregate_order_by()on any function other thanGROUP_CONCAT, includingfunc.aggregate_strings(): no other CUBRID aggregate takes anORDER BY, so it raisesCompileErrorinstead of dropping theORDER BY.aggregate_order_by()on a.filter(...)ed function,GROUP_CONCATincluded: CUBRID has noFILTER (WHERE ...)clause, so it raisesCompileError.
REPLACE INTO¶
CUBRID supports REPLACE INTO which inserts a new row, or deletes the conflicting row and inserts the new one if a duplicate key is found.
from sqlalchemy_cubrid import replace
stmt = replace(users).values(id=1, name="alice", email="alice@example.com")
# REPLACE INTO users (id, name, email) VALUES (1, 'alice', 'alice@example.com')
The replace() construct behaves like insert() but generates REPLACE INTO instead of INSERT INTO.
Query Trace¶
CUBRID uses SET TRACE ON / SHOW TRACE instead of standard EXPLAIN. The dialect provides a trace_query() utility:
from sqlalchemy_cubrid import trace_query
with engine.connect() as conn:
traces = trace_query(conn, text("SELECT * FROM users WHERE id = 1"))
for line in traces:
print(line)
trace_query() handles the full lifecycle: enables tracing, executes your statement, collects trace output, and disables tracing — all within a safe try/finally block.
Index Hints¶
CUBRID supports index hints in SELECT queries. These can be used via SQLAlchemy's built-in hint mechanisms — no custom dialect constructs are needed.
USING INDEX¶
# Using Select.with_hint()
stmt = (
sa.select(users)
.with_hint(users, "USING INDEX idx_users_name", dialect_name="cubrid")
)
# Using Select.suffix_with()
stmt = sa.select(users).suffix_with("USING INDEX idx_users_name")
USE INDEX / FORCE INDEX / IGNORE INDEX¶
stmt = (
sa.select(users)
.with_hint(users, "USE INDEX (idx_users_name)", dialect_name="cubrid")
)
stmt = (
sa.select(users)
.with_hint(users, "FORCE INDEX (idx_users_email)", dialect_name="cubrid")
)
stmt = (
sa.select(users)
.with_hint(users, "IGNORE INDEX (idx_users_old)", dialect_name="cubrid")
)
Note: When using
with_hint(dialect_name="cubrid"), the hint is only emitted when compiling against the CUBRID dialect. Other dialects will ignore it, making your code safely portable.
Known Limitations & Roadmap¶
Features not currently supported that may be added in future releases, depending on CUBRID database evolution and community contributions.
| Feature | Status | Reason |
|---|---|---|
| RETURNING clause | ❌ | CUBRID does not support INSERT/UPDATE/DELETE … RETURNING |
| JSON type | ✅ | Native JSON support (CUBRID 10.2+) with path expressions via JSON_EXTRACT |
| Temporary tables | ❌ | CUBRID does not support CREATE TEMPORARY TABLE |
| Multiple schemas | ❌ | CUBRID operates in a single-schema model |
| Check constraint reflection | ❌ | CUBRID parses but ignores CHECK constraints |
| Sequences | ❌ | CUBRID uses AUTO_INCREMENT instead |
| Lateral joins | ❌ | LATERAL keyword causes syntax error in CUBRID |
| Full-text search | ❌ | No MATCH … AGAINST syntax or full-text indexes |
| Standard EXPLAIN | ❌ | CUBRID uses SET TRACE ON / SHOW TRACE instead (supported via trace_query()) |
| Alembic migrations | ✅ | Supported via CubridImpl, registered when the dialect loads (pip install sqlalchemy-cubrid[alembic]) |
Last updated: September 2026 · sqlalchemy-cubrid v1.8.0 · SQLAlchemy 2.0–2.1