Upgrade notes
Behavior changes you may notice (details in the entries below):
- Alembic upgrades are atomic by default (#503). CUBRID DDL is transactional, so a failed multi-revision
alembic upgradenow rolls back the whole run, including thealembic_versionupdate. Settransaction_per_migration=Trueincontext.configure()to keep per-revision commits (recommended for long or large-table migrations). Offline--sqlscripts no longer containBEGIN;; run them withcsql --no-auto-commit --no-single-lineso a failing statement stops the script. - Alembic registration (#504).
CubridImplis registered when the dialect loads, so a defaultenv.pyworks without importingsqlalchemy_cubrid.alembic_impl. The[alembic]and[dev]extras now requirealembic>=1.7.2;cubrid+aiopycubrid://online migrations need Alembic's async template (alembic init -t async). - Reflection. Unique constraints are no longer reported twice and reflect as unique indexes, as on MySQL (#529); non-DBA users get complete metadata from the public catalog views (#549); a missing table or view raises
NoSuchTableError(#530);has_table()/has_index()match mixed-case names (#543);get_foreign_keys()is sorted by name (#531);BIT(n)/BIT VARYING(n)keep their length (#545); a failing catalog query raises instead of silently returning incomplete metadata (#533, #549). Autogenerate also detectsText↔String(n)changes (#544). Review the first Alembic autogenerate diff after upgrading. - DDL and types.
UnicodeTextandTEXTcompile toSTRING(#534);BINARY(n)/VARBINARY(n)compile toBIT(n*8)/BIT VARYING(n*8)andUUIDtoCHAR(32)(#545);Index.drop()andop.drop_index()emitDROP INDEX <name> ON <table>, soop.drop_index()needstable_name(#533);if_exists=Trueon index drops andif_not_exists=Trueon index creates raiseCompileError(#533, #540), as does an explicitString(0)(#440); Booleanis_()/is_not()against a value render with<=>(#465); JSONas_numeric(p, s)returnsDecimal(#535). - Transactions.
isolation_level="AUTOCOMMIT"works on every driver, invalid levels raiseArgumentError, anddialect.on_connect()no longer appliesisolation_level(a hand-built pool must apply it) (#501). Engine- and connection-level isolation levels survivecommit()/rollback()on pycubrid (#505). Oncubrid://,executemanyruns each parameter set separately:Nonevalues and rowcounts are correct, large batches are slower (#502). - Async.
driver_connectiononcubrid+aiopycubrid://is now thepycubrid.aioconnection; usedbapi_connectionfor the SQLAlchemy adapter (#520). AsyncLargeBinary/BLOBbinding works (#500). - Removed the unreachable SQLAlchemy 1.x hooks
should_autocommit_text(),AUTOCOMMIT_REGEXPand thedbapi()classmethods (#462). - pycubrid 1.8.0 is recommended. It raises
IntegrityErrorfor NOT NULL and foreign-key violations, raises instead of silently returning a truncated result aftercommit()/rollback(), reports correctnull_okand collection type codes incursor.description, and keeps the CAS session across commits and rollbacks except when the CAS itself is restarted (docs/DRIVER_COMPAT.mdKnown Issue 10); the pycubrid contract tests require it (#480, #481, #482). The declared range stayspycubrid>=1.3.2,<2.0(raising it is tracked in #559), so the dialect's isolation-level re-apply workaround is kept for older pycubrid and is a harmless no-op on 1.8.0.
Added
- Upstream canary failures are reported as an issue (#521) —
.github/workflows/upstream-canary.ymlkeeps its canary jobs non-blocking (continue-on-error, schedule and manual dispatch only), so a failing run againstpycubrid@mainused to pass silently. A newreportjob runs after both canary jobs on scheduled and dispatched runs of the default branch (dispatches from feature branches never touch the shared issue, and cancelled runs are not reported): on failure it opens an issue titled "Upstream canary failing against pycubrid@main" with thecilabel, or comments on the open one instead of creating a duplicate, listing each job's outcome, the run URL, the pycubrid commit tested and the sqlalchemy-cubrid ref and commit; once both jobs pass again it closes that issue with a comment. Each canary job now resolvespycubrid@mainwithgit ls-remote, installs that exact commit and exposes it and its own status as job outputs, because job-levelcontinue-on-errorhides failures fromneeds.<job>.result. The workflow defaults tocontents: read; only the report job hasissues: write, and it uses theghCLI withGITHUB_TOKEN(no third-party action), runs one at a time (concurrency), and has nocontinue-on-error, so a failure to report turns the run red.docs/DEVELOPMENT.md(+ Korean) describes the reporting. - Blocking SQLAlchemy compliance lanes for released pycubrid (#463) — the official SQLAlchemy compliance suite now gates merges for
cubrid+pycubrid://as well as CUBRIDdb, as steps of the existingintegration-testscells (no new job or CUBRID service): lanepycubrid@sa2.1(pycubrid 1.7.1, SQLAlchemy 2.1.1) on Python 3.14 × CUBRID 11.4 and lanepycubrid@sa2.0(pycubrid 1.7.1, SQLAlchemy 2.0.53) on Python 3.10 × CUBRID 10.2, both pinned and both recording versions viascripts/report_driver_versions.py; the CUBRIDdb lane (cubrid@sa2.0, CUBRID 11.4) is unchanged.test/known_failures.txtis now keyed per lane: every entry names the<driver>@sa<major.minor>lanes (optionally@cubrid<major.minor>for a server-specific failure) it fails in and is strict-xfailed only there, so a failure baselined for one driver cannot hide a regression on the other. WithCUBRID_STRICT_KNOWN_FAILURES=1a strict XPASS, a stale entry, a listed entry that is skipped instead of xfailed, a lane with no reviewed baseline, or a SQLAlchemy release other than the lane's pinned one fails the run; a malformed entry, a duplicated node id or a server-narrowed tag outside the (lane, server) pairs CI gates is a load error. The pycubrid compliance step runs even when the CUBRIDdb lane failed. A new offlinetest/test_known_failures.pyvalidates the manifest format.docs/DEVELOPMENT.mdanddocs/SUPPORT_MATRIX.md(+ Korean) describe the lanes and the baseline update process. - Required driver-differential lane with an all-skipped guard and version record (#486) — the regular and full integration jobs now run
test/test_driver_differential.pywith pycubrid and CUBRIDdb andCUBRID_REQUIRE_DRIVER_DIFFERENTIAL=1; with that variable set,test/conftest.pyfails the session when no comparison ran and passed, so a lane where every case skipped or none was collected no longer reports success. Local runs without the variable still skip cleanly.scripts/report_driver_versions.pyrecords the exact Python, SQLAlchemy, pycubrid, CUBRIDdb and CUBRID server versions in the job log and the GitHub step summary. New differential cases cover Coreexecutemanywith integer, UTF-8/CJK and NULL values, textualexecutemanywith integer and UTF-8/CJK values (CUBRIDdb 11.3.0.51 reuses the previous row's value for a NULL parameter there), scalar binds, textual-SQL result column names, and commit/rollback visibility; contract areas blocked upstream are tracked in #480–#484 (#480 and #481 have since added differential cases).docs/DEVELOPMENT.mdnow states that a specific pycubrid release candidate must pass the downstream contract suite before thepycubrid>=1.3.2,<2.0bound is widened, while the weeklypycubrid@maincanary stays non-blocking. Tests and CI only; no dialect behavior change. - Live BLOB/CLOB value-contract tests on released drivers (#485) —
test_integration.py(CUBRIDdb andcubrid+pycubrid://lanes) andtest_aio_integration.py(cubrid+aiopycubrid://) now round-tripLargeBinary,BLOB,CLOBandTextthrough Core and ORM with small, Unicode/CJK,NULLand 256 KiB payloads (larger than pycubrid's ~80 KBLOB_READchunk), and assert the returned value isbytes/str, never a driver LOB handle. Verified on CUBRID 10.2 and 11.4: writes store the full value andNULL/Textround-trip, but non-NULLBLOB/CLOBreads return a driver LOB locator (pycubriddicthandle, CUBRIDdb'file:...'string), and asyncLargeBinary/BLOBbinding fails because the async DB-API adapter lacksBinary. Those cases are strict per-driver xfails;docs/TYPES.md,docs/DRIVER_COMPAT.mdanddocs/SUPPORT_MATRIX.mddocument the current behavior (reflection is unaffected), and theCLOBtype row no longer listssqlalchemy.Text, which compiles toSTRING. No dialect behavior changes. - Live tests that results are never silently truncated across commit or rollback (#481) —
test_integration.py(CUBRIDdb andcubrid+pycubrid://lanes) andtest_aio_integration.pyread a 500-row, 1000-byte-per-row result (more than pycubrid's 100-row FETCH batch; the broker's first response holds ~16 rows) withfetchone(),fetchmany()andfetchall()aftercommit(),rollback()or no boundary, and require every row or a DB-API error, never a successful partialResult. The driver-differential suite adds the rollback case for both drivers. Verified on CUBRID 10.2 and 11.4: CUBRIDdb 11.3.0.51 returns all rows after commit and raisesInterfaceErrorafter rollback; pycubridmainraisesInterfaceError; released pycubrid 1.7.1 silently returns only the buffered rows on a warm connection (cubrid-lab/pycubrid#395), so those sync cases are strict xfails for pycubrid only viatest/pycubrid_upstream.py(no-op underCUBRID_PYCUBRID_UPSTREAM=1, set by thepycubrid@mainintegration canary; seedocs/DEVELOPMENT.md). Async results pass on every driver becauseAsyncConnection.execute()buffers the whole result.docs/DRIVER_COMPAT.mddocuments the per-driver behavior. Tests, CI and docs only; no dialect behavior change. - Live
IntegrityErrorcontract tests for constraint violations (#480) —test_integration.py(CUBRIDdb andcubrid+pycubrid://lanes),test_aio_integration.py(cubrid+aiopycubrid://) and the driver-differential suite now check through Core and ORM that NOT NULL, foreign-key and unique/primary-key (control) violations raisesqlalchemy.exc.IntegrityError, and that the same connection orSessionruns new statements afterrollback(). Verified on CUBRID 10.2 and 11.4: CUBRIDdb 11.3.0.51 and pycubridmainpass every case; released pycubrid 1.7.1 raises NOT NULL (-631) and foreign-key (-922) violations asDatabaseError(cubrid-lab/pycubrid#390), so those cases are strict xfails for the pycubrid drivers only. The newtest/pycubrid_upstream.pyhelper applies such xfails unlessCUBRID_PYCUBRID_UPSTREAM=1, which the weeklypycubrid@mainintegration canary sets;docs/DEVELOPMENT.mddocuments it and when to remove it.docs/DRIVER_COMPAT.mddocuments the per-driver exception classes. Tests, CI and docs only; no dialect behavior change. - Stronger
IntegrityErrorcontract tests (#480) — the Core, ORM, async and driver-differential constraint-violation tests now prove same-connection reuse: the error does not invalidate the connection, and afterrollback()the sameConnection(and, for the ORM, aSessionbound to it) keeps the same DBAPI connection. Before the pycubrid-only xfail is applied, they also assert the native server error code (-631 NOT NULL, -922 foreign key, -670 unique; pycubridError.code, CUBRIDdbargs[0]), and the class assertion also checks thatexc.origis the driver'sIntegrityError. A patchedis_disconnect()that always returnsTruenow fails these tests. Theis_disconnect()docstring no longer claims CUBRIDdb lacksOperationalError. Tests and docs only; no behavior change. - Stronger result-completeness tests (#481) — the sync test now asserts that CUBRIDdb raises
sqlalchemy.exc.InterfaceErrorafter rollback (and no error otherwise). It reads the FETCH batch size from pycubrid'sConnectionsignature instead of hardcoding 100. Every fetched prefix must be in order with intact payloads before the pycubrid#395 xfail applies, and the marker now covers only the completeness assertion; an explicit error still turns into a strict XPASS on a fixed build. The driver-differential case also checks that pycubrid's first response does not hold the whole result. A new async case drives the lazily fetchingpycubrid.aiocursor directly (execute, fetch one, commit or rollback, fetch the rest); released pycubrid 1.7.1 silently returns 16 of 500 rows there, so it is a strict xfail for pycubrid#395.docs/DRIVER_COMPAT.mdKnown Issue 8 (en/ko) records the raw async cursor behavior. Tests and docs only; no behavior change. - Live
cursor.descriptioncontract tests (#482) —test_integration.py(CUBRIDdb andcubrid+pycubrid://lanes) andtest_aio_integration.py(cubrid+aiopycubrid://) check the subset SQLAlchemy exposes:Result.keys()versus description names fortext()aliases/expressions and Coreselect(), representative scalar type codes,null_okfor nullable and NOT NULL columns,SET/MULTISET/SEQUENCEtype codes, reflected nullability, and sync/async pycubrid agreement; the driver-differential suite adds a scalar-description comparison. Verified on CUBRID 10.2 and 11.4: CUBRIDdb 11.3.0.51 and pycubridmainpass (CUBRIDdb reports collection columns with CCI composite codes such as 40 forSET(INTEGER), recorded per driver); released pycubrid 1.7.1 reportsnull_okinverted (cubrid-lab/pycubrid#431) and collection columns as their element type (cubrid-lab/pycubrid#430), so those cases are strict xfails for pycubrid only viatest/pycubrid_upstream.py(no-op underCUBRID_PYCUBRID_UPSTREAM=1, set by thepycubrid@mainintegration canary; seedocs/DEVELOPMENT.md).docs/FEATURE_SUPPORT.mddocuments that the dialect passescursor.descriptionthrough without normalization. Tests, CI and docs only; no dialect behavior change. - Review follow-ups for the #480/#481 contract tests — the result-completeness checks (sync, raw async cursor and driver-differential) now validate the order and payload of every returned row, including the one consumed by
fetchone(), before the pycubrid#395 xfail applies. The driver-differential constraint-violation case now also asserts thatexc.origis the driver's ownIntegrityErrorfor CUBRIDdb and, behind the existing pycubrid#390 marker, for pycubrid. Tests only; no behavior change.
Changed
- Contract tests now require the pycubrid 1.8.0 fixes (#480, #481, #482) — the strict
xfail_unreleased_pycubrid_fixmarkers for cubrid-lab/pycubrid#390 (NOT NULL / foreign-key violations raiseIntegrityError), #395 (no silently truncated result after commit/rollback), #430 (collectioncursor.descriptiontype codes) and #431 (null_oknot inverted) are removed fromtest_integration.py,test_aio_integration.pyandtest_driver_differential.py, so those assertions run unconditionally on the pycubrid lanes and require pycubrid 1.8.0 or later.test/pycubrid_upstream.pyand theCUBRID_PYCUBRID_UPSTREAMswitch inupstream-canary.ymlare removed;DRIVER_COMPAT.mdKnown Issues 8 and 9 andFEATURE_SUPPORT.mdnow mark these pycubrid bugs as fixed in 1.8.0. Thepycubrid>=1.3.2,<2.0dependency bound is unchanged. - Alembic: CUBRID DDL is transactional, so the whole upgrade is now atomic by default (#503) —
CubridImpl.transactional_ddlis nowTrue(wasFalse). The docs and docstrings claimed CUBRID implicitly commits DDL; it does not. With client autocommit off, which the dialect always sets,ROLLBACKundoesCREATE TABLE,ALTER TABLE,DROP TABLE,TRUNCATE,CREATE INDEX(includingWITH ONLINE [PARALLEL n]),CREATE VIEW,CREATE SERIALandRENAME TABLE, and DDL never commits pending DML; client autocommit is the only auto-commit behavior. Behavior change: a failedalembic upgradenow rolls back every revision of that run, including thealembic_versionupdate, instead of keeping the revisions that finished before the failure (each revision was already atomic on its own, because Alembic's online mode wraps it inconnection.begin()), andcontext.is_transactional_ddl()returnsTrue. Settransaction_per_migration=Trueincontext.configure()to keep per-revision commits; it is recommended for long or large-table migrations, because uncommitted DDL holds schema locks (other sessions wait onSCH_S_LOCK).CubridImpl.emit_begin()now emits nothing, because CUBRID has noBEGINstatement (csql rejects it), so offline--sqlscripts contain noBEGIN;and end each transaction withCOMMIT;; run them withcsql --no-auto-commit --no-single-line(csql's default single-line mode continues past a failing statement, still runs the trailingCOMMIT;and exits 0). New live tests intest/test_transactional_ddl.py(run in the PR and full integration matrices withCUBRID_REQUIRE_TRANSACTIONAL_DDL=1, which turns an unavailable driver, server or csql into a failure instead of a skip) prove, on CUBRID 10.2 and 11.4 with pycubrid and CUBRIDdb, that each of those statements rolls back, that DDL does not commit earlier DML and thatmetadata.create_all()rolls back; that a failing multi-revision Alembic upgrade leaves no partial schema and no version row, whiletransaction_per_migration=Truekeeps the completed revision; and that offline--sqloutput runs in csql. README (and translations),docs/ALEMBIC.md,docs/TROUBLESHOOTING.md,docs/ISOLATION_LEVELS.md,docs/SUPPORT_MATRIX.md,docs/ARCHITECTURE.md,docs/ORM_COOKBOOK.md(+ Korean),docs/PRD.md, AGENTS.md,llms.txtandscripts/alembic_safety_check.pyno longer describe DDL as auto-committed or recommend one DDL operation per revision.
Removed
- Removed the dead SQLAlchemy 1.x
should_autocommit_text()hook, itsAUTOCOMMIT_REGEXP, and the legacydbapi()classmethods (#462) — SQLAlchemy 2.0 and 2.1DefaultExecutionContextno longer define or callshould_autocommit_text(), so the regex it consulted never affected a supported runtime (this also retires #439's request to addREPLACEto it, surfaced by @biggdawg320 in #468: there is no longer a regex to be inconsistent with the dialect'sREPLACE INTOconstruct).create_engine()only falls back to adbapi()classmethod when the dialect class does not defineimport_dbapi()itself, andCubridDialect,PyCubridDialectandPyCubridAsyncDialectall do, so thedbapi()wrappers were unreachable. Transactions are unchanged: DML and DDL text commits only viaconn.commit(),engine.begin()or aSession. Added offline regression tests asserting the hooks stay gone, that engine creation resolves the DBAPI viaimport_dbapi()without a deprecation warning, and thatDELETE/REPLACE/CREATEtext is rolled back unless explicitly committed;docs/CONNECTION.md,docs/FEATURE_SUPPORT.md,docs/DML_EXTENSIONS.md,docs/SUPPORT_MATRIX.md(+ Korean) anddocs/PRD.mdno longer describe the non-existent statement-text autocommit detection.
Fixed
- Alembic autogenerate detects
Text↔String(n)changes (#544) —CubridImplmatched the reflectedVARCHAR(1073741823)of aSTRINGcolumn by upper-cased class name, and the dialect'sSTRINGtype shares that name with the genericsa.String, so a model column changed fromTexttoString(10)produced no diff and the migration that shrinks the column was never generated. The dialect'sSTRINGis now matched by class;Text,UnicodeText,CLOBand aString()without a length still match a reflectedSTRINGcolumn, whileString(n)goes through Alembic's normal length comparison. Live autogenerate tests on both drivers cover both directions (Text→String(10)andString(10)→Textare detected, applied, and compare clean afterwards) and the unbounded types that must not report a diff. BINARY,VARBINARYandUUIDcreate columns CUBRID accepts (#545) —sqlalchemy.BINARY,VARBINARYandUUIDfell through to SQLAlchemy's genericBINARY/VARBINARY/UUIDDDL, which CUBRID 10.2 and 11.4 reject, so a model using them could not be created.BINARY(n)now compiles toBIT(n*8)(BINARY()toBIT(8)) andVARBINARY(n)toBIT VARYING(n*8)(VARBINARY()toBIT VARYING); both drivers bind and return the values asbytes, and a zero length raisesCompileError.UUIDcompiles toCHAR(32)likeUuid, with SQLAlchemy's non-native UUID handling foras_uuid=TrueandFalse. Reflection now keeps the length andVARYINGofBIT(n)/BIT VARYING(n)columns (they reflected asBIT(1)), so these columns compile back to the same DDL and Alembic autogenerate reports no false type change. A new compiler test sweeps every generic SQLAlchemy type and fails if one compiles to a type name that CUBRID rejects inCREATE TABLE(list taken from live runs on 10.2 and 11.4) or to one not yet checked live.docs/TYPES.md(+ Korean) documents the mappings and fixes theCHAR VARYINGreflection rows, which saidNVARCHARalthough it reflects asVARCHAR.- Boolean
IS/IS NOTpredicates compile to SQL CUBRID accepts (#465) — without a native BOOLEAN, SQLAlchemy renderscol.is_(True),is_(False),is_not(True)andis_not(False)(also withtrue()/false(), a bound parameter, or any expression such as(col == 5).is_(True)) asIS 1/IS 0/IS NOT 1/IS NOT 0, which CUBRID rejects with a syntax error on 10.2 and 11.4: itsISaccepts only[NOT] NULLand[NOT] TRUE/FALSE, and since 11.2IS TRUEalso needs a logical operand (b IS TRUEfails for aSMALLINTcolumn), while(x = 1) IS NOT TRUEis rejected in a SELECT list.CubridCompilernow rendersISagainst a value with the null-safe equal operator, likeIS [NOT] DISTINCT FROM:a IS basa <=> banda IS NOT bas(a <=> b) = 0, which keeps the SQL semantics exactly (IS TRUEis neverNULL;IS NOT TRUEis its complement) inWHEREclauses and SELECT lists;IS [NOT] NULLis unchanged.col == True,not_(col),true()/false()andand_()/or_()already compiled to valid SQL (col = 1,col = 0,1 = 1) and are unchanged; CUBRID itself rejects logical operators (AND,OR,NOT) in a SELECT list, which is documented. New compiler tests and a live truth-table test (stored 1, 0 and NULL;WHEREand projection) on CUBRID 10.2 and 11.4 with CUBRIDdb and pycubrid cover it;docs/TYPES.mdanddocs/FEATURE_SUPPORT.md(+ Korean) document the rendering and its semantics. - Reflection works for non-DBA users: read the public catalog views instead of the DBA-only
_db_*tables (#549) —_db_index,_db_index_keyand_db_attribute(and_db_class) are readable only by DBA on CUBRID 10.2, 11.0, 11.2 and 11.4; for any other user a query fails withSELECT is not authorized on _db_index(-494). The dialect swallowed that error, so reflection silently degraded:get_indexes()reported the primary-key index and the foreign-key auto-indexes as ordinary indexes (spurious Alembic autogeneratedrop_index/create_indexdiffs, the #120 problem),get_pk_constraint()fell back toSHOW COLUMNSand lost the PK name and every column of a composite key after the first (#426),get_unique_constraints()fell back toSHOW CREATE TABLEparsing,get_columns()returned no column comments, andhas_index()raised.has_index()keeps the name matching and owner preference from #543 on the public view. These methods now read the publicdb_index,db_index_keyanddb_attributeviews, which every user can read (flags are'YES'/'NO'there). Since CUBRID 11.2 those views also list same-named classes of other owners that the user may read, so the rows are limited to the owner the cacheddb_classlookup from #529 prefers (the current user's class first); before 11.2 class names are global and the views have noowner_namecolumn. This also stops a same-named class of another owner from adding its indexes or comments on 11.2+. The catalog lookups also match the table name as given or folded to lower case, like thedb_classlookup andSHOW ... IN <name>:get_indexes("Users")used to report the PK and FK auto-indexes (andget_pk_constraint()lost composite columns,get_columns()the comments) even for DBA. A failing catalog query now raises instead of silently falling back to incomplete metadata;get_pk_constraint()still usesSHOW COLUMNSandget_unique_constraints()still parsesSHOW CREATE TABLEwhen the catalog has no matching row, andget_unique_constraints()maps anUnknown classerror from itsSHOW INDEXESstep toNoSuchTableError. A new live test reflects a table with a named composite PK, a foreign key, a unique constraint, an index and a column comment as DBA and as a newly created user, by its name and in upper case, on CUBRID 10.2 and 11.4 with both drivers (on 11.2+ with a same-named decoy table of the other user).docs/FEATURE_SUPPORT.md,docs/ARCHITECTURE.md(+ Korean) anddocs/PRD.mddescribe the catalog sources. has_table()andhas_index()find mixed-case names, socheckfirst=Trueno longer silently skips (#543) — CUBRID stores identifiers in lower case, even quoted ones, but both methods compared the names exactly as passed in. ForIndex("IX_Mixed543", Table("Users543", ...)),has_index()returnedFalse, soIndex.drop(checkfirst=True)did nothing and the index stayed;has_table()had the same gap. Both now also match the lower-case form (IN (:name, LOWER(:name))), like_get_class_type()and the other reflection methods, andhas_index()resolves the table with the same owner preference (the current user's own class first, which matters on CUBRID 11.2+ where owners can share a class name).@reflection.cacheand error propagation (#444, #538) are unchanged. Live tests cover mixed-casecheckfirstcreate and drop on CUBRID 10.2 and 11.4 with both drivers.CreateIndex(..., if_not_exists=True)raisesCompileErrorinstead of emittingCREATE INDEX IF NOT EXISTS(#540) — CUBRID 10.2, 11.0, 11.2 and 11.4 rejectCREATE INDEX IF NOT EXISTS(andCREATE UNIQUE INDEX IF NOT EXISTS) with a syntax error, but the DDL compiler emitted it forIndex(...)/CreateIndex(..., if_not_exists=True)and Alembicop.create_index(..., if_not_exists=True).CubridDDLCompiler.visit_create_indexnow refuses it with aCompileErrorthat points toinspect(conn).has_index()andIndex.create(checkfirst=True), mirroringDROP INDEX IF EXISTS(#533). A plainCREATE INDEXis unchanged.docs/ALEMBIC.mdanddocs/FEATURE_SUPPORT.md(+ Korean) document the limitation.- Reflecting a missing table or view raises
NoSuchTableError(#530) —get_foreign_keys()andget_unique_constraints()returned[]for an object that does not exist (theSHOW CREATE TABLEfallbacks swallowed the server'sUnknown classerror with a WARNING traceback),get_table_comment()returned{"text": None}, andget_pk_constraint()andget_view_definition()let the raw -493ProgrammingErrorescape. Because SQLAlchemy's defaultget_multi_*()implementations leave out only the names whose single-object method raisesNoSuchTableError, the multi variants also reported the missing object.get_foreign_keys()andget_unique_constraints()now look up the class type first (the cached, owner-awaredb_classlookup from #529): no row raisesNoSuchTableError, and a view (VCLASS) returns[]without running the table-onlySHOW CREATE TABLE, which removes the WARNING traceback logged for every reflected view.get_table_comment()raises whendb_classhas no row, and matches the name like the class-type lookup (as given or lower-cased, the current user's class first).get_pk_constraint()(SHOW COLUMNSfallback),get_view_definition()(SHOW CREATE VIEW) and theSHOW CREATE TABLEfallbacks ofget_foreign_keys()/get_unique_constraints()map an error toNoSuchTableErroronly when its message is CUBRID'sUnknown class: pycubrid reports syntax errors,<name> is not a classand some permission errors with the same -493 / SQLSTATE 42S02 (#454), so a generic -493 still propagates (or, in the DDL fallbacks, keeps the logged empty result).get_view_definition()on a table (for whichSHOW CREATE VIEWreturns no row) now raisesNoSuchTableErrorinstead of returning"", as SQLAlchemy's suite expects. The 9 entries of the #530 group, includingtest_get_multi_unique_constraints[True-ObjectKind.ANY-ObjectScope.ANY…]that also needed #529, are removed fromtest/known_failures.txtfor all three lanes;docs/FEATURE_SUPPORT.md(+ Korean) documents the behavior, and the per-lane counts in thetest/known_failures.txtheader anddocs/SUPPORT_MATRIX.md(+ Korean) are updated (cubrid@sa2.0 68 / 61, pycubrid@sa2.0 54, pycubrid@sa2.1 58 / 51). UnicodeTextcreates a CUBRIDSTRINGcolumn instead of the non-existentTEXT(#534) —CubridTypeCompileroverrodevisit_text(Text→STRING) but notvisit_unicode_textorvisit_TEXT, soUnicodeTextandsqlalchemy.TEXTfell through to SQLAlchemy's genericTEXT, andCREATE TABLEfailed with "dba.TEXT is not defined" (-494) on every CUBRID version. Both now compile toSTRING, likeText; CUBRID strings use the database charset, so there is no separate national text type. Alembic'scompare_typenow also treatsUnicodeTextlikeTextagainst a reflectedVARCHAR(1073741823), so autogenerate does not report a spurious type change for it. New compiler tests and a liveCREATE TABLE+ Korean/Japanese/Chinese/emoji round trip on CUBRIDdb and pycubrid cover it, and the sixUnicodeTextTestcompliance entries are removed fromtest/known_failures.txtfor every lane.docs/TYPES.md(+ Korean) anddocs/PRD.mdnow listUnicodeTextasSTRING, andUnicode(n)as theVARCHAR(n)it actually compiles to;docs/TYPES.md(+ Korean) also notes that no column-levelCHARSET/COLLATEis rendered for string and text types (collation=is dropped) and that non-ASCII text needs a UTF-8 database. The per-lane known-failure counts in thetest/known_failures.txtheader anddocs/SUPPORT_MATRIX.md(+ Korean) are updated (cubrid@sa2.0 77 / 70, pycubrid@sa2.0 63, pycubrid@sa2.1 67 / 60).- Unique constraints are no longer reported twice, and views no longer reflect their base table's indexes (#529) — CUBRID implements a
UNIQUEconstraint as a unique index and cannot tell it apart fromCREATE UNIQUE INDEX(same_db_indexflags;SHOW CREATE TABLEprints both asUNIQUE KEY), soget_indexes()andget_unique_constraints()both listed the same object, andTablereflection built both a uniqueIndexand aUniqueConstraintfor it. Following SQLAlchemy's MySQL dialect, everyget_unique_constraints()entry (catalog andSHOW CREATE TABLEpaths) now carries"duplicates_index": <name>, soTablereflection and Alembic autogenerate keep only the unique index, andrequirements.pyopensunique_constraints_reflect_as_indexandunique_index_reflect_as_unique_constraints.get_indexes()on a view returned the base table's indexes, including its primary key, becauseSHOW INDEXES IN <view>lists them; it now checksdb_class.class_typeand returns[]for a view (VCLASS). That lookup matches the name as given or folded to lower case, prefers the current user's class over a same-named class of another owner (CUBRID 11.2+ allows one per owner; a DBA viewy_dupgranted to PUBLIC made user u2'sget_indexes("y_dup")return[]), and is cached perInspector. Reflection also no longer leaks server query entries (#548): the single-row lookups (db_classclass type and table comment,SHOW CREATE TABLEfor foreign keys and the unique-constraint fallback,SHOW CREATE VIEW) read their row withResult.first(), which closes the result, instead offetchone(), which left it open. Each open result held one of the connection's 100 server query entries, soMetaData.reflect()of about 50–60 tables on one pycubrid connection failed with -830 "Cannot allocate query entry". An audit found no other unclosed single-row read indialect.py,base.py,pycubrid_dialect.py,alembic_impl.pyortrace.py(the raw-cursorfetchone()calls close their cursor infinally). New live tests reflect 150 tables on one connection (pycubrid on CUBRID 10.2 and 11.4, and CUBRIDdb) and reflect a u2 table that shares its name with a DBA view (CUBRID 11.2+). The 36ComponentReflectionTestentries of the #529 group are removed fromtest/known_failures.txtfor all three lanes;docs/FEATURE_SUPPORT.md(+ Korean) documents the behavior, and the per-lane counts in thetest/known_failures.txtheader anddocs/SUPPORT_MATRIX.md(+ Korean) are updated (cubrid@sa2.0 83 / 76, pycubrid@sa2.0 69, pycubrid@sa2.1 73 / 66). get_foreign_keys()returns foreign keys sorted by name (#531) — the constraints were returned in the orderSHOW CREATE TABLElists them, which is not name order, so the SQLAlchemy compliance suite'sComponentReflectionTest::test_get_multi_foreign_keys(which expectsfk_dingalings_id_userbeforezz_email_add_id_fg) compared the wrong foreign keys even though each reflected constraint was correct.get_foreign_keys()(and thereforeget_multi_foreign_keys()) now sorts by constraint name, like SQLAlchemy's built-in dialects. The 8test_get_multi_foreign_keys[False-…]entries are removed fromtest/known_failures.txtfor all three lanes; the per-lane counts in its header and indocs/SUPPORT_MATRIX.md(+ Korean) are updated (cubrid@sa2.0 119 / 112, pycubrid@sa2.0 105, pycubrid@sa2.1 109 / 102).- Foreign keys and unique constraints on column names containing parentheses are reflected (#532) — the
SHOW CREATE TABLEparser matched a constraint's column list up to the first), so a bracketed name such as[(3)]cut the list short:REFERENCES [dba.p] ([(3)])reflectedreferred_columns: [], and autoloading the child table raisedArgumentError._RE_FOREIGN_KEYand_RE_UNIQUE_KEYnow match the column list as a sequence of bracketed names (each optionally followed byASC/DESC, with optional whitespace inside the parentheses), so names containing(,),,or spaces parse correctly. New offline regex tests use DDL captured from CUBRID 11.4 for such names. The 6BizarroCharacterTest::test_fk_ref[…-(3)-…]entries are removed fromtest/known_failures.txtfor all three lanes; the per-lane counts in its header and indocs/SUPPORT_MATRIX.md(+ Korean) are updated (cubrid@sa2.0 127 / 120, pycubrid@sa2.0 113, pycubrid@sa2.1 117 / 110). Index.drop()and Alembicop.drop_index()emitDROP INDEX <name> ON <table>(#533) —CubridDDLCompilerhad novisit_drop_index, so SQLAlchemy's genericDROP INDEX <name>was emitted and CUBRID rejected it with a -493 syntax error (CUBRID scopes an index to its table). The compiler now emitsDROP INDEX <quoted name> ON <quoted table>, including the table's schema when one is set. CUBRID 10.2, 11.0, 11.2 and 11.4 have noDROP INDEX IF EXISTS(all four answer a syntax error), soDropIndex(..., if_exists=True)andop.drop_index(..., if_exists=True)now raiseCompileErrorinstead of emitting SQL the server rejects; checkinspect(conn).has_index()first, asIndex.drop(checkfirst=True)does. An index not bound to a table also raisesCompileError, andop.drop_index()withouttable_name(which Alembic binds to a placeholder table namedno_table) raisesCompileErrorasking fortable_nameinstead of emittingDROP INDEX <name> ON no_table.CubridDialect.has_index()is now@reflection.cached likehas_table()and theget_*methods, so anInspectoranswershas_index()from its cache untilclear_cache()(calls without aninfo_cache, such asIndex.drop(checkfirst=True), still query the catalog each time); with the drop fixed this was the remainingHasIndexTest::test_has_index[inspector]failure. Because a cached answer lives as long as theInspector, a failing catalog query inhas_index()now raises instead of being swallowed asFalse; a missing table or index still returnsFalse. The per-lane known-failure counts in thetest/known_failures.txtheader anddocs/SUPPORT_MATRIX.md(+ Korean) are updated (cubrid@sa2.0 133 / 126, pycubrid@sa2.0 119, pycubrid@sa2.1 123 / 116). New compiler tests, an offline cache test, offline Alembic--sqltests and liveIndex.drop()/op.drop_index()tests cover it, and theHasIndexTest::test_has_index[dialect|inspector]compliance entries are removed fromtest/known_failures.txtfor every lane.docs/ALEMBIC.md(+ Korean) documents theON <table>form and theif_existslimitation.- JSON
.as_numeric(p, s)returnsDecimalinstead offloat(#535) — a JSON element cast withas_numeric()was renderedCAST(JSON_EXTRACT(...) AS DOUBLE), so the driver returned afloat, and because the dialect declares native decimal support no result processor converted it:select(t.c.data["a"].as_numeric(10, 2))returned15.0instead ofDecimal('15.00')and lost precision, on SQLAlchemy 2.0 and 2.1 alike. ANumerictarget with both precision and scale now rendersCAST(JSON_EXTRACT(...) AS NUMERIC(p,s)), as SQLAlchemy's MySQL dialect rendersDECIMAL(p, s), and both drivers return aDecimalat scales(JSON numbers, including exponent notation such as1e3, plain-decimal numeric strings such as"15.5", andtrue/falsecast; JSONnullis still SQLNULL). CUBRID raises -181 ("Cannot coerce value of domain json to domain numeric") for a string in exponent notation ("1e3") and for a non-numeric string such as"abc"or"", and -427 (data overflow) for a value that does not fitNUMERIC(p,s). For exponent-notation strings and values outsideNUMERIC(p,s), useas_float(), which casts toDOUBLE.as_float()also raises -181 for a non-numeric string, so validate or filter such values before casting; withasdecimal=Falsethe result is still converted tofloat.as_float()and anyFloatstayDOUBLE, and aNumericwithout both precision and scale also staysDOUBLE, because a bare CUBRIDNUMERICmeansNUMERIC(15,0)and would truncate. Verified on CUBRID 10.2 and 11.4. New offline compiler tests and a live round trip intest_integration.py(CUBRIDdb and pycubrid, SQLAlchemy 2.0.53 and 2.1.1) cover it; the 18JSONTest::test_index_cross_casts[...-numeric]entries are removed fromtest/known_failures.txt(lanepycubrid@sa2.1: 143 / 136 → 125 / 118), anddocs/FEATURE_SUPPORT.md(+ Korean) listsas_numeric(). isolation_level="AUTOCOMMIT"works on every driver, and an engine-level isolation level survives a per-connection override (#501) —docs/TROUBLESHOOTING.mdrecommendedexecution_options(isolation_level="AUTOCOMMIT"), butget_isolation_level_values()did not list it, soexecution_options()raisedArgumentErrorandcreate_engine(..., isolation_level="AUTOCOMMIT")raisedValueErroron first connect, oncubrid://,cubrid+pycubrid://andcubrid+aiopycubrid://.CubridDialectkeptisolation_levelto itself and applied it from its ownon_connecthooks, bypassing SQLAlchemy's validation, and itsreset_isolation_level()override always reset a checked-in connection to READ COMMITTED, dropping an engine-level level after a per-connection override. Following SQLAlchemy's mysqldb/psycopg pattern,AUTOCOMMITis now a valid level:set_isolation_level()turns on the driver'sautocommit(CUBRIDdb, pycubrid and the async adapter all expose the property), and any other level turns it off beforeSET TRANSACTION ISOLATION LEVEL. It toggles only when the mode changes, because on pycubrid each toggle ends the transaction and triggers a reconnect.detect_autocommit_setting()reads the same flag, socreate_engine(skip_autocommit_rollback=True)works too.isolation_levelis passed toDefaultDialect, which applies it on connect after the dialect disables autocommit and restores it on checkin; thereset_isolation_level()override and theon_connectisolation handling are removed. An engine-level alias is stored under its canonical name (for exampleCURSOR STABILITYbecomesREAD COMMITTED), so SQLAlchemy's checkin restore accepts it. Invalid levels now raiseArgumentErrorfromcreate_engine(),engine.execution_options()andconn.execution_options()on every driver, and a non-stringisolation_levelraises it from the dialect constructor; callingdialect.set_isolation_level()directly still raisesValueError, and a DBAPI connection without anautocommitproperty now fails withAttributeErrorinstead of being treated as non-autocommit. Behavior changes for code outsidecreate_engine():on_connect()no longer appliesisolation_level, so a pool built by hand arounddialect.on_connect()must apply it itself (or usedialect._builtin_onconnect()likecreate_engine()); anddialect.isolation_levelnow holds the canonical name (CURSOR STABILITYreads back asREAD COMMITTED). On pycubrid,AUTOCOMMITstatements run at the server default level and reconnect the broker session each time (cubrid-lab/pycubrid#468); the docs recommendskip_autocommit_rollback=Truewith an engine-levelAUTOCOMMITthere. New live tests on all three drivers cover each path: an AUTOCOMMIT row is visible to a second session before any commit and survivesrollback(), the same pooled connection is transactional after checkin, the engine-level level (SERIALIZABLE or AUTOCOMMIT, sync and async) and an engine-level alias are restored on the same pooled connection after a per-connection override,skip_autocommit_rollback=Trueworks with engine-level AUTOCOMMIT, and invalid levels raiseArgumentError. They fail without the fix.docs/ISOLATION_LEVELS.md,docs/TROUBLESHOOTING.mdanddocs/FEATURE_SUPPORT.md(+ Korean) documentAUTOCOMMITand the checkin restore.- Engine- and connection-level
isolation_levelno longer falls back to READ COMMITTED on pycubrid aftercommit()/rollback()(#505) — after the driver'scommit()orrollback()the broker returns the CAS status byte as inactive (out of transaction), and pycubrid 1.7.1 (andmain) then opens a new broker connection before the next request. The new session starts at the server default level, and pycubrid restores onlyautocommit, socreate_engine("cubrid+pycubrid://…", isolation_level="SERIALIZABLE")already reported READ COMMITTED on the first checkout (SQLAlchemy rolls back after its first-connect checks), and a per-connection level was lost at the next commit.CUBRIDdbkeeps the same session and was not affected; the driver bug is cubrid-lab/pycubrid#468.PyCubridDialect(and the async dialect) now remember the level they set on each DBAPI connection and re-apply it indo_commit()/do_rollback(). This costs oneSET TRANSACTION ISOLATION LEVEL+COMMITper commit/rollback, only on connections with a configured level; nothing runs per statement, and such a pooled connection holds a broker CAS while idle. A failed re-apply never turns a successful commit into an error or masks the exception that caused a rollback: it is logged and retried indo_begin()before the next transaction's first statement. On an engine without a configured level, checkin after a per-connection override stops the re-apply for that connection. Remove the workaround once a pycubrid release fixing pycubrid#468 is the minimum. New live tests on all three drivers checkget_isolation_level()after commit, rollback, pool checkin (including the pool's reset-on-return rollback alone),engine.begin()and aSession, and fail without the fix on pycubrid; pycubrid-only live tests simulate a failed re-apply and check that the commit stands, the rollback'sIntegrityErrorpropagates and the next transaction runs at the configured level.docs/DRIVER_COMPAT.mdKnown Issue 10 anddocs/ISOLATION_LEVELS.md(+ Korean) document the driver behavior and the re-apply. Resetting to the engine level after a per-connection override is tracked in #501. driver_connectiononcubrid+aiopycubrid://is now thepycubrid.aioconnection (#520) —PyCubridAsyncDialectdid not overrideget_driver_connection(), so(await conn.get_raw_connection()).driver_connectionreturned SQLAlchemy'sAsyncAdapt_pycubrid_connectionadapter instead of the driver's own connection. The dialect now returns the wrappedpycubrid.aio.AsyncConnection, as SQLAlchemy's asyncpg and aiomysql dialects do, so driver-specific async APIs (ping(),cursor(), ...) are reachable through the documented accessor. A unit test and a live async test cover it, and the async integration tests no longer reach the driver connection through.dbapi_connection.driver_connectionor the adapter's_connection. Code that relied ondriver_connectionbeing the adapter should usedbapi_connectioninstead.- The SQLAlchemy compliance suite no longer silently skips most of its tests (#463) — every property in
sqlalchemy_cubrid/requirements.pyreturned one sharedexclusions.open()/exclusions.closed()object, and SQLAlchemy extends the first requirement of a stacked@testing.requireschain in place, so after collection every "open" requirement also carried the closed rule's skip. Each property now returns a new object (a regression test checks that stacking does not leak). About 300 more suite tests now run per lane (pycubrid, SQLAlchemy 2.0.53, CUBRID 11.4: 334 → 631 passed), and the baselines of all three lanes were recaptured on fresh CUBRID 10.2 and 11.4 databases and classified. The recapture also opensreflects_pk_namesandimplicitly_named_constraints(the suite reported unexpected successes), gatesunicode_ddlon a UTF-8 database (the official Docker image creates ISO-8859-1 databases, where non-ASCII table names break both drivers' catalog decoding for every later test), and closesprecision_generic_float_type(CUBRIDFLOATis single precision). Newly visible dialect bugs are listed as strict known failures, each linked to its follow-up issue: unique constraints reported twice and views reflecting their base table's indexes (#529), missing-object reflection not raisingNoSuchTableError(#530), foreign keys returned in DDL order instead of sorted by name (#531), bracketed column names containing parentheses such as[(3)]not parsed in FK/UNIQUE DDL (#532),Index.drop()emittingDROP INDEXwithoutON <table>(#533),UnicodeTextcompiling toTEXT(#534), and JSON.as_numeric()returningfloatinstead ofDecimal(#535). Inspector.has_table()honors the inspector cache (#463) —CubridDialect.has_table()was not decorated with@reflection.cache, soinspect(engine).has_table()queried the catalog on every call and reported a table created after the first call beforeclear_cache(), unlike SQLAlchemy's built-in dialects. Found by the SQLAlchemy 2.1HasTableTest::test_has_table_cachein the new pycubrid compliance lane (SQLAlchemy 2.0's harness never reached it). Directdialect.has_table()calls without aninfo_cacheandcreate_all(checkfirst=True)are unaffected.cubrid://executemany no longer stores the previous row's value forNoneor reports a last-row rowcount (#502) — CUBRIDdb 11.3.0.51 skips bindingNoneand preparesexecutemanyonce, so aNoneparameter kept the previous row's value, and itsrowcountcounted only the last row. Through SQLAlchemy this silently corruptedtext()and CoreUPDATE/DELETEexecutemany and made batched ORM UPDATEs raiseStaleDataError.CubridDialect.do_executemanynow executes each parameter set and reports the summed rowcount, sosupports_sane_multi_rowcountis accurate on both drivers.cubrid+pycubrid://andcubrid+aiopycubrid://keep the driver's prepare-onceexecutemany, and a multi-row Coreinsert()that uses insertmanyvalues is unaffected. An INSERT into a table with a column whose type definesbind_expression()falls back to executemany (#421), so oncubrid://it uses the per-row guard. The textual executemany differential case now includes NULL, the CUBRIDdbRowCountTestmulti-row entries are removed fromtest/known_failures.txt,docs/DRIVER_COMPAT.md(+ Korean) records the driver bug and when the guard will be removed, anddocs/SUPPORT_MATRIX.md(+ Korean) documents executemany rowcount behavior and the guard's per-row cost.- Alembic finds
CubridImplwith a defaultenv.py(#504) —alembic upgradeagainst acubrid://,cubrid+pycubrid://orcubrid+aiopycubrid://URL failed withKeyError: 'cubrid'unlessenv.pyimportedsqlalchemy_cubrid.alembic_implby hand. The package declared analembic.ddlentry point, but no Alembic release reads that group: Alembic looks the impl up in a registry keyed bydialect.namethatDefaultImplsubclasses fill on import, and thealembic.pluginsentry points it does read exist only from 1.18 onward.sqlalchemy_cubrid/dialect.pynow importsalembic_implwhen Alembic is installed and skips it when it is not, which is how other third-party dialects register their impl and works on every supported Alembic release; all dialect variants sharename = "cubrid". The deadalembic.ddlentry point is removed. The standardalembic inittemplate works unchanged for the synchronous URLs; forcubrid+aiopycubrid://online migrations,docs/ALEMBIC.mdnow points to Alembic's async template (alembic init -t async), whose unmodifiedenv.pywas verified live on CUBRID 11.4 (the standard template's synchronous engine fails there withMissingGreenlet). The import never stops the dialect from loading: a missing Alembic is skipped silently, and an installed Alembic that fails to import (1.7.0/1.7.1 raiseNameErroron SQLAlchemy 2.x) only disables the integration with oneRuntimeWarningnaming the exception (logged via thesqlalchemy_cubrid.dialectlogger instead if a warning filter turns it into an error). The[alembic]/[dev]extras, the pre-commit mypy hook and the docs now requirealembic>=1.7.2,<2.0, the oldest release that imports on SQLAlchemy 2.x. A newtest/test_alembic_registration.pyruns thealembicCLI in a fresh interpreter on an unmodifiedalembic initproject, offline (--sql) for all four URL forms and online against a live CUBRID; it fails on the previous code with the reportedKeyError. The roundtrip tests no longer importalembic_implthemselves. README (and translations),docs/ALEMBIC.md,docs/TROUBLESHOOTING.md,docs/SUPPORT_MATRIX.md,docs/ARCHITECTURE.md(+ Korean),docs/PRD.md,docs/FEATURE_SUPPORT.md, AGENTS.md and the wheel smoke checks now describe and verify registration on dialect import. - Async
LargeBinary/BLOBbinding no longer raisesAttributeError(#500) —AsyncAdapt_pycubrid_dbapicopied onlyparamstyle, the exception classes and the type objects frompycubrid, but SQLAlchemy'sLargeBinarybind processor readsdialect.dbapi.Binarywhenever the statement compiles, so everycubrid+aiopycubrid://INSERT/UPDATE touching a binary column failed, even withNoneor an unset ORM attribute. The adapter now copies the full PEP 249 module surface (apilevel,threadsafety,paramstyle, all exception classes,Date/Time/Timestamp, the*FromTicksconstructors,Binaryand theSTRING/BINARY/NUMBER/DATETIME/ROWIDtype objects) from the sync module via one declared tuple. A unit test asserts every PEP 249 name onpycubridis on the adapter; live async tests cover Core inserts ofNoneandbytesintoLargeBinary/BLOBand an ORM insert with an unsetLargeBinary. The async binding xfails from #485 are removed, leaving only the strict LOB-read locator xfails (cubrid-lab/pycubrid#441);docs/DRIVER_COMPAT.mdanddocs/TYPES.md(+ Korean) no longer list the asyncBinaryissue. String(0)/VARCHAR(0)/NVARCHAR(0)no longer silently compile to the 4096 default (#440) — the type compiler checked the length by truthiness, so an explicitlength=0was treated as "no length" and renderedVARCHAR(4096)(orNCHAR VARYING(4096)).length=Nonestill gets the documented default; an explicit zero now raisesCompileError, sinceVARCHAR(0)is not a valid CUBRID length.- Reflection now returns table and view names in deterministic order (#443) — added
ORDER BY class_nametoget_table_names()andget_view_names()catalog queries so reflection results no longer depend on database row order. - Contributor workflow and docs exceptions clarified — normal contributions use shared checks and matching docs; maintainers coordinate project-specific reviews, labels, translation deferrals and releases. Docs-not-needed body reasons require a populated standalone statement, with quoted/template examples excluded; executable doctests and real event/workflow regressions run locally and in docs-sync. Four existing reusable workflow callers are pinned to a verified upstream commit without changing their inputs or rollout settings.
- Pure-driver integration setup is explicit (#464) —
tox -e integrationnow requires acubrid+pycubrid://test URL rather than the former barecubrid://C-extension URL. A bounded sync/asyncSELECT 1preflight fails before pytest when the URL, driver or database is unavailable. Async and stress suites derive their endpoint from the selected URL, preserving authentication, port and query options; an explicitCUBRID_TEST_AURLremains supported. The formal C-extension compliance CI route is unchanged, and optional native-driver comparisons remain explicitly skipped when that driver is absent. - Local tooling agrees with CI (#464) — Ruff/mypy pre-commit revisions match the dev pins; the isolated mypy hook uses interpreter-compatible SQLAlchemy pins and existing Alembic dependencies without automatic stub installation. Tox covers Python 3.10–3.14, uses integration markers, retains 95% coverage, and matches CI's type-check cells. CLI/CI/tox share the Makefile's maintained Python source paths while hooks keep all tracked Python/pyi coverage. A deterministic consistency check rejects stale pins, missing interpreter branches, source omissions and runner drift. Two helper scripts received formatting-only changes with unchanged ASTs.
- Strict typing is enforced across SQLAlchemy 2.0 and 2.1 (#459) — timezone constructor annotations retain their always-enabled timezone behavior; compiler overrides match upstream positional arguments with typed boundaries for unannotated framework hooks. UNIQUE-index FK collision checks now raise a clear
CompileErrorwhen no live Alembic connection is available.make typecheckreports dependency versions, and two pinned CI cells must pass through the requiredmatrix-resultcheck. Runtime SQL compilation and the 95% offline coverage threshold remain unchanged. - Async installation includes SQLAlchemy's required bridge (#448) — the existing
[pycubrid]and[dev]extras now requestSQLAlchemy[asyncio], which installsgreenleton both SQLAlchemy 2.0 and 2.1. The[pycubrid]extra intentionally supports sync and async URLs; bare installation keeps the same dependency contract. Fresh wheel smokes exercise async engine creation on 2.0.53 and 2.1.1 without a database or ambient packages. This is a patch-level installation bug fix. - Installation guidance distinguishes the driver from the async bridge (#448) — FAQs, connection and troubleshooting guides, and translations now consistently state that pycubrid itself needs no CUBRID native libraries, while the
[pycubrid]extra'sgreenletdependency may need build tools when no compatible wheel is available. - Last-insert-ID SQL fallback works on SQLAlchemy 2.x (#458) — both execution contexts now obtain a regular cursor from the active DBAPI connection for
SELECT LAST_INSERT_ID(), instead of calling SQLAlchemy's unimplemented server-side-cursor hook. Native driver IDs (includingNone) remain preferred, and the temporary cursor is closed even if execution, fetching, or integer conversion fails. This is a backward-compatible bug fix suitable for a patch release. - Nightly mutation testing migrated to mutmut 3 — the
devextra now pinsmutmut>=3.8,<4(was>=2.5,<3; supersedes #450). The nightlymutation-testingjob used mutmut 2 CLI options (run --paths-to-mutate,result-ids) that mutmut 3 removed, and masked failures with|| true, so an upgrade would have printed a wrong score instead of failing. The[mutmut]section insetup.cfgnow uses mutmut 3 keys (source_paths,only_mutate, pytest arguments,forkserverprocess isolation because two MERGE tests cannot run twice in one process), and the job reads the score frommutmut export-cicd-stats. A mutmut crash, failing clean test run, zero mutants or zero kills now fails the step; surviving mutants only lower the reported score, and the job remains non-gating (continue-on-error). mutmut 3 generates more mutants than mutmut 2, so scores are not comparable with the earlier 288/449 baseline. CI/dev tooling only; no runtime change.
Docs
- Added a README "First contribution" guide (with Korean translation) pointing newcomers to the right sibling repo for their first PR, and documented the
good first issue→status: in progresslabel lifecycle in AGENTS.md.