Skip to main content

SQLAlchemy 2.0 dialect for dqlite distributed SQLite

Project description

sqlalchemy-dqlite

SQLAlchemy 2.0 dialect for dqlite.

Installation

pip install sqlalchemy-dqlite

Usage

from sqlalchemy import create_engine, text

# Sync
engine = create_engine("dqlite://localhost:9001/mydb")
with engine.connect() as conn:
    result = conn.execute(text("SELECT 1"))
    print(result.fetchone())

# Async
from sqlalchemy.ext.asyncio import create_async_engine

async_engine = create_async_engine("dqlite+aio://localhost:9001/mydb")
async with async_engine.connect() as conn:
    result = await conn.execute(text("SELECT 1"))
    print(result.fetchone())

Transactions

SQLAlchemy owns the BEGIN/COMMIT/ROLLBACK for any block opened via engine.begin(), connection.begin(), or session.begin(). Do not issue raw BEGIN yourself.

from sqlalchemy import create_engine, text

engine = create_engine("dqlite://localhost:9001/mydb")

# OK — SA emits BEGIN / COMMIT for you
with engine.begin() as conn:
    conn.execute(text("INSERT INTO t VALUES (1)"))

# WRONG — second BEGIN inside an SA-managed transaction errors with
#   OperationalError: cannot start a transaction within a transaction
with engine.begin() as conn:
    conn.execute(text("BEGIN"))                  # error
    conn.execute(text("INSERT INTO t VALUES (1)"))

The same rule applies to engine.connect(): SA auto-begins a transaction on the first execute, so a user-issued text("BEGIN") collides the same way. This matches every other SA backend (pysqlite, postgres, mysql); SA's transaction model is universal.

isolation_level="AUTOCOMMIT" is rejected — every dqlite statement goes through Raft consensus and there is no per-statement autocommit mode. Use engine.begin() (or connection.begin()) for writes.

See SQLAlchemy's transaction docs for the full model.

Savepoint naming

The dqlite client tracks active SAVEPOINTs to keep the SQLAlchemy pool's ROLLBACK-on-checkin path correct. The tracker only handles bare-ASCII SQLite identifiers (e.g. sa_savepoint_1, my_sp) — SQLAlchemy's generated savepoint names always match this shape, so engine.begin() / Session.begin_nested() / connection.begin_nested() are unaffected.

If user-issued raw SQL uses quoted, backticked, square-bracketed, unicode, or leading-digit savepoint names (e.g. text('SAVEPOINT "weird name"')), the client conservatively flags the connection as carrying an untracked savepoint. On the next pool checkin SQLAlchemy issues a safety ROLLBACK, paying one extra round-trip per checkout for the remainder of that connection's lifetime in the pool. Stick to bare-ASCII SAVEPOINT names in raw text SQL to avoid the overhead, or accept the per-checkout cost.

URL Format

dqlite://host:port/database
dqlite+aio://host:port/database

When a query parameter is repeated (?max_total_rows=100&max_total_rows=200), the last occurrence wins. This matches urllib.parse.parse_qsl ordering. Templated connection URLs that layer values from multiple sources should be aware that duplicated keys silently override earlier values rather than raising.

The URL host:port pair is the bootstrap address — the dqlite client discovers the rest of the cluster from that one node's leader-info response. If the URL host is unreachable, leader-discovery cannot start; operators that want bootstrap-from-many-addresses should put a load balancer or DNS round-robin in front of the cluster, or rotate the URL host across deployments. Multi-address bootstrap is not exposed at the dialect URL surface.

STRICT-table DDL is unavailable through SA's compiler

SA's SQLite dialect gates STRICT-table compilation on server_version_info >= (3, 37) (sqlalchemy/dialects/sqlite/base.py). The dqlite dbapi pins sqlite_version_info = (3, 35, 0) as the documented floor (see python-dqlite-dbapi/src/dqlitedbapi/_constants.py) to avoid silently rejecting connections to older-server clusters that ship SQLite below the floor. As a consequence, SA's compiler will NOT emit CREATE TABLE ... STRICT DDL through this dialect, even when the cluster ships SQLite 3.37 or newer.

If your cluster supports STRICT tables and you want STRICT semantics, emit the DDL via raw SQL (engine.execute(text("CREATE TABLE ... STRICT"))) rather than SA's Table / Column model.

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 that commit, a NULL in a BOOLEAN column was emitted as BOOLEAN(0) (decodes to False) and a NULL in a DATETIME column was emitted as ISO8601("") — indistinguishable on the wire from real FALSE / empty-string values. After the commit, NULL is emitted with the SQLite NULL type and decodes to None.

ORM models with Boolean() and DateTime() columns will start returning None for previously-False / "" values after a server upgrade. SQLAlchemy nullable=False constraints will start tripping on rows that previously decoded to non-NULL. There is no driver-level handshake distinguishing the two server versions — check your dqlite cluster version before relying on the post-fix semantics.

Development

See DEVELOPMENT.md for setup, contribution guidelines, and the SQLAlchemy compliance test-suite documentation (how to run it locally, why it's not part of the default pytest invocation, and why ~700 of its tests are correctly skipped).

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

sqlalchemy_dqlite-0.2.0.tar.gz (362.5 kB view details)

Uploaded Source

Built Distribution

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

sqlalchemy_dqlite-0.2.0-py3-none-any.whl (103.5 kB view details)

Uploaded Python 3

File details

Details for the file sqlalchemy_dqlite-0.2.0.tar.gz.

File metadata

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

File hashes

Hashes for sqlalchemy_dqlite-0.2.0.tar.gz
Algorithm Hash digest
SHA256 82715cc15f08a4b0b6bb617d3455789e57cc474415892337913eb0318ec95c0e
MD5 22aa6107005279b193e14c06a41fb45e
BLAKE2b-256 f76c0191a827d79466e6d0f9dd6ec6c496ee88c51569527717ab4a15525a59b9

See more details on using hashes here.

Provenance

The following attestation bundles were made for sqlalchemy_dqlite-0.2.0.tar.gz:

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

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

File details

Details for the file sqlalchemy_dqlite-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for sqlalchemy_dqlite-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 54b2a08c9f844d4e795114ac581c28845369c2214fe1dcfd135e4b6ed2190621
MD5 36b84a899c91245a21c28de227b50d43
BLAKE2b-256 7a3185ef51c46b66017bf2755ae3e4204ecbfd0522fd1a56534d0615737ca638

See more details on using hashes here.

Provenance

The following attestation bundles were made for sqlalchemy_dqlite-0.2.0-py3-none-any.whl:

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

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