Skip to content

v1.7.0

Choose a tag to compare

@yeongseon yeongseon released this 02 Sep 04:40
· 175 commits to main since this release
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_schema() helper that raises NoSuchTableError("<schema>.<object>") when schema= is not the default, matching SQLite's behaviour and preventing a real "object not found" from being hidden behind empty metadata. List/existence methods keep returning empty/false via _schema_is_default().
  • Schema-default comparison is now case-insensitive (#292) — _schema_is_default() compared schema == self.default_schema_name with a plain, case-sensitive ==. CUBRID reports catalog names uppercased (DBA) while SQLAlchemy normalizes to lower case, so MetaData.reflect(schema="dba") (or any case-mismatched schema argument) failed the guard and was treated as a foreign schema — yielding no reflection or a spurious NoSuchTableError. The guard now normalizes both sides via self.normalize_name() (the dialect's own identifier rules), so "dba", "DBA", and the reported default all compare equal. Names the user explicitly quoted (quoted_name with quote=True) are still compared case-sensitively, honouring the intent to preserve case.
  • Schema reflection is now internally consistent (#280) — get_schema_names() previously returned [] with the docstring "CUBRID does not support schemas", directly contradicting _get_default_schema_name() (which returns a real schema via SELECT SCHEMA()), and the schema= argument was handled differently per method: get_table_names returned [] for a non-default schema while get_view_names/has_table/has_index ignored the argument entirely — so MetaData.reflect(schema=<x>) produced a contradictory "0 tables + all views". The dialect now operates in a consistent single-schema mode: get_schema_names() returns [default_schema_name], and a shared _schema_is_default() guard makes get_table_names, get_view_names, has_table, and has_index honour schema= uniformly (the default schema is reflected; any other schema yields nothing). Owner-qualified cross-schema reflection is intentionally not attempted. Verified against live CUBRID 11.2.
  • Isolation-level set → get round-trip is now symmetric (#281) — get_isolation_level() previously returned only the long granular spelling per code (setting "REPEATABLE READ" came back as "REPEATABLE READ SCHEMA, REPEATABLE READ INSTANCES"; "READ COMMITTED" / "CURSOR STABILITY" came back as the long form), so the short standard names a user passes in were never returned — a mismatch SQLAlchemy compares on pool return/reset. _ISOLATION_LEVEL_REVERSE now returns one canonical name per integer code: the short standard names for READ COMMITTED (4), REPEATABLE READ (5), and SERIALIZABLE (6), and the granular spelling for levels 1–3 (which have no short name). Aliases still collapse onto their canonical code, so set → get round-trips to the same level for every accepted input name. reset_isolation_level() and the None-row fallback now use the canonical "READ COMMITTED". Added offline round-trip tests plus live CUBRID verification.
  • Obsolete pre-MVCC isolation levels (1–3) removed from the accepted set (#307) — the dialect's _ISOLATION_LEVEL_MAP and get_isolation_level_values() still advertised the four legacy granular levels that CUBRID's MVCC engine (10.0+) removed. On a modern server (verified against live CUBRID 11.2.9) SET TRANSACTION ISOLATION LEVEL 1|2|3 is rejected with "Isolation level value in MVCC must be 'read committed', 'repeatable read' or 'serializable'", so any name resolving to codes 1–3 could only ever fail at the server. Those three names (REPEATABLE READ SCHEMA, READ UNCOMMITTED INSTANCES, READ COMMITTED SCHEMA, READ COMMITTED INSTANCES, READ COMMITTED SCHEMA, READ UNCOMMITTED INSTANCES) now raise a clear client-side ValueError instead. The dialect keeps the three supported MVCC levels — READ COMMITTED (4), REPEATABLE READ (5), SERIALIZABLE (6) — plus the still-valid aliases (CURSOR STABILITY and the two long spellings that map to 4/5). _ISOLATION_LEVEL_REVERSE drops the dead 1–3 entries while get_isolation_level() stays tolerant of any unexpected server value via its string fallback. docs/ISOLATION_LEVELS.md rewritten to document the three MVCC levels and the historical removal.
  • Lint job red on main (#268) — #257 removed the only use of re in test/test_packaging.py but left import re behind, so ruff check sqlalchemy_cubrid/ test/ failed with F401 and took matrix-result down with it. Removed the orphaned import.

CI

  • Async integration jobs now install pycubrid via the declared [pycubrid] extra instead of an unpinned bare pip install pycubrid (#318) — ci.yml and integration-full.yml installed pycubrid for the async integration step with a bare pip install pycubrid, which ignored the project's declared support range ([pycubrid] extra = pycubrid>=1.3.2,<2.0) and could silently pull an out-of-range release (e.g. a future 2.0). Both steps now run pip install -e ".[pycubrid]", honoring the constraint (the redundant pytest-asyncio install was also dropped — it already ships in the [dev] extra installed earlier in the job). Clarified intent in-workflow: these jobs are release verification (validate against a supported released driver); cross-package testing against pycubrid@main (HEAD) remains the dedicated job in upstream-canary.yml. CI-only change; no runtime or packaging behavior change.
  • sqlalchemy-22-canary pin was unsatisfiable; retargeted to SA 2.1 and renamed (#312) — the canary installed --pre "sqlalchemy>=2.2.0b1,<2.3", but no 2.2.x release exists on PyPI (SQLAlchemy's next line is 2.1, latest stable 2.0.52), so the job failed at the install step on every PR and main and never actually ran. It now installs --pre "SQLAlchemy>=2.1.0b1,<2.3" and the job is renamed sqlalchemy-21-canary. It remains continue-on-error: true (non-gating). This reverts the incorrect #231 "bump" (which assumed 2.1.0b1 was no longer a pre-release) and restores the intent of #206.
  • upstream-canary now exercises the online/integration suite against pycubrid@main, and the Alembic autogenerate patch target is version-robust (#323) — despite #319, no CI path actually ran the integration tests against pycubrid git HEAD: ci.yml and integration-full.yml install the released [pycubrid] range, and upstream-canary.yml pinned HEAD but ran offline tests only. Cross-stack fixes landing in pycubrid main were therefore never exercised end-to-end. Added a second upstream-canary-integration job that spins up a CUBRID 11.4 service, installs pycubrid@main, and runs test_integration.py + test_aio_integration.py (both continue-on-error: true, non-gating). Separately, test/test_alembic.py and test/test_alembic_roundtrip.py patched alembic.autogenerate.compare.schema.inspect, but in alembic ≥1.14 compare is a module (not a package) with inspect bound at module level, raising ModuleNotFoundError (4 local failures). The patch target is now resolved at runtime against the installed alembic layout, and the alembic constraint is tightened to >=1.7,<2.0 to bound future API drift. CI/test-only change; no runtime or packaging behavior change.