Repository navigation
Major internal refactor: the protocol hot path is now compiled with Cython, modeled on the sister project asyncmy. The public API (Connection, Cursor, DictCursor, Pool, DSNs) is unchanged.
Upgrading: Python 3.11 or newer is required, and asynch now ships platform-specific binary wheels rather than a pure-Python one. Everything else is backwards compatible — pip install -U asynch is enough.
Performance
Measured against clickhouse-driver, the synchronous C-extension driver (500k rows, best of 3; reproduce with make benchmark):
| Scenario | asynch | clickhouse-driver |
|---|---|---|
| Export 500k rows, wide events table | 438 ms | 728 ms |
| 100 concurrent queries (pool of 10) | 2103 q/s | 1310 q/s |
DateTime decode |
13.8M rows/s | 2.3M rows/s |
String decode |
21.7M rows/s | 15.2M rows/s |
Small filtered queries, aggregations and batch inserts are server-bound and unchanged — both drivers sit at the wire limit.
New
JSONtype support (ClickHouse 24.8+): reads as nested dicts, accepts dicts or JSON text on insert.Object('json')no longer exists server-side, so JSON was previously unusable. (#142)- Query cancellation:
Connection.cancel()/Cursor.cancel()stop a running query from another task, leaving the connection usable. (#104) Connection.last_queryexposes per-query statistics (elapsed, rows/bytes, profile info). (#85)Pool(idle_timeout=...)reaps idle connections down tominsize; pool checkouts also cost one liveness ping instead of two. (#137)- PEP 249 module surface:
apilevel,threadsafety,paramstyle,connect()and the exception hierarchy are importable fromasynch. (#159)
Fixes
- Compressed inserts corrupted the stream and the server dropped the connection — every compressed INSERT was affected. (#149, #153)
DateTime64was decoded as unsigned, silently corrupting pre-1970 timestamps.- A cancelled query wedged the connection permanently; any
asyncio.timeoutaround a query, or a web framework cancelling a request task, triggered it. (#93) alt_hostsnever failed over — the first host's failure aborted the connect. (#144, #136)- Dead pooled connections were handed back to callers: the "reconnect" was a no-op. (#145)
connect_timeout/send_receive_timeout/sync_request_timeoutwere accepted but never used. (#114)- Secure connections without an explicit port used 9000 instead of 9440. (#162, #40, #22)
- Server errors disconnected the connection, costing a pooled connection per failed query. (#150)
- Streaming was impossible for
readonly=1users —max_block_sizewas forced. (#67) getpass.getuser()raisesOSErroron Python 3.13+, breaking the handshake in containers. (#157, #156)Nonein a non-Nullable column now reports the column and expected type instead of a bareTypeError. (#80, #146)
Packaging
- Python 3.11+; binary wheels for Linux (x86_64/arm64), Windows and macOS (Intel/ARM)
- Dependency management moved from Poetry to uv; PyPI publishing via trusted publishing (OIDC)
pytzandleb128dropped;lz4/zstdmoved into thecompressionextra (stdlib zstd on 3.14+)- Type information ships as generated
.pyistubs validated by stubtest - CI across Python 3.11–3.14 and ClickHouse latest + LTS lines
Full details in CHANGELOG.md.
Thanks to @nils-borrmann-tacto, @vlad-zverev, @shsailaubay, @stankudrow, @turquoisehealth, @vizor-games, @baconfield, @objecthuman, @itssimon, @medikos and everyone who filed issues.