Skip to main content

mpedb — a drop-in sqlite3 replacement

PyPI Python Linux macOS Windows

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.10+ (abi3: one wheel per platform covers every 3.10+ version). Wheels for Linux (x86-64, aarch64, armv7), Windows x86-64 and macOS (arm64 and Intel x86-64, each built and suite-tested natively on its own hardware), 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.
  • Crash-safe on every platform this ships a wheel for, and by the same standard on each: Linux (x86-64, aarch64, armv7l), macOS/Apple Silicon and Windows x86-64 all run all six multi-process crash harnesses in CI — crash, stress, powerloss, collide, queue-collide, mirror-collide — with processes SIGKILLed mid-write and the file verified afterwards. Windows is not a port that merely compiles: shared CreateFileMapping views, a LockFileEx writer lock with owner-death release, GetProcessTimes reader identity, FlushViewOfFile + FlushFileBuffers durability. The first thing those harnesses found on Windows was a corruption bug that turned out to be ours on every platform.
  • Compiled plans. SQL compiles once to a content-hashed plan shared across processes; repeated parameterised statements execute with zero parsing.
  • Keep your .db files. 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)

  • db.prepare(...), db.verify(), db.query(...), db.query_full(...) and a second db.begin() are REFUSED while a Transaction from the same handle is open on the same thread (#161) — a ProgrammingError that names the method and what to do instead. They may need the single writer lock the transaction already holds, and that is a hang, not an error: no traceback, no hint which call was the mistake. Prepare the statements you need before db.begin(); inside the transaction, tx.query/tx.execute are always safe.

    It refuses unconditionally, not only when the plan is uncached, and that is deliberate: a guard that allows the call when the plan happens to be in the registry reproduces the original bug's worst property — it works in testing and hangs in production. The first thing this caught was a call in mpedb's own test suite, in a test whose comment already said the prepare belonged outside the block.

    Another THREAD calling these is ordinary contention and waits its turn; only the thread holding the lock is refused.

  • 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.query and tx.commit() raises mpedb.OperationalError ("… poisoned …") and commit rolls back instead of persisting the torn statement. Only tx.rollback() (or leaving the with block 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.

  • A Transaction is refused from any thread but the one that created it (#161). The writer lock is a mutex with thread affinity — releasing it from another thread is undefined behaviour in POSIX, not a rule this API invented. __exit__ is the one exception: it runs on whatever thread is unwinding, and refusing there would leave a with block that cannot be left and a writer lock that is never released.

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

Metadata

Release files for mpedb 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mpedb 0.3.1
File Size Uploaded
mpedb-0.3.1.tar.gz 2.8 MB Details

Built distributions (wheels)

Table of built distributions (wheels) for mpedb 0.3.1
File
mpedb-0.3.1-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
mpedb-0.3.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
mpedb-0.3.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl CPython 3.10 abi3 Linux glibc 2.17+ ARMv7l Details
mpedb-0.3.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details
mpedb-0.3.1-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details
mpedb-0.3.1-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details

Total release size: 34.0 MB

Release files / mpedb-0.3.1.tar.gz

Download URL mpedb-0.3.1.tar.gz
Size 2.8 MB
Tags Source
SHA-256 checksum
How to use checksums
9e80d2eed41a215555176018de4d9f5a2b0348e6d6601c059751ce186b80f482
BLAKE2b-256 checksum
How to use checksums
ca90b3a6520304eb6781340c4bc3d2d94f2a681a9f7d19b51d31f25f0bbf3ec4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / mpedb-0.3.1-cp310-abi3-win_amd64.whl

Download URL mpedb-0.3.1-cp310-abi3-win_amd64.whl
Size 5.6 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
a487841151503b8291b68f0eb4896a107514117476737558455a917e2282723b
BLAKE2b-256 checksum
How to use checksums
ba632c4b53f61684304d7fcb98d8b8bcb67673b83d56e6ffa40ad7227e742573
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / mpedb-0.3.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL mpedb-0.3.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 5.4 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
4a552840ac15c6c1289a64b9907f3e5104b1e651b176e31cd3f55a9b2ad84e11
BLAKE2b-256 checksum
How to use checksums
c7087b5daf22598603617024b3aa6b558b6b7509456b027da362dbf21015439e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / mpedb-0.3.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl

Download URL mpedb-0.3.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
Size 5.0 MB
Tags CPython 3.10 Linux glibc 2.17+ ARMv7l abi3
SHA-256 checksum
How to use checksums
05788807ed1a287be4cabcfac0faed89a751247819ff51b40ae73256bc58ecad
BLAKE2b-256 checksum
How to use checksums
0b7028bc4aee2de16e9e089b3f140e06fbc5a651571b2b2e588a0f6ebc28e6a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / mpedb-0.3.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL mpedb-0.3.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 5.0 MB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
2c54df8feabf3f2f0809c09c7ad93976f9674fae0be66c7732bae1a6332f2995
BLAKE2b-256 checksum
How to use checksums
a7937c8b8d69ed03ed4631b324d8060c8244f347b3254a1146cf31090b56736f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / mpedb-0.3.1-cp310-abi3-macosx_11_0_arm64.whl

Download URL mpedb-0.3.1-cp310-abi3-macosx_11_0_arm64.whl
Size 5.0 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
0ccf4181692bf49504a22aea1e5279520580a3349f17bd4a876338c8b3d5c0b7
BLAKE2b-256 checksum
How to use checksums
1fd7a3ee76c8b74072ffeaae38c81beafc33320bb2f1a2afd3771d41dde9eaf1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / mpedb-0.3.1-cp310-abi3-macosx_10_12_x86_64.whl

Download URL mpedb-0.3.1-cp310-abi3-macosx_10_12_x86_64.whl
Size 5.2 MB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
54de62f9e67b2f270f1cb6ea14c7e1f06cabd52c3a18adbcdddf32ec09b4150a
BLAKE2b-256 checksum
How to use checksums
87b234c0c61a537efbd771e0861c9ff83cb0657b75ccd29fb8e872e2b5c2551f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.3.1 This release

7 release files

0.3.0

7 release files

0.2.9

6 release files

0.2.8

6 release files

0.2.7

6 release files

0.2.6

6 release files

0.2.5

6 release files

0.2.4

6 release files

0.2.3

6 release files

0.2.2

6 release files

0.2.1

6 release files

0.2.0

6 release files

0.1.5

6 release files

0.1.3

6 release files

0.1.2

3 release files

0.1.1

3 release files

0.1.0

3 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page