Skip to content

letsdiscodev/sqlalchemy-dqlite

Repository files navigation

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.

Development

See DEVELOPMENT.md for setup and contribution guidelines.

License

MIT

About

SQLAlchemy 2.0 dialect for dqlite.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages