dqlite-dbapi is shaped like the stdlib sqlite3 module, but it talks to a
replicated cluster over the network rather than a local file. This page
covers the differences you are most likely to notice when porting code.
- Autocommit by default. No implicit
BEGINbefore DML — the opposite of stdlib's implicit-transaction model. See Transactions. - One statement per
execute().cursor.execute("SELECT 1; SELECT 2;")raisesProgrammingError(client-side, before any network round-trip). Split into separate calls. There is noexecutescript()— it raisesNotSupportedError. qmarkparameters only. Use?placeholders. Stdlib also accepts:name; this driver advertises onlyqmarkand rejects mappings withProgrammingError.- Foreign keys are enforced by default. dqlite defaults every fresh
connection to
PRAGMA foreign_keys = ON— the opposite of stdlib/pysqlite, which defaultOFF. IssuePRAGMA foreign_keys = OFFper connection for SQLite's legacy unenforced behavior. - SERIALIZABLE isolation only. Every statement is ordered by Raft; weaker isolation levels are not exposed.
connect()timeouts differ from stdlib. stdlib'stimeoutis the busy (lock-wait) budget; heretimeoutis a per-RPC-phase deadline and the busy budget is the separatebusy_timeoutargument.connect()also exposes safety caps —max_message_size,max_continuation_frames,max_total_rows— andtrust_server_heartbeat.- Result sets are fully materialized at
execute()time. Stdlib streams rows lazily from the C engine; dqlite drains every continuation frame into the cursor's in-memory buffer beforeexecute()returns, andfetchone()/fetchmany()/fetchall()iterate that buffer. For large queries, cap memory withmax_total_rows(forwarded fromconnect()) or shape queries with an explicitLIMIT. - No SQLite extension APIs.
create_function,create_aggregate,create_window_function,iterdump,backup,set_authorizer,serialize,blobopen,executescript, andregister_converterhave no server-side counterpart in dqlite; their stubs raiseNotSupportedError. rowcountafter SELECT islen(rows). Stdlib returns-1for queries. dqlite knows the full result at execute time, so it reports the count.rowcountstays-1for non-result paths, for all PRAGMA statements (read or write — matching stdlib), and for a never-executed cursor.
A 2026 dqlite server change (upstream commit f30fc99) changed how a NULL
cell is encoded in columns declared BOOLEAN, DATE, DATETIME, or
TIMESTAMP:
- Before: a NULL came back as
False(forBOOLEAN) or""(for datetime types) — indistinguishable from a real value. - After: a NULL comes back as
None.
There is no handshake that distinguishes the two server versions, and the
driver decodes faithfully whatever the server sends. Code like
if row[0] is None: will silently miss rows on an old cluster and start
matching after an upgrade. Check your dqlite cluster version before relying
on is None for these columns.
You can usually ignore these, but they are here so nothing surprises you:
lastrowidisNone(not0) before the first INSERT. After the first INSERT both drivers agree and keep the value sticky across UPDATE/DELETE/DDL.fetchmany(0)returns[](PEP 249 read literally). PassNoneor omit the argument to default toarraysize.fetchmany(negative)raisesProgrammingError.execute("")raisesProgrammingError(empty/whitespace/comment-only SQL). Stdlib treats it as a no-op.- Empty result sets report the
UNKNOWNtype code indescription[i][1](importable fromdqlitedbapi). Test it withtype_code == UNKNOWN; for column types on an empty result, queryPRAGMA table_info(...). - CTE-prefixed DML (
WITH ... INSERT/UPDATE/DELETE) setslastrowidand reportsrowcountas the affected-row count. Thatrowcountis a dqlite extra: stdlib sqlite3 returns-1for CTE-prefixed DML (it counts only non-CTE DML).WITH ... SELECTandWITH ... RETURNINGreturn rows as usual.
The PEP 249 type sentinels (STRING, BINARY, NUMBER, DATETIME,
ROWID) match a description type code through __eq__, and each wraps
several wire types (NUMBER covers INTEGER+FLOAT+BOOLEAN; DATETIME covers
DATE+TIMESTAMP+ISO8601). A bare wire-int type code does not hash equal to a
sentinel, so set/dict membership silently returns False:
type_code = cur.description[i][1]
if type_code == STRING or type_code == NUMBER: # correct
...
if type_code in {STRING, NUMBER}: # WRONG: silently False
...Stdlib sqlite3 doesn't export these sentinels at all, so the
chained-equality form is also the cross-driver-portable idiom.