Skip to content

Feature: PEP 249 and SQLAlchemy compatibility enhancements#159

Open
danhogue wants to merge 16 commits into
long2ice:devfrom
turquoisehealth:pep249
Open

Feature: PEP 249 and SQLAlchemy compatibility enhancements#159
danhogue wants to merge 16 commits into
long2ice:devfrom
turquoisehealth:pep249

Conversation

@danhogue

@danhogue danhogue commented May 6, 2026

Copy link
Copy Markdown

Summary

Implements PEP 249 (Python Database API Specification v2.0) compliance and adds SQLAlchemy Core/ORM compatibility. Also fixes a couple of bugs found along the way and adds dev tooling infra.

These update were needed by our team at Turquoise.health where we have a FastAPI + SQLAlchemy Core application that needs a native async TCP ClickHouse connection for performance at scale. The clickhouse-driver, clickhouse-connect and asynch libraries appear to be the most common ways to connect to ClickHouse in Python codebases, but we found that clickhouse-driver did not support async connections, clickhouse-connect only supported simulated async via a threadpool (but also does not support native TCP) and while asynch promised us both, it initially broke due to incomplete PEP 249 / SQLAlchemy Core compatibility.

The updates in this branch are currently running in production and have increased our throughput by ~67% and our request latency by ~85%. These numbers are from load tests benchmarked against a threadpool based simulated async connection we built around the clickhouse-driver library.

Specific changes overview

  • PEP 249 compliance

    • Module globals: apilevel = "2.0", threadsafety = 1, paramstyle = "pyformat"
    • Module-level connect() factory function
    • All PEP 249 exceptions re-exported at the top level
    • New asynch/dbapi_types.py: type singletons (STRING, NUMBER, DATETIME, BINARY, ROWID) and constructors (Date, Time, Timestamp, DateFromTicks, etc.), plus a ClickHouse → DB-API type map (handles Nullable/LowCardinality)
    • Cursor: public arraysize property, setoutputsize() (setoutputsizes kept as deprecated alias), lastrowid (None), callproc() (raises NotSupportedError), async nextset() (None), fetchmany(size=None) default
    • cursor.description now returns PEP 249 type objects and None for non-SELECT statements
    • connection.commit() changed from raising NotSupportedError to a no-op (ClickHouse has no transactions)
    • Cleaner __repr__ output for Cursor/Connection status
  • SQLAlchemy compatibility

    • Verified DB-API surface works with SQLAlchemy Core and ORM
    • New test suite (tests/sqlalchemy/), skips gracefully if clickhouse-sqlalchemy isn't installed
    • README docs + example for compiling SQLAlchemy constructs and executing via an asynch cursor
  • Bug fixes discovered during development

    • asynch/proto/utils/dsn.py / asynch/proto/connection.py: DSN scheme validation was comparing enum objects instead of .value, breaking URL parsing (regression from 23da5d6, Oct 2024)
    • Connection pool: recreate asyncio primitives and discard stale connections when the running event loop changes, avoiding cross-loop RuntimeErrors
  • Dev/test infra

    • docker-compose.yml + ClickHouse config for local test runs
    • Makefile targets: test-pep249, test-sqlalchemy, test-compat
    • docs/pep249_requirements.md and docs/pep249_implementation_plan.md (spec reference + gap analysis used to drive the TDD suites)
    • docs/local_clickhouse_development.md, docs/pep249_sqlalchemy_support.md
    • CHANGELOG.md entry for unreleased 0.4.0

Testing

  • make test-pep249 — new PEP 249 compliance suite passes
  • make test-sqlalchemy — SQLAlchemy Core/ORM compatibility suite passes
  • make test — full existing suite still passes
  • Manual smoke test: asynch.connect(...), run a query, confirm description/lastrowid/arraysize behave per spec

danhogue and others added 14 commits May 5, 2026 10:10
- docs/pep249_requirements.md: full PEP 249 spec reference including
  module globals, exception hierarchy, type objects, connection/cursor
  interface, and SQLAlchemy-specific requirements beyond base PEP 249
- docs/pep249_implementation_plan.md: gap analysis against the current
  asynch codebase with prioritised implementation suggestions for each
  missing or incorrect item (apilevel/threadsafety/paramstyle, connect(),
  type singletons, arraysize, setoutputsize, lastrowid, callproc,
  nextset, description type_code, commit no-op)
- tests/pep249/: TDD suite covering every PEP 249 requirement
  (module globals, exceptions, type objects, connection lifecycle,
  cursor attributes/methods, execute/executemany, fetch semantics,
  description format and type_code mapping)
- tests/sqlalchemy/: compatibility suite verifying asynch provides the
  exact DB-API interface SQLAlchemy needs; Core and ORM tests skip
  gracefully when clickhouse-sqlalchemy is not installed
- Makefile: add test-pep249, test-sqlalchemy, test-compat targets that
  show all failures at once (suitable for TDD workflow)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The DSN parser was using enum objects directly in string sets instead of their
values, causing scheme validation failures when enums were used in f-strings.

Changes:
- asynch/proto/utils/dsn.py: Use .value for enum string sets and comparisons
- asynch/proto/connection.py: Fix alt_hosts URL construction with enum.value
- tests/test_proto/utils/test_dsn.py: Fix test DSN construction to use .value

The bug was introduced in commit 23da5d6 (Oct 2024) when enum imports were
updated but the string sets weren't updated to use .value.

Fixes test failures in DSN parsing and ensures proper URL scheme validation.
Reduce max_threads test from 1234 to 4 to work with 2-core Docker container.
ClickHouse auto-caps threads based on available cores, causing test failures
with unrealistic values. Test functionality remains unchanged.
- Add module globals: apilevel=2.0, threadsafety=1, paramstyle=pyformat
- Implement module-level connect() factory function with full parameter support
- Export all PEP 249 exceptions (Warning, Error, DatabaseError, etc.)
- Create asynch/dbapi_types.py with comprehensive type system:
  - Type objects: STRING, NUMBER, DATETIME, BINARY, ROWID
  - Type constructors: Date, Time, Timestamp, DateFromTicks, etc.
  - ClickHouse to PEP 249 type mapping with Nullable/LowCardinality support
- Add missing cursor methods and properties:
  - cursor.arraysize public property with getter/setter
  - cursor.setoutputsize() (renamed from setoutputsizes, kept alias)
  - cursor.lastrowid property (returns None for ClickHouse)
  - cursor.callproc() method (raises NotSupportedError per spec)
  - cursor.nextset() async method (returns None for single result sets)
- Fix cursor.description to use PEP 249 type objects and return None for non-SELECT
- Change connection.commit() from raising NotSupportedError to no-op behavior
- Fix cursor and connection __repr__ methods to show clean status strings
@danhogue
danhogue marked this pull request as draft May 6, 2026 19:23
Comment thread asynch/proto/utils/dsn.py
_SCHEME_SEPARATOR = "://"

_COMPRESSION_ALGORITHMS: set[str] = {
CompressionAlgorithm.lz4,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These .value additions fix a (probably latent for most people) bug discovered while working with the codebase. Current happy path should be unaffected.

@danhogue danhogue changed the title Pep249 Feature: PEP 249 and SQLAlchemy compatibility enhancements Jul 21, 2026
@danhogue
danhogue marked this pull request as ready for review July 21, 2026 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant