Skip to content

Releases: cubrid-lab/sqlalchemy-cubrid

v1.8.0

Choose a tag to compare

@github-actions github-actions released this 28 Sep 18:49
78bcbc7

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 upgrade now rolls back the whole run, including the alembic_version update. Set transaction_per_migration=True in context.configure() to keep per-revision commits (recommended for long or large-table migrations). Offline --sql scripts no longer contain BEGIN;; run them with csql --no-auto-commit --no-single-line so a failing statement stops the script.
  • Alembic registration (#504). CubridImpl is registered when the dialect loads, so a default env.py works without importing sqlalchemy_cubrid.alembic_impl. The [alembic] and [dev] extras now require alembic>=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 detects Text ↔ String(n) changes (#544). Review the first Alembic autogenerate diff after upgrading.
  • DDL and types. UnicodeText and TEXT compile to STRING (#534); BINARY(n) / VARBINARY(n) compile to BIT(n*8) / BIT VARYING(n*8) and UUID to CHAR(32) (#545); Index.drop() and op.drop_index() emit DROP INDEX <name> ON <table>, so op.drop_index() needs table_name (#533); if_exists=True on index drops and if_not_exists=True on index creates raise CompileError (#533, #540), as does an explicit String(0) (#440); Boolean is_() / is_not() against a value render with <=> (#465); JSON as_numeric(p, s) returns Decimal (#535).
  • Transactions. isolation_level="AUTOCOMMIT" works on every driver, invalid levels raise ArgumentError, and dialect.on_connect() no longer applies isolation_level (a hand-built pool must apply it) (#501). Engine- and connection-level isolation levels survive commit() / rollback() on pycubrid (#505). On cubrid://, executemany runs each parameter set separately: None values and rowcounts are correct, large batches are slower (#502).
  • Async. driver_connection on cubrid+aiopycubrid:// is now the pycubrid.aio connection; use dbapi_connection for the SQLAlchemy adapter (#520). Async LargeBinary / BLOB binding works (#500).
  • Removed the unreachable SQLAlchemy 1.x hooks should_autocommit_text(), AUTOCOMMIT_REGEXP and the dbapi() classmethods (#462).
  • pycubrid 1.8.0 is recommended. It raises IntegrityError for NOT NULL and foreign-key violations, raises instead of silently returning a truncated result after commit() / rollback(), reports correct null_ok and collection type codes in cursor.description, and keeps the CAS session across commits and rollbacks except when the CAS itself is restarted (docs/DRIVER_COMPAT.md Known Issue 10); the pycubrid contract tests require it (#480, #481, #482). The declared range stays pycubrid>=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.yml keeps its canary jobs non-blocking (continue-on-error, schedule and manual dispatch only), so a failing run against pycubrid@main used to pass silently. A new report job 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 the ci label, 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 resolves pycubrid@main with git ls-remote, installs that exact commit and exposes it and its own status as job outputs, because job-level continue-on-error hides failures from needs.<job>.result. The workflow defaults to contents: read; only the report job has issues: write, and it uses the gh CLI with GITHUB_TOKEN (no third-party action), runs one at a time (concurrency), and has no continue-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 existing integration-tests cells (no new job or CUBRID service): lane pycubrid@sa2.1 (pycubrid 1.7.1, SQLAlchemy 2.1.1) on Python 3.14 × CUBRID 11.4 and lane pycubrid@sa2.0 (pycubrid 1.7.1, SQLAlchemy 2.0.53) on Python 3.10 × CUBRID 10.2, both pinned and both recording versions via scripts/report_driver_versions.py; the CUBRIDdb lane (cubrid@sa2.0, CUBRID 11.4) is unchanged. test/known_failures.txt is 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. With CUBRID_STRICT_KNOWN_FAILURES=1 a 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 offline test/test_known_failures.py validates the manifest format. docs/DEVELOPMENT.md and docs/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.py with pycubrid and CUBRIDdb and CUBRID_REQUIRE_DRIVER_DIFFERENTIAL=1; with that variable set, test/conftest.py fails 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.py records the exact Python, SQLAlchemy, pycubrid, CUBRIDdb and CUBRID server versions in the job log and the GitHub step summary. New differential cases cover Core executemany with integer, UTF-8/CJK and NULL values, textual executemany with 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.md now states that a specific pycubrid release candidate must pass the downstream contract suite before the pycubrid>=1.3.2,<2.0 bound is widened, while the weekly pycubrid@main canary 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 and cubrid+pycubrid:// lanes) and test_aio_integration.py (cubrid+aiopycubrid://) now round-trip LargeBinary, BLOB, CLOB and Text through Core and ORM with small, Unicode/CJK, NULL and 256 KiB payloads (larger than pycubrid's ~80 KB LOB_READ chunk), and assert the returned value is bytes/str, never a driver LOB handle. Verified on CUBRID 10.2 and 11.4: writes store the full value and NULL/Text round-trip, but non-NULL BLOB/CLOB reads return a driver LOB locator (pycubrid dict handle, CUBRIDdb 'file:...' string), and async LargeBinary/BLOB binding fails because the async DB-API adapter lacks Binary. Those cases are strict per-driver xfails; docs/TYPES.md, docs/DRIVER_COMPAT.md and docs/SUPPORT_MATRIX.md document the current behavior (reflection is unaffected), and the CLOB type row no longer lists sqlalchemy.Text, which compiles to STRING. No dialect behavior changes.
  • Live tests that results are never silently truncated across commit or rollback (#481) — test_integration.py (CUBRIDdb and cubrid+pycubrid:// lanes) and test_aio_integration.py read a 500-row, 1000-byte-per-row result (more than pycubrid's 100-row FETCH batch; the broker's first response holds ~16 rows) with fetchone(), fetchmany() and fetchall() after commit(), rollback() or no boundary, and require every row or a DB-API error, never a successful partial Result. 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 raises InterfaceError after rollback; pycubrid main raises InterfaceError; 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 via test/pycubrid_upstream.py (no-op under CUBRID_PYCUBRID_UPSTREAM=1, set by the pycubrid@main integration canary; see docs/DEVELOPMENT.md). Async results pass on every driver because AsyncConnection.execute() buffers the whole result. docs/DRIVER_COMPAT.md documents the per-driver behavior. Tests, CI and docs...
Read more

v1.7.1

Choose a tag to compare

@github-actions github-actions released this 18 Sep 11:20
68f61bb

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://, async cubrid+aiopycubrid://) in the package docstring, the ON DUPLICATE KEY UPDATE construct on insert(), and reflection via inspect() in the dialect module docstring (REPLACE and MERGE already 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 for sa.Uuid() (Python uuid.UUID values) and sa.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 as CHAR(32), and these 8 integration-marked tests catch any regression in that CHAR-backed storage or the value coercion (verified: sa.Uuid() round-trips as uuid.UUID, as_uuid=False as str, WHERE/UPDATE by UUID work, and reflection reports CHAR). Wired into the nightly integration-full bug-hunt job.
  • Mutation testing for compiler.py (stabilization Phase 8) — added a [mutmut] configuration in setup.cfg (pinned mutmut>=2.5,<3 in the dev extra) and a non-gating nightly mutation-testing job in integration-full. Mutation testing checks whether the offline suite actually verifies compiler logic rather than merely executing it: it mutates compiler.py and 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 sql while letting the surrounding SQL structure mutate freely. Strengthening those to exact-match assertions on the full compiled output (JSON path extraction by type, LIMIT/OFFSET operand order and the offset-without-limit sentinel form, UPDATE ... LIMIT, and GROUP_CONCAT) raised the score to 64.1% (288/449). The job is continue-on-error because 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, selectinload eager, joinedload eager, many-to-many, back_populates navigation), complex queries (join+filter, GROUP BY/HAVING, aggregate scalars, correlated subquery, LIMIT/OFFSET pagination, LEFT OUTER JOIN), and transactions/mutations (rollback discards changes, commit persists across sessions, bulk UPDATE, cascade delete of orphaned orders, auto-increment PK populated after flush()). These combinations surface dialect bugs the single-construct unit tests miss. All 16 tests are integration-marked and run in the nightly integration-full bug-hunt job across every supported CUBRID version.
  • CUBRID version-differential snapshot testing (stabilization Phase 6) — added test/version_differential/snapshot.py and test/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 own get_columns() reflection of a fixed battery of column types. The nightly integration-full matrix now generates one snapshot per supported CUBRID version (10.2/11.0/11.2/11.4) and a new version-differential job compares them, failing if any dialect-visible behavior diverges across versions without being declared in KNOWN_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 a supports_* flag and reality is caught immediately instead of surfacing as a confusing downstream error. Added test/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 are integration-marked and run in the nightly integration-full bug-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, and test/test_metamorphic.py. The fuzzers generate SELECT/INSERT combinations and random tables the hand-written suite never enumerated (boundary values 0, ±1, 2^31, 2^63-1, empty/Unicode/reserved-word/backslash strings) and assert dialect invariants: compilation raises only CompileError (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 in conftest.py; PR CI runs the fast profile, and the nightly integration-full workflow runs the 2000-examples profile against live CUBRID. This bug-hunt found and fixed #426.

Fixed

  • Removed the dead implicit_returning = False dialect attribute (#396) — this was a SQLAlchemy 1.x-era switch with a comment claiming it prevents a ResourceClosedError on server-default INSERTs. In SQLAlchemy 2.x the CRUD compiler decides implicit INSERT ... RETURNING from insert_returning (already False), not implicit_returning, and DefaultDialect no longer even defines the attribute. Verified live on CUBRID 11.4 that a server-default INSERT + ORM refresh behaves identically with the attribute removed (no ResourceClosedError), and added an offline regression test asserting insert_returning/update_returning/delete_returning are the real switches and that the dead attribute stays gone.
  • docs/TROUBLESHOOTING.md LIMIT/OFFSET section no longer documents SQL the dialect never emits (#416) — the section claimed the dialect generates LIMIT n OFFSET m and that CUBRID rejects the comma form; both were wrong. The dialect only ever emits CUBRID's comma form LIMIT offset, count (offset first), and offset-without-limit uses a sentinel count. select(users).limit(10).offset(20) compiles to LIMIT 20, 10, not LIMIT 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.txt regenerated 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-#356 VALUES() ODKU pattern). It is regenerated from source via scripts/generate_llms_full.py, and the lint CI job now fails if llms-full.txt drifts 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's ComponentReflectionTest exposed. (1) get_columns() never returned column comments because the _db_attribute catalog query filtered on class_name instead of class_of.class_name (a semantic error that was silently swallowed), so every reflected comment was None; it now uses the correct catalog column. (2) Reflecting a non-existent table via get_columns() / get_indexes() leaked the raw driver ProgrammingError (errno -493, SQLSTATE 42S02) instead of SQLAlchemy's NoSuchTableError, 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 filters is_system_class = 'NO', matching get_table_names(). Together these resolve 22 of the 25 baselined ComponentReflectionTest nodes. Also set requires_name_normalize = False: CUBRID folds identifiers to lower case, not the upper case SQLAlchemy's denormalized_names requirement assumes, so NormalizedNameTest is now correctly skipped rather than failing (schema case-insensitive matching is unaffected). The remaining 3 ComponentReflectionTest nodes (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 with PRIMARY KEY (a, b) returned only constrained_columns: ['a'], silently losing every PK column after the first (and giving no column order). Root cause: the reflection scanned SHOW COLUMNS for the PRI key flag, but CUBRID (MySQL-compatible) marks only the first column of a composite PK as PRI. The PK columns are now read, in key order, from the `...
Read more

v1.7.0

Choose a tag to compare

@yeongseon yeongseon released this 02 Sep 04:40
ba739c3

Docs

  • Documented that CUBRID's LIST collection type is a synonym for SEQUENCE — CUBRID accepts LIST(type) in DDL but normalizes it to SEQUENCE at parse time, so a LIST(INTEGER) column is stored and reflected as SEQUENCE OF INTEGER (verified on live CUBRID 11.2). Clarified in docs/TYPES.md and via a code comment in ischema_names why the dialect exposes only the canonical SEQUENCE type and deliberately omits a LIST type/reflection entry (a type compiling to LIST(...) would produce spurious Alembic autogenerate diffs against the reflected SEQUENCE(...)). 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 to set_isolation_level() (CUBRID accepts several aliases per numeric level). Added a note to docs/ISOLATION_LEVELS.md and the get_isolation_level() docstring. Behavior is unchanged; the reverse mapping was already correct.
  • Aligned the Documentation project URL with the README docs badge (#294) — pyproject.toml pointed Documentation at the repo tree (.../tree/main/docs) while the README badge pointed at the published site https://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 optional cubrid = ["CUBRID-Python"] extra (the legacy C-extension driver used by the bare cubrid:// URL) was declared in pyproject.toml but 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, and ROADMAP.md claimed support for a non-existent SQLAlchemy 2.2 line, contradicting docs/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 only 2.1.0b1/b2/b3 pre-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 an OSError in 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 / SQLSTATE 08S01) to the known disconnect codes alongside the existing -21003/-21005/-10005/-10007. Detection stays conservative to avoid false-positive pool invalidation: only the explicit raise ... from cause chain is followed (implicit __context__ is ignored, so an unrelated in-flight OSError does not invalidate a live connection), and a bare OperationalError/InterfaceError with no disconnect code, no OSError cause, 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 lacks OperationalError) and for pycubrid's client-side string-only errors (e.g. "connection lost during receive", which carries neither a code nor an OSError cause). Added offline regression tests covering the -4 code, explicit-cause OSError detection, implicit-__context__ non-detection, non-OSError-cause message fallback, and non-disconnect OperationalError/InterfaceError cases.
  • Ruff lint rule selection now declared explicitly (#271) — pyproject.toml configured ruff but never set [tool.ruff.lint] select, so ruff check inherited 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, via select = ["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 legacy CUBRIDdb C-extension driver (the driver bound to the bare cubrid:// URL) can now be selected unambiguously via the explicit cubrid+cubriddb:// URL, backed by a matching cubriddb = ["CUBRID-Python"] install extra. The bare cubrid:// URL continues to bind CUBRIDdb — no behavior change to any existing URL. For new projects the pure-Python pycubrid driver is the recommended choice: install sqlalchemy-cubrid[pycubrid] and use cubrid+pycubrid:// (installs with pip alone, no C toolchain).
  • Native Alembic ALTER COLUMN type changes and column renames (#305) — CubridImpl.alter_column() previously raised NotImplementedError for column type changes and renames, forcing every such migration through batch_alter_table (full table recreate). CUBRID in fact supports MySQL-compatible ALTER TABLE ... MODIFY, CHANGE, and RENAME COLUMN, so the dialect now emits native DDL: a type change compiles to MODIFY, a rename to RENAME COLUMN ... TO ..., and a combined rename + type change to a single CHANGE. Type conversions are governed by the server's alter_table_change_type_strict system parameter (incompatible/truncating conversions error when yes, may silently truncate when no); batch_alter_table remains 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 the is_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.py sync path) with no numeric error code and no OSError cause, 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_messages so the message fallback matches. Added an offline regression test asserting is_disconnect() returns True for a bare driver error carrying only that message.
  • SELECT SCHEMA() returning NULL no longer leaks the fake schema "None" (#290) — _get_default_schema_name() did str(connection.execute(text("SELECT SCHEMA()")).scalar()), so when SCHEMA() returned SQL NULL the Python None was stringified to the literal "None". Because get_schema_names() and _schema_is_default() test the value against the None object (not the string), that fake name leaked through as a real schema (get_schema_names() → ["None"]). _get_default_schema_name() now returns Optional[str] — None when SCHEMA() is NULL, otherwise the real name — so get_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 did rendered.replace("\\", "\\\\"), doubling every backslash in inline SQL literals. This is correct for MySQL but wrong for CUBRID, whose no_backslash_escapes system parameter defaults to yes — a backslash is a literal character, not an escape. On a default server this silently corrupted any backslash-bearing literal rendered via literal_binds=True (e.g. C:\temp was stored/compared as two backslashes), affecting DML literals, JSON inline path literals (types.py), and DDL COMMENT clauses (table/column comments, Alembic column comments). Verified empirically on live CUBRID 11.2 and against the official docs (default no_backslash_escapes=yes), and consistent with sibling driver pycubrid, which negotiates this per-connection. The compiler now preserves backslashes by default. A new CubridDialect(no_backslash_escapes=False) option restores the legacy doubling for the rare server explicitly configured with no_backslash_escapes=no (backslash-as-escape); it is a static dialect option because literal_binds compilation 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 NoSuchTableError for 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 ignored schema= 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...
Read more

v1.6.0 — Reflection Hardening & SA 2.2 Compat

Choose a tag to compare

@yeongseon yeongseon released this 18 Jul 12:19

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-existent db_constraint system view (verified against CUBRID 10.1/11.2/11.4 official manuals — no such view exists). PK constraint names were always None in 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 via SHOW INDEXES as the primary path. DDL regex retained as fallback. Matches the proven get_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_type private API insulation: Added try/except AttributeError guard around element._clone(). Fallback constructs a fresh BindParameter if SA removes/renames _clone() in a future release.
  • SA 2.2 canary: CI job bumped to sqlalchemy>=2.2.0b1 with continue-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

Choose a tag to compare

@yeongseon yeongseon released this 18 Jul 11:30
ab9b2a3

Patch Release — Dialect Hardening

Fixed

  • P1 #229: RETURNING now raises explicit CompileError — the dialect previously silently fell back to LAST_INSERT_ID() for any .returning() call. Users now get an actionable error message pointing to result.inserted_primary_key.
  • P2 #230: Two-phase commit explicitly disabled — supports_twophase_commit = False added to CubridDialect, and two_phase_transactions is now a _CLOSED requirement 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

Choose a tag to compare

@yeongseon yeongseon released this 23 May 14:24

Highlights

Added

  • SQLAlchemy 2.1 / forward-compat shims for SA 2.2 (#206) — dependency upper bound bumped to <2.3 (now sqlalchemy>=2.0,<2.3), enabling installation on SA 2.1 and future 2.2 releases.
    • New CubridCompiler.update_post_criteria_clause override routes the existing cubrid_limit LIMIT rendering through the SA 2.1 hook that replaced update_limit_clause.
    • _render_json_extract_from_binary now recognises Float as a numeric affinity since SA 2.1 split it out of Numeric, restoring CAST(... AS DOUBLE) emission for JSON[...].as_float().
    • AsyncAdapt_pycubrid_connection.await_ is redeclared as a class-level staticmethod because SA 2.1 dropped the inherited attribute on AsyncAdapt_dbapi_connection.
    • Cross-version offline test suite (639 tests) green on both SA 2.0.49 and SA 2.1.0b2.
  • `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

Choose a tag to compare

@yeongseon yeongseon released this 13 May 00:35

What's New in v1.4.3

Added

  • visit_double alias — forward compatibility with SQLAlchemy 2.1 (#206)
  • MERGE column resolution docs — column resolution rules and error reference (#207)
  • Collection member split tests — _split_collection_members unit 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

Choose a tag to compare

@yeongseon yeongseon released this 21 Apr 11:47

Full Changelog: v1.4.1...v1.4.2

v1.4.1

Choose a tag to compare

@yeongseon yeongseon released this 21 Apr 01:18

See CHANGELOG.md for the full 1.4.1 release notes. This Beta docs-only patch consolidates Oracle audit fixes, PRD/development docs alignment, and README translation sync across five languages.

v1.4.0

Choose a tag to compare

@yeongseon yeongseon released this 20 Apr 04:16

[1.4.0] - 2026-04-20

Added

  • SQLAlchemy 2.2 compatibility shim — sqlalchemy_cubrid/_compat.py insulates compiler from SA private API changes (is_literal_value, bind_with_type, for_update_arg, limit_clause, offset_clause). bind_with_type now preserves expanding/literal_execute/isoutparam flags; is_literal_value handles visitors.Visitable instances (Oracle post-review fixes) (#142)
  • Alembic safety checklist + advisory CLI — docs/ALEMBIC.md adds Pre-Migration Checklist, Pre-Deploy Sequence, and Rollback Template; scripts/alembic_safety_check.py provides advisory detection for non-transactional DDL risks (#144)
  • Compiler benchmark baseline — scripts/bench_compile.py per-construct timing baseline. Baselines: SELECT+LIMIT ~178µs, INSERT ~129µs, INSERT ON DUPLICATE KEY UPDATE ~234µs (1.8× simple INSERT due to replacement_traverse overhead), 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_timeout exhaustion, pool_recycle aged-connection replacement, async gather within pool_size, async overflow burst

Fixed

  • pycubrid dependency pin — pycubrid>=1.2.0,<2.0 (was missing upper bound) (#143)
  • F401 lint regression — removed unused CubridDialect import in test/test_logging.py

Deferred

  • SA 2.2 compatibility — remains pinned to <2.2 per existing limitation; the compat shim prepares the codebase for the future bump but does not lift the pin