Skip to main content

Async Python client for dqlite with connection pooling and leader detection

Project description

dqlite-client

Async Python client for dqlite. The API shape (explicit connect() / create_pool(), fetch / fetchall / fetchval, context-manager-driven transactions) is inspired by asyncpg's ergonomics, but the data model is simpler: fetch returns list[dict], fetchall returns list[list], fetchval returns a scalar. There is no Record type — callers who need the asyncpg Record surface should wrap the rows explicitly.

Installation

pip install dqlite-client

Usage

import asyncio
from dqliteclient import connect

async def main():
    conn = await connect("localhost:9001")
    async with conn.transaction():
        await conn.execute("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)")
        await conn.execute("INSERT INTO test (name) VALUES (?)", ["hello"])
        rows = await conn.fetch("SELECT * FROM test")
        for row in rows:
            print(row)
    await conn.close()

asyncio.run(main())

Connection Pooling

from dqliteclient import create_pool

pool = await create_pool(["localhost:9001", "localhost:9002", "localhost:9003"])
async with pool.acquire() as conn:
    rows = await conn.fetch("SELECT 1")

The pool issues min_size connection handshakes in parallel during initialize(). All initial connects target whichever node the cluster client identifies as the current leader, so the leader serialises its incoming-connection acceptance — min_size=N does NOT speed up startup linearly with N. A balanced default (min_size=1 or low single digits) keeps cold-start latency predictable; raise it only when steady-state concurrency demands warm connections at engine startup.

Forking and multiprocessing

Connections, pools, and the cluster client are not safe to use across os.fork(). The library detects fork-after-init and raises InterfaceError from any operation in the child process; the inherited TCP socket would otherwise be shared with the parent (writes would interleave on the wire) and asyncio primitives are bound to the parent's event loop.

Common deployment patterns that fork after import:

  • gunicorn with --preload: workers inherit pools created in the parent. Move pool creation into a per-worker post_fork hook (gunicorn post_fork config) instead of the module top level.
  • multiprocessing: child processes must reconstruct connections / pools from configuration (addresses, database name) rather than receive a parent-built object.
  • Celery prefork pool: each worker process must create its own pool inside the worker init signal, not at module load.

The fork detection is best-effort (pid mismatch); silent double-FIN on the parent's socket is a real risk if the guard is bypassed (e.g. via __new__ to skip __init__). Just create the pool / connection in the child process you intend to use it from.

Cross-version semantic shift: NULL in BOOLEAN/DATETIME columns

Upstream dqlite commit f30fc99 (query: preserve SQLITE_NULL type for NULL values, 2026-01-25) changed the wire encoding of NULL cells in columns declared BOOLEAN, DATE, DATETIME, or TIMESTAMP:

  • Before f30fc99: a NULL cell was emitted with the column's coerced type (BOOLEAN with value 0, or ISO8601 with value "") — indistinguishable from a real FALSE or empty string.
  • After f30fc99: a NULL cell is emitted with SQLITE_NULL (tag 5) and decodes to None.

Python code that uses if row[0] is None: against an old-server cluster will silently miss NULL rows. After a cluster upgrade past f30fc99, the same code starts firing where it previously read False or "". There is no driver-level handshake field that distinguishes the two server versions; check your dqlite cluster version before relying on is None for BOOLEAN / DATETIME columns. The Python codec faithfully decodes whatever the server emits — the server, not the driver, drives this semantics shift.

Layering

dqlite-client is the low-level async wire client. Most applications should reach for one of the higher-level packages instead:

  • dqlite-dbapi — PEP 249–compliant wrapper (sync or async). Plug-and-play with SQLAlchemy, Alembic, most ORMs.
  • sqlalchemy-dqlite — SQLAlchemy 2.0 dialect, built on top of dqlite-dbapi.

Use dqlite-client directly when you need fine-grained control over the wire protocol: custom cluster bootstrapping, explicit message decoding, or building a new high-level driver.

Development

See DEVELOPMENT.md for setup and contribution guidelines.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dqlite_client-0.2.1.tar.gz (634.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dqlite_client-0.2.1-py3-none-any.whl (192.5 kB view details)

Uploaded Python 3

File details

Details for the file dqlite_client-0.2.1.tar.gz.

File metadata

  • Download URL: dqlite_client-0.2.1.tar.gz
  • Upload date:
  • Size: 634.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dqlite_client-0.2.1.tar.gz
Algorithm Hash digest
SHA256 71755ff0a3b170297c4f636f1bb7147de04ae326edd8447165adf4eda6285ee9
MD5 c8411cf423627675fb4be5ab3035a6a1
BLAKE2b-256 d4dcbd6f1ded9bb75858370e9061786e97eb11ad569b7f24677dd0295df1f7fa

See more details on using hashes here.

Provenance

The following attestation bundles were made for dqlite_client-0.2.1.tar.gz:

Publisher: publish-to-pypi.yml on letsdiscodev/python-dqlite-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dqlite_client-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: dqlite_client-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 192.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dqlite_client-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f14b3261926bde384a04c35ebdb6b76f161be798455c16361a3d48d9efc8cb58
MD5 58d5dd021690c2084f876152f3d1d8a2
BLAKE2b-256 2f533efac20d02e1e247873fd34988a9b8d89e675dbe34a43f38b103abfd0d9a

See more details on using hashes here.

Provenance

The following attestation bundles were made for dqlite_client-0.2.1-py3-none-any.whl:

Publisher: publish-to-pypi.yml on letsdiscodev/python-dqlite-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page