Releases: cubrid-lab/sqlalchemy-cubrid
Releases · cubrid-lab/sqlalchemy-cubrid
Release list
v1.8.0
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...
v1.7.1
Added
- Runnable usage examples in public docstrings (#337) — added short, live-verified examples to the five main entry points: the connection URL forms (
cubrid+pycubrid://,cubrid+cubriddb://, asynccubrid+aiopycubrid://) in the package docstring, theON DUPLICATE KEY UPDATEconstruct oninsert(), and reflection viainspect()in the dialect module docstring (REPLACEandMERGEalready had examples). Docstrings only — no behavior change; each example was executed against a live CUBRID 11.4. - UUID type contract test matrix (#376) — added
test/test_uuid_contract.py, which pins the full round-trip contract forsa.Uuid()(Pythonuuid.UUIDvalues) andsa.Uuid(as_uuid=False)(string values) across INSERT, SELECT, WHERE, UPDATE, and reflection, against a live CUBRID. CUBRID has no native UUID type; the dialect stores it asCHAR(32), and these 8integration-marked tests catch any regression in that CHAR-backed storage or the value coercion (verified:sa.Uuid()round-trips asuuid.UUID,as_uuid=Falseasstr, WHERE/UPDATE by UUID work, and reflection reportsCHAR). Wired into the nightlyintegration-fullbug-hunt job. - Mutation testing for
compiler.py(stabilization Phase 8) — added a[mutmut]configuration insetup.cfg(pinnedmutmut>=2.5,<3in thedevextra) and a non-gating nightlymutation-testingjob inintegration-full. Mutation testing checks whether the offline suite actually verifies compiler logic rather than merely executing it: it mutatescompiler.pyand asserts the tests catch each change. The initial run scored 55.9% (251/449 mutants killed) and surfaced weak substring assertions — several tests asserted only"JSON_EXTRACT" in sql,"LIMIT" in sql, or"GROUP_CONCAT" in sqlwhile letting the surrounding SQL structure mutate freely. Strengthening those to exact-match assertions on the full compiled output (JSON path extraction by type,LIMIT/OFFSEToperand order and the offset-without-limit sentinel form,UPDATE ... LIMIT, andGROUP_CONCAT) raised the score to 64.1% (288/449). The job iscontinue-on-errorbecause a score dip is a signal to strengthen a test, not a reason to block a release; the run needs no database. - Real-application ORM dogfood corpus (stabilization Phase 9) — added
test/test_dogfood_orm.py, which models a realistic schema (users 1:N orders, orders N:M tags) and drives it the way an application would, asserting on returned data rather than generated SQL. Covers relationship loading strategies (lazy,selectinloadeager,joinedloadeager, many-to-many,back_populatesnavigation), complex queries (join+filter,GROUP BY/HAVING, aggregate scalars, correlated subquery,LIMIT/OFFSETpagination,LEFT OUTER JOIN), and transactions/mutations (rollback discards changes, commit persists across sessions, bulkUPDATE, cascade delete of orphaned orders, auto-increment PK populated afterflush()). These combinations surface dialect bugs the single-construct unit tests miss. All 16 tests areintegration-marked and run in the nightlyintegration-fullbug-hunt job across every supported CUBRID version. - CUBRID version-differential snapshot testing (stabilization Phase 6) — added
test/version_differential/snapshot.pyandtest/version_differential/compare_snapshots.py. The snapshot generator connects to one live CUBRID server and records a deterministic JSON of dialect-visible behavior: server capability probes (native ENUM, native BOOLEAN cast,<=>, CTE, window functions,RETURNING,INTERSECT/EXCEPT) and the dialect's ownget_columns()reflection of a fixed battery of column types. The nightlyintegration-fullmatrix now generates one snapshot per supported CUBRID version (10.2/11.0/11.2/11.4) and a newversion-differentialjob compares them, failing if any dialect-visible behavior diverges across versions without being declared inKNOWN_DIFFERENCES. This enforces the support claim that a program sees identical dialect behavior on every supported CUBRID version, and turns any undeclared portability difference into a red nightly build. - Capability probe suite and driver-differential testing (stabilization Phases 5 & 7) — added
test/capability/test_capability_probes.py, which runs raw SQL against a live CUBRID and asserts the server's actual behavior matches what the dialect declares (supports_native_enum,supports_native_boolean,supports_is_distinct_from,supports_multivalues_insert,supports_alter,supports_comments,insert_returning, plus CTE/window/LIMIT-OFFSET), so a drift between asupports_*flag and reality is caught immediately instead of surfacing as a confusing downstream error. Addedtest/test_driver_differential.py, which runs the same Core operations on both the pycubrid and CUBRIDdb C-extension drivers and asserts they agree on SELECT round-trips, UPDATE/DELETE rowcount, aggregates, and NULL handling (skipping cleanly if the C-extension driver is not built). Both areintegration-marked and run in the nightlyintegration-fullbug-hunt job. - Property-based fuzz + metamorphic testing with Hypothesis (stabilization Phases 1–4) — added
test/test_fuzz_select.py,test/test_fuzz_insert.py,test/test_fuzz_reflection.py, andtest/test_metamorphic.py. The fuzzers generate SELECT/INSERT combinations and random tables the hand-written suite never enumerated (boundary values0,±1,2^31,2^63-1, empty/Unicode/reserved-word/backslash strings) and assert dialect invariants: compilation raises onlyCompileError(never an unexpected exception), the positional-placeholder count equals the bound-parameter count, single/executemany/multi-values inserts store identical data, DDL→reflection round-trips agree (columns, nullability, primary keys, unique constraints), and (metamorphic) Core vs ORM SELECT, bind vs literal, and SQL LIMIT/OFFSET vs a Python slice return identical rows. Hypothesis profiles (dev/ci/nightly) are registered inconftest.py; PR CI runs the fast profile, and the nightlyintegration-fullworkflow runs the 2000-examples profile against live CUBRID. This bug-hunt found and fixed #426.
Fixed
- Removed the dead
implicit_returning = Falsedialect attribute (#396) — this was a SQLAlchemy 1.x-era switch with a comment claiming it prevents aResourceClosedErroron server-default INSERTs. In SQLAlchemy 2.x the CRUD compiler decides implicitINSERT ... RETURNINGfrominsert_returning(alreadyFalse), notimplicit_returning, andDefaultDialectno longer even defines the attribute. Verified live on CUBRID 11.4 that a server-default INSERT + ORM refresh behaves identically with the attribute removed (noResourceClosedError), and added an offline regression test assertinginsert_returning/update_returning/delete_returningare the real switches and that the dead attribute stays gone. docs/TROUBLESHOOTING.mdLIMIT/OFFSET section no longer documents SQL the dialect never emits (#416) — the section claimed the dialect generatesLIMIT n OFFSET mand that CUBRID rejects the comma form; both were wrong. The dialect only ever emits CUBRID's comma formLIMIT offset, count(offset first), and offset-without-limit uses a sentinel count.select(users).limit(10).offset(20)compiles toLIMIT 20, 10, notLIMIT 10 OFFSET 20— a reader trusting the old doc would also read the two operands in the wrong order. Corrected in both the English and Korean (docs/ko/TROUBLESHOOTING.md) docs.docs/llms-full.txtregenerated and a drift-check added to CI (#383) — the AI-facing doc bundle was stale relative to the corrected source docs (it still taught the pre-#356VALUES()ODKU pattern). It is regenerated from source viascripts/generate_llms_full.py, and thelintCI job now fails ifllms-full.txtdrifts from its generating source (regenerate-and-diff), so it can no longer silently go stale.- Reflection gaps fixed: column comments, missing-table
NoSuchTableError, and system-view filtering (#387) — three real reflection bugs that the compliance suite'sComponentReflectionTestexposed. (1)get_columns()never returned column comments because the_db_attributecatalog query filtered onclass_nameinstead ofclass_of.class_name(a semantic error that was silently swallowed), so every reflectedcommentwasNone; it now uses the correct catalog column. (2) Reflecting a non-existent table viaget_columns()/get_indexes()leaked the raw driverProgrammingError(errno -493, SQLSTATE 42S02) instead of SQLAlchemy'sNoSuchTableError, which the reflection contract requires; both now translate the "table not found" error. (3)get_view_names()returned CUBRID's internal system views (db_class,db_index, …) alongside user views; it now filtersis_system_class = 'NO', matchingget_table_names(). Together these resolve 22 of the 25 baselinedComponentReflectionTestnodes. Also setrequires_name_normalize = False: CUBRID folds identifiers to lower case, not the upper case SQLAlchemy'sdenormalized_namesrequirement assumes, soNormalizedNameTestis now correctly skipped rather than failing (schema case-insensitive matching is unaffected). The remaining 3ComponentReflectionTestnodes (test_get_noncol_index×2,test_get_view_names[False]) are SA-harness fixture limitations — the test's own views / non-column-index tables are never created on CUBRID — not dialect bugs. get_pk_constraint()no longer drops composite primary-key columns (#426) — reflecting a table withPRIMARY KEY (a, b)returned onlyconstrained_columns: ['a'], silently losing every PK column after the first (and giving no column order). Root cause: the reflection scannedSHOW COLUMNSfor thePRIkey flag, but CUBRID (MySQL-compatible) marks only the first column of a composite PK asPRI. The PK columns are now read, in key order, from the `...
v1.7.0
Docs
- Documented that CUBRID's
LISTcollection type is a synonym forSEQUENCE— CUBRID acceptsLIST(type)in DDL but normalizes it toSEQUENCEat parse time, so aLIST(INTEGER)column is stored and reflected asSEQUENCE OF INTEGER(verified on live CUBRID 11.2). Clarified indocs/TYPES.mdand via a code comment inischema_nameswhy the dialect exposes only the canonicalSEQUENCEtype and deliberately omits aLISTtype/reflection entry (a type compiling toLIST(...)would produce spurious Alembic autogenerate diffs against the reflectedSEQUENCE(...)). No behavior change. - Documented canonical isolation-level names on read-back (#293) — clarified that
get_isolation_level()returns the canonical name for a level, which may differ from the alias passed toset_isolation_level()(CUBRID accepts several aliases per numeric level). Added a note todocs/ISOLATION_LEVELS.mdand theget_isolation_level()docstring. Behavior is unchanged; the reverse mapping was already correct. - Aligned the
Documentationproject URL with the README docs badge (#294) —pyproject.tomlpointedDocumentationat the repo tree (.../tree/main/docs) while the README badge pointed at the published sitehttps://cubrid-lab.github.io/sqlalchemy-cubrid/. Both now use the published site so PyPI metadata and the README agree. - Documented the
[cubrid]install extra in the README (#295) — the optionalcubrid = ["CUBRID-Python"]extra (the legacy C-extension driver used by the barecubrid://URL) was declared inpyproject.tomlbut undocumented. Added a README installation note explaining what it installs and clarifying that the pure-Python[pycubrid]driver remains the recommended driver for new projects. - Realigned the SQLAlchemy version-support narrative from "2.0–2.2" to "2.0–2.1" (#312) — README (+5 locale docs),
docs/index.md,docs/ARCHITECTURE.md,docs/QUICKSTART.md,docs/PRD.md,docs/FEATURE_SUPPORT.md,docs/CONNECTION.md,docs/SA_COMPAT.md, andROADMAP.mdclaimed support for a non-existent SQLAlchemy 2.2 line, contradictingdocs/SUPPORT_MATRIX.md(which correctly lists 2.1.x as the latest tested version and ≥2.2 as unsupported). SQLAlchemy's next feature line is 2.1, not 2.2 — PyPI ships only2.1.0b1/b2/b3pre-releases. Historical CHANGELOG entries are left intact as an accurate record.
Changed
is_disconnect()hardened against pycubrid error-message wording drift (#314) — connection-pool invalidation detection now anchors first on stable numeric error codes and then on anOSErrorin the exception's explicit__cause__chain (cycle-guarded), demoting substring matching of error messages to a last-resort fallback. Previously detection was primarily message-based, so a change to pycubrid's wording of a socket/transport failure could silently stop a dead connection from being recycled (stale connection served to the next checkout). Added pycubrid's-4(ER_COMMUNICATION/ SQLSTATE08S01) to the known disconnect codes alongside the existing-21003/-21005/-10005/-10007. Detection stays conservative to avoid false-positive pool invalidation: only the explicitraise ... fromcause chain is followed (implicit__context__is ignored, so an unrelated in-flightOSErrordoes not invalidate a live connection), and a bareOperationalError/InterfaceErrorwith no disconnect code, noOSErrorcause, and a non-disconnect message (e.g."invalid isolation level", a closed-cursor misuse) is not treated as a disconnect. The legacy string-match list is retained as the fallback for the CUBRIDdb C-extension driver (which lacksOperationalError) and for pycubrid's client-side string-only errors (e.g."connection lost during receive", which carries neither a code nor anOSErrorcause). Added offline regression tests covering the-4code, explicit-causeOSErrordetection, implicit-__context__non-detection, non-OSError-cause message fallback, and non-disconnectOperationalError/InterfaceErrorcases.- Ruff lint rule selection now declared explicitly (#271) —
pyproject.tomlconfigured ruff but never set[tool.ruff.lint] select, soruff checkinherited ruff's implicit defaults. Ruff expanded that default set in 0.16 (59 → 413 rules against this repo's config), which is why #267 (0.15.21 → 0.16.2) failed lint with 151 errors in untouched code. Pinning the ruff version in #252 stopped unpinned installs from drifting, but could not survive the bump itself — the rule set is now pinned too, viaselect = ["E4", "E7", "E9", "F"], which is exactly what ruff selected by default through 0.15.x (same 59 rules under both versions).
Added
- New explicit
cubrid+cubriddb://URL and[cubriddb]install extra for the legacy CUBRIDdb driver (#276) — the legacyCUBRIDdbC-extension driver (the driver bound to the barecubrid://URL) can now be selected unambiguously via the explicitcubrid+cubriddb://URL, backed by a matchingcubriddb = ["CUBRID-Python"]install extra. The barecubrid://URL continues to bind CUBRIDdb — no behavior change to any existing URL. For new projects the pure-Pythonpycubriddriver is the recommended choice: installsqlalchemy-cubrid[pycubrid]and usecubrid+pycubrid://(installs with pip alone, no C toolchain). - Native Alembic
ALTER COLUMNtype changes and column renames (#305) —CubridImpl.alter_column()previously raisedNotImplementedErrorfor column type changes and renames, forcing every such migration throughbatch_alter_table(full table recreate). CUBRID in fact supports MySQL-compatibleALTER TABLE ... MODIFY,CHANGE, andRENAME COLUMN, so the dialect now emits native DDL: a type change compiles toMODIFY, a rename toRENAME COLUMN ... TO ..., and a combined rename + type change to a singleCHANGE. Type conversions are governed by the server'salter_table_change_type_strictsystem parameter (incompatible/truncating conversions error whenyes, may silently truncate whenno);batch_alter_tableremains available as a fallback for genuinely lossy conversions. Added SQL-emission tests covering all three DDL forms.
Fixed
is_disconnect()now actually recognizes pycubrid's"connection lost during receive"message (#322) — the #314 hardening documented (in both the CHANGELOG and theis_disconnect()docstring) that pycubrid's client-side"connection lost during receive"string was covered by the message fallback, but the pattern was never added to_disconnect_messages. pycubrid raises this on a clean-EOF receive (connection.pysync path) with no numeric error code and noOSErrorcause, so all three detection layers missed it and SQLAlchemy failed to invalidate the dead connection — a stale connection could be served to the next pool checkout. Added the substring"connection lost"to_disconnect_messagesso the message fallback matches. Added an offline regression test assertingis_disconnect()returnsTruefor a bare driver error carrying only that message.SELECT SCHEMA()returning NULL no longer leaks the fake schema"None"(#290) —_get_default_schema_name()didstr(connection.execute(text("SELECT SCHEMA()")).scalar()), so whenSCHEMA()returned SQL NULL the PythonNonewas stringified to the literal"None". Becauseget_schema_names()and_schema_is_default()test the value against theNoneobject (not the string), that fake name leaked through as a real schema (get_schema_names()→["None"])._get_default_schema_name()now returnsOptional[str]—NonewhenSCHEMA()is NULL, otherwise the real name — soget_schema_names()correctly returns[]with no default schema. This pins the default-schema contract used by the follow-up schema-guard fixes.- Literal rendering no longer doubles backslashes, silently corrupting data on a default CUBRID (#313) —
CubridSQLCompiler.render_literal_value()unconditionally didrendered.replace("\\", "\\\\"), doubling every backslash in inline SQL literals. This is correct for MySQL but wrong for CUBRID, whoseno_backslash_escapessystem parameter defaults toyes— a backslash is a literal character, not an escape. On a default server this silently corrupted any backslash-bearing literal rendered vialiteral_binds=True(e.g.C:\tempwas stored/compared as two backslashes), affecting DML literals, JSON inline path literals (types.py), and DDLCOMMENTclauses (table/column comments, Alembic column comments). Verified empirically on live CUBRID 11.2 and against the official docs (defaultno_backslash_escapes=yes), and consistent with sibling driver pycubrid, which negotiates this per-connection. The compiler now preserves backslashes by default. A newCubridDialect(no_backslash_escapes=False)option restores the legacy doubling for the rare server explicitly configured withno_backslash_escapes=no(backslash-as-escape); it is a static dialect option becauseliteral_bindscompilation may run offline with no live connection. Migration note: databases written by an affected version may already contain unintended doubled backslashes in literal-rendered data — audit such rows if you relied on inline literals. Added compiler, JSON, DDL-comment, and live-roundtrip regression tests. - Object-detail reflection now raises
NoSuchTableErrorfor a non-default schema (#291) — the shared_schema_is_default()guard was only applied to list/existence methods (get_table_names,get_view_names,has_table,has_index), so object-detail methods (get_columns,get_pk_constraint,get_foreign_keys,get_indexes,get_unique_constraints,get_view_definition,get_table_comment) silently ignoredschema=and returned metadata from the default schema — masking the fact that CUBRID exposes a single effective schema per connection. Those seven methods now call a new `_raise_if_non_default...
v1.6.0 — Reflection Hardening & SA 2.2 Compat
Summary
v1.6.0 ships reflection hardening and SQLAlchemy 2.2 forward-compatibility from Sprint 2.
Changes
Reflection Hardening (#120, PR #236)
- Fixed latent PK constraint name bug:
get_pk_constraint()queried the non-existentdb_constraintsystem view (verified against CUBRID 10.1/11.2/11.4 official manuals — no such view exists). PK constraint names were alwaysNonein production. Now targets_db_index(is_primary_key = 1), the authoritative system catalog. - Hardened UNIQUE constraint reflection:
get_unique_constraints()now queries_db_index(is_unique = 1, excluding PK/FK auto-indexes) and resolves column names viaSHOW INDEXESas the primary path. DDL regex retained as fallback. Matches the provenget_indexes()two-query pattern. - FK reflection: Extracted
_get_foreign_keys_from_ddl()as a standalone, testable helper. CUBRID system views expose no FK referenced-table/column metadata, so DDL parsing remains the sole FK reflection path.
SA 2.2 Compatibility (#231, PR #235)
bind_with_typeprivate API insulation: Addedtry/except AttributeErrorguard aroundelement._clone(). Fallback constructs a freshBindParameterif SA removes/renames_clone()in a future release.- SA 2.2 canary: CI job bumped to
sqlalchemy>=2.2.0b1withcontinue-on-error: true.
Oracle post-implementation review: PASS
Test Results
- 646 offline tests pass (was 641 before Sprint 2)
- 97% dialect.py coverage
- Integration tests pass across CUBRID 10.2–11.4 × Python 3.10–3.14
Ultraworked with Sisyphus
v1.5.1 — RETURNING CompileError, two-phase commit flag
Patch Release — Dialect Hardening
Fixed
- P1 #229: RETURNING now raises explicit
CompileError— the dialect previously silently fell back toLAST_INSERT_ID()for any.returning()call. Users now get an actionable error message pointing toresult.inserted_primary_key. - P2 #230: Two-phase commit explicitly disabled —
supports_twophase_commit = Falseadded toCubridDialect, andtwo_phase_transactionsis now a_CLOSEDrequirement flag so the SA test suite properly skips two-phase tests.
Full Changelog: v1.5.0...v1.5.1
v1.5.0 — SQLAlchemy 2.1/2.2 forward-compat shims, async stability
Highlights
Added
- SQLAlchemy 2.1 / forward-compat shims for SA 2.2 (#206) — dependency upper bound bumped to
<2.3(nowsqlalchemy>=2.0,<2.3), enabling installation on SA 2.1 and future 2.2 releases.- New
CubridCompiler.update_post_criteria_clauseoverride routes the existingcubrid_limitLIMIT rendering through the SA 2.1 hook that replacedupdate_limit_clause. _render_json_extract_from_binarynow recognisesFloatas a numeric affinity since SA 2.1 split it out ofNumeric, restoringCAST(... AS DOUBLE)emission forJSON[...].as_float().AsyncAdapt_pycubrid_connection.await_is redeclared as a class-level staticmethod because SA 2.1 dropped the inherited attribute onAsyncAdapt_dbapi_connection.- Cross-version offline test suite (639 tests) green on both SA 2.0.49 and SA 2.1.0b2.
- New
- `sqlalchemy-22-canary` CI job promoted to gating (#206) — previously `continue-on-error: true` against a non-existent `sqlalchemy>=2.2.0b1`. Now installs `--pre "sqlalchemy>=2.1.0b1,<2.3"` so the job actually exercises the latest available SA pre-release and fails the build on regressions.
Fixed
- Async integration stability for issue #208 — `test/test_aio_integration.py` now seeds per-test data instead of relying on module-shared CRUD state, adds live `pool_pre_ping=True` recovery coverage after an internal async transport drop, and verifies async SQLAlchemy INSERT returns `lastrowid`.
- bandit B110 cleared in `trace.py` — replaced `try/except/pass` cleanup with `_logger.debug("...", exc_info=exc)` so cleanup failures are observable without masking the original exception (#212).
Validated
- Native pycubrid ping causally validated — Tier 2 ORM benchmark in cubrid-benchmark `2026-04-22_native-ping-hotpath` (paired same-version A/B, 7 trials, bootstrap 95% CI) confirms the `do_ping()` native CHECK_CAS path delivers a practical pre-ping hot-path win: SQLAlchemy Core `checkout_select_by_pk` +108.2% throughput, ORM `session_select_by_pk` +42.1%, with p50/p95 latency also reduced.
Compatibility
Backward-compatible with 1.4.x. No public API removals or signature changes. Same driver options (`cubrid://`, `cubrid+pycubrid://`, `cubrid+aiopycubrid://`). Pairs with pycubrid 1.5.0 released alongside.
Install
```bash
pip install --upgrade sqlalchemy-cubrid
or with the pure Python driver:
pip install --upgrade "sqlalchemy-cubrid[pycubrid]"
```
Full changelog
https://github.com/cubrid-lab/sqlalchemy-cubrid/blob/v1.5.0/CHANGELOG.md
v1.4.3
What's New in v1.4.3
Added
visit_doublealias — forward compatibility with SQLAlchemy 2.1 (#206)- MERGE column resolution docs — column resolution rules and error reference (#207)
- Collection member split tests —
_split_collection_membersunit tests with depth guard (#204)
Fixed
- Paren-depth-aware collection member split — reflection now correctly handles nested types like
NUMERIC(15,2)inside collections (#204) - Collection member type params preserved — compilation and reflection retain precision/scale (#194)
- Oracle review fixes — type args, timezone semantics, regression tests (#203)
Full Changelog: v1.4.2...v1.4.3
v1.4.2
v1.4.1
v1.4.0
[1.4.0] - 2026-04-20
Added
- SQLAlchemy 2.2 compatibility shim —
sqlalchemy_cubrid/_compat.pyinsulates compiler from SA private API changes (is_literal_value,bind_with_type,for_update_arg,limit_clause,offset_clause).bind_with_typenow preservesexpanding/literal_execute/isoutparamflags;is_literal_valuehandlesvisitors.Visitableinstances (Oracle post-review fixes) (#142) - Alembic safety checklist + advisory CLI —
docs/ALEMBIC.mdadds Pre-Migration Checklist, Pre-Deploy Sequence, and Rollback Template;scripts/alembic_safety_check.pyprovides advisory detection for non-transactional DDL risks (#144) - Compiler benchmark baseline —
scripts/bench_compile.pyper-construct timing baseline. Baselines: SELECT+LIMIT ~178µs, INSERT ~129µs, INSERT ON DUPLICATE KEY UPDATE ~234µs (1.8× simple INSERT due toreplacement_traverseoverhead), SELECT FOR UPDATE ~153µs (#145) - QueuePool concurrency stress tests — 6 tests covering sync concurrent checkouts within
pool_size, overflow burst absorption with barrier sync,pool_timeoutexhaustion,pool_recycleaged-connection replacement, asyncgatherwithinpool_size, async overflow burst
Fixed
- pycubrid dependency pin —
pycubrid>=1.2.0,<2.0(was missing upper bound) (#143) - F401 lint regression — removed unused
CubridDialectimport intest/test_logging.py
Deferred
- SA 2.2 compatibility — remains pinned to
<2.2per existing limitation; the compat shim prepares the codebase for the future bump but does not lift the pin