mpedb — a drop-in sqlite3 replacement
A multi-process embedded database engine with PostgreSQL-grade concurrency
(MVCC snapshots, lock-free readers) behind the sqlite3 API you already use.
Swap one import and existing code runs unchanged:
import mpedb as db # was: import sqlite3 as db
conn = db.connect("app.db")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
conn.execute("INSERT INTO users (id, name) VALUES (?, ?)", (1, "Ada"))
conn.commit()
print(conn.execute("SELECT name FROM users").fetchall())
pip install mpedb
CPython 3.12+ (abi3: one wheel per platform covers every 3.12+ version).
Wheels for Linux x86-64 and macOS arm64, published automatically when the full
engine test suite is green on CI.
The path decides the engine
| path | what happens |
|---|---|
*.db |
The file is read as a real sqlite database. Writes land in an mpedb delta next to it, and commit() checkpoints them back into the .db — sqlite tools and mpedb see one store, kept in sync. |
*.mpedb |
Native mpedb: multi-process shared-memory engine; attaching processes may be SIGKILLed at any instant without corrupting the file. |
:memory: |
Native mpedb, process-private memory. |
*.toml |
An explicit mpedb config file (declared schema, durability modes, sizing). |
import mpedb.sqlite3 as db # the default routing, under its explicit name
import mpedb.mpedb as db # force the NATIVE engine for any path
Why
- Multi-process for real. sqlite's operational model (no server, attach by path) with lock-free MVCC readers: readers never block the writer, the writer never blocks readers, and a crash mid-write never corrupts the file.
- Compiled plans. SQL compiles once to a content-hashed plan shared across processes; repeated parameterised statements execute with zero parsing.
- Keep your
.dbfiles. The sqlite-backed mode means adopting mpedb does not mean leaving sqlite — your existing tools keep reading the same file. - Never a wrong answer. mpedb's SQL is a differentially tested subset of
sqlite's: a statement is either answered exactly as sqlite answers it, or
refused loudly with
ProgrammingError— never silently misinterpreted.
Honest status (0.1)
The DB-API core is implemented: connect, Connection
(execute/commit/rollback/close, context manager), Cursor
(execute/executemany/fetchone/fetchmany/fetchall/description/
rowcount/iteration), ? parameters, the PEP 249 exception hierarchy, and
live DDL (CREATE TABLE / DROP TABLE / ALTER TABLE).
Not yet implemented (the 0.2 roadmap, mapped against CPython's own
test_sqlite3 suite): Row/row_factory, executescript, lastrowid,
adapters/converters (detect_types), create_function, blobopen,
isolation_level/autocommit control. On the native engine, reads on a
connection do not yet see its own uncommitted writes (they do on the .db
overlay backend). Details and progress:
github.com/punnerud/mpedb.
Reference
Advanced API (beyond sqlite3)
The native module also exposes mpedb's own machinery — content-hashed prepared
plans, explicit write sessions, EXPLAIN, streaming blob inserts:
import mpedb
db = mpedb.Database("app.toml") # open/create from a TOML config file
| Call | Returns | Notes |
|---|---|---|
mpedb.Database(config_path) |
Database |
Opens/creates the database described by the TOML config. Thread-safe; share one handle. |
db.prepare(sql) |
str (64-hex plan hash) |
Compiles once, publishes to the shared plan registry: any attached process can execute it by hash. |
db.execute(hash, params=None) |
SELECT → list[tuple]; DML → int (affected) |
Hot path — no SQL parsing. params is a list/tuple. |
db.query(sql, params=None) |
as execute; EXPLAIN … → str |
One-shot prepare + execute. Use $1…$n parameters, never interpolate values into the SQL text (each distinct text becomes a registry plan). |
db.query_full(sql, params=None) |
(columns: list[str], rows: list[tuple]) |
For callers who need output column names. Raises ProgrammingError for non-SELECT. |
db.explain(sql) |
str |
Plan rendering; nothing is executed (prepends EXPLAIN if absent). |
db.tables() |
list[str] |
Table names from the schema. |
db.verify() |
None |
Page-accounting verification; raises on integrity failure. Takes the writer lock briefly. |
db.begin() |
Transaction |
Interactive write transaction; holds the single writer lock until commit/rollback. |
tx.execute(hash, params=None) / tx.query(sql, params=None) |
as above | Run inside the transaction; SELECTs see the session's own uncommitted writes. tx.query plans are cached process-locally, never published. |
tx.insert_file(table, values, stream_col, path) |
None |
INSERT one row, streaming column stream_col from the file at path a page at a time (never resident — files larger than RAM are fine). values is the full row; values[stream_col] is a placeholder (b""). Path-based on purpose: the engine pulls pages with the writer lock held, so there is no Python read()-callback variant. The streamed column must be the table's last varlen column; tables with a secondary UNIQUE index are refused. |
tx.commit() / tx.rollback() |
None |
Explicit finish. A dropped/GC'd transaction rolls back. |
with db.begin() as tx: |
Commits on clean exit, rolls back if an exception propagates (never suppresses it). |
Free-threading friendly by design: no module-level mutable state, and the GIL is released around every engine call, so point reads from multiple Python threads run truly in parallel.
Value mapping (both directions)
| Python | mpedb column type | Notes |
|---|---|---|
None |
NULL | |
bool |
bool |
checked before int (Python bool subclasses int) |
int |
int64 |
out of range → OverflowError |
float |
float64 |
|
str |
text |
|
bytes / bytearray |
blob |
always returned as bytes |
datetime.datetime |
timestamp |
stored as microseconds since epoch, UTC. Aware datetimes are converted to UTC; naive ones are treated as UTC. Returned as an aware UTC datetime. A plain int is also accepted for timestamp parameters and taken as raw microseconds. |
Exceptions
mpedb.Error (Exception)
├── mpedb.IntegrityError primary-key / UNIQUE / NOT NULL / CHECK violations
├── mpedb.ProgrammingError parse, bind, type mismatch, wrong param count,
│ unknown/invalidated plan, unsupported statement
└── mpedb.OperationalError I/O, corruption, DbFull, ReadersFull, evicted
snapshot, config/schema mismatch, poisoned write
session, engine internals
The sqlite3 aliases (DatabaseError, InterfaceError, DataError,
InternalError, NotSupportedError, Warning) exist and alias the closest
parent, so except sqlite3.DatabaseError keeps catching. Messages carry the
engine's Display text. Binding-level misuse (bad params container,
non-convertible value) raises the ordinary TypeError/OverflowError.
Locking rules (inherited from the Rust facade)
- Never call
db.prepare(...),db.verify(), ordb.query(...)for a not-yet-cached statement while aTransactionfrom the same handle is open on the same thread. They may need the single writer lock the transaction already holds; the ERRORCHECK mutex turns the relock into an error rather than a deadlock. Prepare the statements you need beforedb.begin(); inside the transaction,tx.query/tx.executeare always safe. - Sessions poison on partially-applied statements. Statements are not
internally atomic: if e.g. a multi-row UPDATE fails on its third row, the
first two are already modified and the session becomes poisoned — every
further
tx.execute/tx.queryandtx.commit()raisesmpedb.OperationalError("… poisoned …") andcommitrolls back instead of persisting the torn statement. Onlytx.rollback()(or leaving thewithblock via the exception) is valid. A statement that fails before any side effect (single-row constraint violation, type error) does not poison the session. - One writer at a time, process-wide and machine-wide:
db.begin()blocks on (or errors for re-entry into) the single writer lock. Readers never block. - Use a
Transactionfrom the thread that created it; the writer lock is a pthread mutex with thread affinity.
Building from source
See github.com/punnerud/mpedb:
maturin build --release in crates/mpedb-py/ produces the wheel; the test
suite is crates/mpedb-py/pytest/test_mpedb.py (plain Python, no pytest —
run it twice against the same directory to also exercise persistence across
process restarts).
License: MIT OR Apache-2.0
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mpedb-0.1.3.tar.gz.
File metadata
- Download URL: mpedb-0.1.3.tar.gz
- Upload date:
- Size: 1.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
204cc1276fd9b006820248b6a7a55870f9595aca3540cf342651a90e5c013200
|
|
| MD5 |
9a82e5f206e887da00981ae6ed50dd1d
|
|
| BLAKE2b-256 |
e736f08a0be7fc557fe2a7c37b6217759c1e326847e2355d2b229d0f272e20d1
|
File details
Details for the file mpedb-0.1.3-cp312-abi3-win_amd64.whl.
File metadata
- Download URL: mpedb-0.1.3-cp312-abi3-win_amd64.whl
- Upload date:
- Size: 3.2 MB
- Tags: CPython 3.12+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ede11b2120486a82e2f52360f2ae392bf4100a2c5298b91c83ab177668d862d
|
|
| MD5 |
7ad0895c100e37708bdb4edf16dcfffe
|
|
| BLAKE2b-256 |
07753e17407e2164dca87a116f7f5cc761c8a61eb77e897b38b01ebfdb080c05
|
File details
Details for the file mpedb-0.1.3-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: mpedb-0.1.3-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 3.2 MB
- Tags: CPython 3.12+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
715bd0705447e4a66e3b3ce2651fe4a331d285a259b2b5e14f0bebfcaa2f677b
|
|
| MD5 |
43891b8f249ff345920a10b9c76d2a29
|
|
| BLAKE2b-256 |
a2eac0557e77e1345ba8f3a8074722395e5cb3e421818d027c540702c6e84aa3
|
File details
Details for the file mpedb-0.1.3-cp312-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl.
File metadata
- Download URL: mpedb-0.1.3-cp312-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.12+, manylinux: glibc 2.17+ ARMv7l
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3fdb9b4b1d8ad72418371ad87c91ac257bec3edc0bdba3b19d2ca5288980bb67
|
|
| MD5 |
67b4d905c2fb4a19d1bf57bc990851a5
|
|
| BLAKE2b-256 |
29fa86b4cc676e7bb173c9515c94d91ec1c6dd0f5fbf7ecc6a8af73e39c7e9e4
|
File details
Details for the file mpedb-0.1.3-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: mpedb-0.1.3-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 2.9 MB
- Tags: CPython 3.12+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b27971dfe00f197a3c46ff6a52c90e59e320405058d34f4202cf1f53dd2b5fd2
|
|
| MD5 |
cebd13a457d6d0858be6a7dbd8a22f34
|
|
| BLAKE2b-256 |
0a8131eecde5951c399f5c0c72267628ae8810d2e7b80929b4c6ab5def909f6d
|
File details
Details for the file mpedb-0.1.3-cp312-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: mpedb-0.1.3-cp312-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.9 MB
- Tags: CPython 3.12+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0d4b38cd7b5ff13cfacbb2982ca863e1a22997e6b11a5c2bdad2d03003458c2
|
|
| MD5 |
9ac551514560fa13ef26b40d8e804592
|
|
| BLAKE2b-256 |
a003d67262174d8b76271dce5344a2c2f60e8e3c8c79c84a82ffae97787ea7c3
|