Skip to content

v1.7.1

Choose a tag to compare

@github-actions github-actions released this 18 Sep 11:20
· 106 commits to main since this release
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 _db_index / _db_index_key system catalog, with a SHOW COLUMNS fallback if the catalog query fails. Fixes reflected multi-column-PK ORM mappings and Alembic autogenerate. Found by the new reflection round-trip fuzzer.
  • executemany INSERT into a column with a bind_expression() CAST no longer sends the wrong parameter count (#421) — SQLAlchemy's insertmanyvalues row-expansion miscounts bind parameters when a target column's type wraps its bind in a bind_expression (e.g. a TypeDecorator rendering CAST(? AS VARCHAR(50))): it emitted 4 placeholders but only 3 parameters (INSERT ... VALUES (?, CAST(? AS ...)), (?, CAST(? AS ...)) with (2, 3, 2)), so the driver raised "wrong number of parameters". The dialect now drops the insertmanyvalues plan (in visit_insert) for INSERTs targeting such a column, falling back to ordinary DBAPI executemany; single-row inserts and the normal multi-row fast path (columns without a bind_expression) are untouched. Fixes the compliance suite's CastTypeDecoratorTest::test_special_type.
  • Identity() columns now emit AUTO_INCREMENT, and DATETIME/TIME microsecond limits are declared to the test suite (#388) — SQLAlchemy stores an Identity() as column.server_default, which made the DDL compiler skip its AUTO_INCREMENT branch and emit a plain INTEGER NOT NULL column, so inserts failed with Missing value for attribute 'id'. Identity() is now treated as CUBRID's AUTO_INCREMENT (its spelling of an identity/autoincrement column). Also declared datetime_microseconds/time_microseconds as unsupported in requirements.py (CUBRID DATETIME stores millisecond precision only and TIME has no fractional seconds — verified live on 11.4), so the SQLAlchemy compliance suite skips those tests via its own machinery instead of baselining them as xfail. Remaining #388 triage entries (DistinctOnTest, ReturningGuardsTest, C-driver-specific RowCountTest) are kept as documented xfails with precise root-cause comments; the genuine CastTypeDecoratorTest insertmanyvalues parameter-count bug is split out to #421.
  • Documented that the legacy CUBRIDdb C-extension driver loses NUMERIC/DECIMAL fractional precision (#386) — the C-extension driver (bare cubrid://) truncates the fractional part of NUMERIC/DECIMAL values below the DBAPI layer, returning e.g. 15 for a stored 15.7563. Because the data is already lost before SQLAlchemy sees it, no dialect result processor can recover it — this is an upstream C-driver limitation, not a dialect bug. The pure-Python cubrid+pycubrid:// driver returns Decimal correctly (verified live on 11.4) and is the recommended driver. The affected NumericTest nodes stay baselined in test/known_failures.txt with this explanation, and docs/DRIVER_COMPAT.md / docs/TYPES.md now document the fidelity difference between the two drivers.
  • ON DUPLICATE KEY UPDATE referencing stmt.inserted.col now fails clearly for inline multi-row VALUES instead of a confusing bind error (#371) — CUBRID has no VALUES(col) / row-alias syntax, so the dialect re-emits the INSERT bind parameter to reference the inserted value. That works for a single-row INSERT and for executemany (each execution is logically single-row), but for an inline multi-row insert(t).values([{...}, {...}]) there is no single value to bind per conflicting row — the previous code raised a misleading cannot resolve INSERT bind parameter ... Ensure the column is included in the INSERT values (the column was included). It now raises a clear CompileError naming the CUBRID limitation and pointing to executemany, single-row statements, a literal/expression update, or MERGE. Nested references (e.g. func.coalesce(stmt.inserted.col, ...)) are caught too; literal/expression updates that do not reference the inserted value keep compiling for multi-row inserts. Refusing to compile avoids the silent-corruption trap of binding one row's value for every conflicting row.
  • OFFSET without LIMIT no longer silently caps result sets at ~1.07B rows (#414) — a bare .offset(n) compiled to LIMIT n, 1073741823, reusing CUBRID's VARCHAR-length constant (2^30-1) as the row-count sentinel. That is a string-length bound, not a legal upper bound for a LIMIT row count, so a query with an offset and no limit was silently truncated at 1,073,741,823 rows (wrong results, no error) on tables past that size. The sentinel is now 2^62 (4611686018427387904) — effectively unbounded (~4.6×10^18 rows). It is deliberately not the signed BIGINT maximum (2^63-1): CUBRID computes offset + row_count internally and overflow-checks it, so a BIGINT-max sentinel raises ERROR -458 (Overflow occurred in addition context) for any positive offset (caught by the SQLAlchemy compliance suite's FetchLimitOffsetTest). 2^62 leaves ~4.6×10^18 rows of offset headroom before the sum can overflow. Named the value _CUBRID_OFFSET_NO_LIMIT_ROW_COUNT in compiler.py, and the regression test now asserts on the constant (and that the old 2^30-1 cap is gone) instead of the bare literal.
  • Numeric bind parameters keep their scale in arithmetic, and FROM-less SELECT ... WHERE compiles (#386) — CUBRID coerces a bound parameter in NUMERIC(p,s) + ? arithmetic to an integer, silently dropping the fractional scale (a SQL literal or an explicit CAST keeps it). Scaled NUMERIC/DECIMAL binds now render CAST(? AS NUMERIC(p,s)) so the scale is preserved; unconstrained Numeric() binds are left untouched (CUBRID's bare NUMERIC is scale 0 — specify precision/scale for exact decimals). Also added a default_from() of FROM db_root (CUBRID's DUAL equivalent) so a FROM-less SELECT carrying a WHERE clause no longer raises a syntax error.
  • supports_is_distinct_from / supports_native_enum now report their real capabilities (#381, #382) — both flags were False, which made SQLAlchemy's official suite skip IsOrIsNotDistinctFromTest and the native-enum tests even though both features are fully implemented (IS DISTINCT FROM emulation via <=> from #345/#377; native ENUM(...) DDL). Flipped both to True; the previously-skipped suite tests now run and pass live on CUBRID 11.4 (5 IS DISTINCT FROM + native-enum cases, zero regressions). Also corrected the ENUM docstring, which wrongly claimed native_enum=False is "ignored" — it correctly emits VARCHAR(n) per the SQLAlchemy contract (verified live).
  • SQLAlchemy dialect compliance suite now gates CI (#380) — the test_suite.py step ran with a trailing || true and, worse, had no [sqla_testing] config, so the official SQLAlchemy suite crashed at session start (configparser.NoSectionError) and CI silently ignored it — the suite had never actually run. Added setup.cfg with the required [sqla_testing] section, removed || true, and gated the suite on the representative Python 3.14 × CUBRID 11.4 cell. Added has_temp_table/temp_table_reflection requirement exclusions in requirements.py (CUBRID has no temp tables), which eliminated 732 setup errors. The remaining genuine failures are pinned as a strict-xfail baseline in test/known_failures.txt (loaded by conftest.py): any new failure fails CI, and fixing a baselined test forces its removal from the manifest. The baseline is SQLAlchemy-version-specific (suite parametrization changes between releases), so the gating cell pins sqlalchemy==2.0.53 and conftest.py fails the gating run (CUBRID_STRICT_KNOWN_FAILURES=1) if any manifest entry no longer matches a collected test. Follow-up bug trackers: #385 (view DDL), #386 (Numeric/Decimal), #387 (reflection), #388 (misc).
  • IS DISTINCT FROM live execution fixed (#377) — live CUBRID rejects MySQL-style NOT (a <=> b) when the expression appears in a SELECT projection. The emulation now negates the NULL-safe <=> result as (a <=> b) = 0, which preserves the four-row SQL truth table and is valid CUBRID syntax. Added live truth-table coverage for both IS DISTINCT FROM and IS NOT DISTINCT FROM.
  • create-release.yml: dropped --target from gh release create — with an already-pushed tag (the normal tag-push trigger) --verify-tag already guarantees the tag exists, and passing target_commitish for an existing tag makes the Releases API return 422 Validation Failed, so the first tag-triggered run of this workflow always failed. Verified live by the v0.4.0 tag attempt in cubrid-mcp-server.

Docs

  • Demo GIF embedded in README — auto-generated terminal demo showing create_engine → connect → execute → result.
  • Korean/multi-language docs governance — every docs/README.<lang>.md translation now carries a sync marker, and docs-sync gained a translation-sync job that fails a PR when README.md changes without any translation changing (escape hatch: the translations-deferred label).
  • 한국어 문서 페이지 — 배치 5 완결 (#341) — TROUBLESHOOTING(1,325줄) 번역으로 14페이지 전체 완성.
  • 한국어 문서 페이지 — 배치 4 (#341) — FEATURE_SUPPORT(기능 비교)·DEVELOPMENT(개발 가이드) 번역 추가. TROUBLESHOOTING만 남음.
  • 한국어 문서 페이지 — 배치 3 (#341) — ALEMBIC(마이그레이션 가이드) 번역 추가. FEATURE·TROUBLE·DEV만 남음.
  • 한국어 문서 페이지 — 배치 2 완결 (#341) — ORM_COOKBOOK·DML_EXTENSIONS 번역으로 Usage 축 완성. ALEMBIC·FEATURE·TROUBLE·DEV는 후속 배치.
  • 한국어 문서 페이지 — 배치 2 (Usage/Reference 1차, #341) — TYPES·ARCHITECTURE 번역 추가. ORM_COOKBOOK·DML_EXTENSIONS·ALEMBIC·FEATURE·TROUBLE·DEV는 후속.
  • 한국어 문서 페이지 — 배치 1 (Getting Started/Ref/Ops 축, #341) — QUICKSTART·CONNECTION·DRIVER_COMPAT·ISOLATION_LEVELS·SUPPORT_MATRIX·PERFORMANCE 6페이지 번역을 docs/ko/에 추가. Usage 축(ORM·DML·TYPES·ALEMBIC)과 FEATURE/ARCH/TROUBLE/DEV는 후속 배치.
  • Docs site information architecture unified across the ecosystem — nav reorganized to the shared six-tab skeleton (Home / Getting Started / Usage / Reference / Operations / Project), the five README translations (ko/de/hi/ru/zh) are now reachable via Project → Translations (previously URL-only), palette unified to blue with search-suggest, and the homepage gains an Ecosystem section linking the three sibling sites.
  • CUBRID-Python BSD basis documented, server-license line added, NOTICE created, copyright unified (#333) — THIRD_PARTY_LICENSES.md now records that the optional CUBRID-Python extra's BSD claim rests on PyPI metadata and setup.py (the upstream repository ships no LICENSE file or headers; 2- vs 3-Clause unspecified), and carries the verified CUBRID server licensing statement (engine Apache-2.0, APIs/connectors BSD per upstream COPYING — GPL v2+ is outdated). Added a two-line NOTICE (independent implementation, no third-party code). LICENSE copyright unified to Yeongseon Choe, Gyeongjun Paik (2021-2026).

Docs

  • Added THIRD_PARTY_LICENSES.md and a Provenance section in docs/ARCHITECTURE.md — the license inventory covers the default runtime tree (SQLAlchemy, greenlet, typing_extensions) and the optional [alembic]/[cubrid] extras; the provenance note states that this dialect is an independent implementation, not a port of the legacy CUBRID-Python-bundled dialect. Documentation only.