Skip to content

Releases: punnerud/mpedb

v0.3.1 — a sqlite drop-in that PHP can load without installing anything

Choose a tag to compare

@punnerud punnerud released this 22 Aug 10:45

Same release as v0.3.0, renumbered so the published wheel matches the tag:
PyPI already held an mpedb 0.3.0, uploaded on 2026-08-12 from the abi3-py310
work, while every other crate was still at 0.2.9. pip install mpedb==0.3.0
therefore predates everything below. 0.3.1 is a number no artifact has used.

The shim reads and writes real sqlite files

mpedb-sqlitefmt gained a writer for b-trees that span pages, with indexes
that stock sqlite's integrity_check accepts. The C-API shim can open a real
.db, keep writes in a .mpedb sidecar, and PRAGMA wal_checkpoint the whole
database back out as a sqlite file.

NOT NULL survives both directions. That is not tidiness: mpedb's planner
reads declared nullability, and a row with a NULL in an indexed column has no
index entry at all — so a composite index whose trailing column is nullable
cannot serve a range over the leading one. Losing the flag on import turned an
indexed lookup into a full scan on a 945 234-row table.

PHP, with nothing installed

No PECL, no extension to compile, no rebuild of PHP. The distro's own
pdo_sqlite and sqlite3 are dynamically linked against libsqlite3.so.0,
so substituting the shim under that soname is the whole procedure:

mkdir shim && ln -s $PWD/libmpedb_sqlite3.so shim/libsqlite3.so.0
LD_LIBRARY_PATH=$PWD/shim php app.php

LD_PRELOAD does not work here: PHP loads extensions with dlopen, and
their own DT_NEEDED wins over the preload scope, so the shim loads, binds
nothing, and the program quietly keeps using stock sqlite.

Four missing symbols were added (sqlite3_sql, sqlite3_column_table_name,
sqlite3_mprintf, sqlite3_snprintf). The printf pair cannot be written in
Rust — defining a C-variadic function is still unstable — and a symbol defined
only in a C object binds local in a cdylib, invisible to any consumer. So
Rust owns the names and C owns the bodies, joined by naked tail-branch thunks
that leave every argument register untouched.

A CI job now loads both PHP extensions against the shim, compares its printf
against the runner's real sqlite format for format, and checks that stock
sqlite accepts the checkpointed file.

Islands in the planner

A predicate can bound a column without ever naming a bound.
(lat-A)*(lat-A) + (lon-B)*(lon-B) < R*R is a circle, and a circle is a box in
every axis — but the planner saw f(col) > 0, classified it unknown, and took
a full scan over a tree that could have answered it. planner::interval
computes the box by backward interval arithmetic. Nothing in it knows
Pythagoras, circles or geography.

IndexState (schema v21)

A background build must publish an index before filling it, so concurrent
writers maintain it from that moment. But an index the planner can see is one
it will use, and a half-filled index is not a slower answer — it is a wrong
one. Building participates in maintenance and is invisible to access
selection; a crash leaves it resumable, where a Ready index missing rows
would be unrecoverable.

Fixed before release

This branch had never run CI. The first run found four defects, two of them
answer-changing:

  • an island bound encoded as a float against integer keys — keycode gives an
    Int the sign-flipped i64 image and a Float the IEEE total-order image,
    and neither orders against the other, so UPDATE t SET b = a WHERE a + 3 < -15 silently skipped the row where a = -47;
  • a checkpoint that wrote mpedb's synthesized rowid as a real column, giving
    every row a NULL in a column the schema declared;
  • the printf thunks assembling only on x86-64 (macos-latest is ARM);
  • an EXPLAIN label naming the wrong column for a composite range.

mpedb-pg is versioned with everything else again — it is its own workspace
and was left at 0.2.9 when v0.2.9 was cut.

v0.3.0 — a sqlite drop-in that PHP can load without installing anything

Choose a tag to compare

@punnerud punnerud released this 22 Aug 10:28

The shim reads and writes real sqlite files

mpedb-sqlitefmt gained a writer for b-trees that span pages, with indexes
that stock sqlite's integrity_check accepts. The C-API shim can now open a
real .db, keep writes in a .mpedb sidecar, and PRAGMA wal_checkpoint the
whole database back out as a sqlite file.

NOT NULL survives both directions. That is not tidiness: mpedb's planner
reads declared nullability, and a row with a NULL in an indexed column has no
index entry at all — so a composite index whose trailing column is nullable
cannot serve a range over the leading one. Losing the flag on import turned an
indexed lookup into a full scan on a 945 234-row table.

PHP, with nothing installed

No PECL, no extension to compile, no rebuild of PHP. The distro's own
pdo_sqlite and sqlite3 are dynamically linked against libsqlite3.so.0,
so substituting the shim under that soname is the whole procedure:

mkdir shim && ln -s $PWD/libmpedb_sqlite3.so shim/libsqlite3.so.0
LD_LIBRARY_PATH=$PWD/shim php app.php

LD_PRELOAD does not work here, and the reason is worth knowing before
debugging it: PHP loads extensions with dlopen, and their own DT_NEEDED
wins over the preload scope, so the shim loads, binds nothing, and the program
quietly keeps using stock sqlite.

Four missing symbols were added (sqlite3_sql, sqlite3_column_table_name,
sqlite3_mprintf, sqlite3_snprintf). The printf pair cannot be written in
Rust — defining a C-variadic function is still unstable — and a symbol defined
only in a C object binds local in a cdylib, invisible to any consumer. So
Rust owns the names and C owns the bodies, joined by naked tail-branch thunks
that leave every argument register untouched.

A CI job now loads both PHP extensions against the shim, compares its printf
against the runner's real sqlite format for format, and checks that stock
sqlite accepts the checkpointed file.

Islands in the planner

A predicate can bound a column without ever naming a bound.
(lat-A)*(lat-A) + (lon-B)*(lon-B) < R*R is a circle, and a circle is a box in
every axis — but the planner saw f(col) > 0, classified it unknown, and took
a full scan over a tree that could have answered it. planner::interval
computes the box by backward interval arithmetic. Nothing in it knows
Pythagoras, circles or geography.

IndexState (schema v21)

A background build must publish an index before filling it, so concurrent
writers maintain it from that moment. But an index the planner can see is one
it will use, and a half-filled index is not a slower answer — it is a wrong
one. Building participates in maintenance and is invisible to access
selection; a crash leaves it resumable, where a Ready index missing rows
would be unrecoverable.

Also

mpedb-pg is versioned with everything else again — it is its own workspace
and was left at 0.2.9 when v0.2.9 was cut.

v0.2.9 — the connect cell, cut by three quarters

Choose a tag to compare

@punnerud punnerud released this 03 Aug 14:59

Verified on Linux x86-64, macOS/Apple Silicon and Windows x86-64 — tests, clippy and the crash harnesses green on all three (CI runs the full suite on each). Prebuilt binaries attached below; sha256sums included.

macOS binaries follow shortly. The Apple-silicon build host was offline when this was cut, so the macOS CLI and shim are uploaded to this same release as soon as it is back. macOS is tested every push through CI — only the prebuilt artifact is late. pip install mpedb and cargo install are unaffected on every platform.

Covers 0.2.8 as well. 0.2.8's wheels went to PyPI but its GitHub release never got cut (the macOS build host was offline that night), so its notes are folded in here. If you are on 0.2.7, this release is both rounds.

The connect cell, cut by three quarters (0.2.9)

The Django 4.2 project driving this series re-profiled after 0.2.8 and reported the suite gap at noise level — with connect cost the last item on sqlite's win list at ~30×. That cell is now measured, attributed, and mostly gone:

  • A fresh memfd is already zero. format() wiped the whole control region — metas, lock area, reader table, intent ring — which on a fresh in-memory backing is not a memset but 92 page faults: 235 of a :memory: connect's ~330 µs. That wipe exists for reused shared files, whose control bytes carry a previous incarnation. A private backing (a memfd this very open created — by definition nothing else can attach) is kernel-zeroed by construction and now skips it. Every control invariant reads identically; the full wipe stays on every shared path, so the cross-process apparatus is exactly as SIGKILL-safe as before.
  • Two /proc reads per open, gone. The Linux boot id was read three times per open and our own start time once — both per-process constants. boot_id is now cached for the process lifetime (except on Windows, whose pair carries a live uptime clock that must be read fresh); the own-start-time cache is keyed by pid, because Python consumers fork and a child must not inherit its parent's reader identity.
  • MPEDB_OPEN_TRACE=1 makes the open path self-attributing (per-stage µs through the facade, the engine and the shm init), with a connect_cost.rs loop that splits open from close.

Measured on a quiet box, :memory: open+close: 391 µs → ~100 µs (75 open, 15 close). What remains is the mapping's own lifecycle; the cross-process machinery now costs about 30 µs of it, and the reader table and intent ring cost nothing at all — their pages are never faulted in.

What 0.2.8 brought (folded in)

  • ROLLBACK TO SAVEPOINT stops paying for DDL that never happened. Every rollback rebuilt the session's schema view and cleared the entire shared plan cache — machinery that exists solely for rolled-back DDL, now gated on whether the session ever ran any. Savepoint statements went from ~2.4 µs to ~0.77 µs (level with the bench's null control), the disk savepoint-cycle cell from 4.1× to 1.93× stdlib. The suite-level effect is larger than the cell: Django's savepoint-per-test isolation no longer evicts every cached plan once per test.
  • executemany got its engine half (query_many): plan and per-statement facts resolved once, rows looped inside an engine savepoint — any row error rolls the batch back whole, so the per-row road reruns it with double-apply structurally impossible. 1.8 → 1.5 µs/row.
  • Two report items settled by measurement, not code: the "legacy BEGIN" divergence is pinned stock-identical on the native route (stdlib refuses it too — the report expected otherwise), and the memory-vs-disk asymmetry in the microbench attributes to the control arm's storage sensitivity; mpedb's own per-op cost is backing-independent.

Also since 0.2.7

  • The playground opens again (punnerud.github.io/mpedb). It had been trapping at open since late July: the notify epoch stamp and the :memory: namer read std::time/std::process::id directly, both of which panic on wasm32, while every native suite stayed green and the Pages workflow's "success" only ever meant deployed. Both cures landed — and Pages gained the gate that was missing: the freshly built module must open, not merely compile.
  • benchmarks/concurrency.md — "how many concurrent locks before it melts", answered with a curve: 1→1 024 writer processes against same-shape sqlite (WAL + synchronous=FULL + fullfsync) and PostgreSQL 16 controls on one host. It climbs to a 128-process peak (47 k durable commits/s, 5× PostgreSQL's best) and sags; it does not melt. Plus the latency half, because throughput can hide starvation: mpedb's ordered ring holds p99 ≈ 2× p50 through 128 writers, while the sqlite control's worst writer starves 15+ seconds from 8 processes up.

Attached binaries

file contents
mpedb-0.2.9-linux-x86_64.tar.gz mpedb CLI, Linux x86-64
mpedb-0.2.9-windows-x86_64.zip mpedb.exe, Windows x86-64
libmpedb_sqlite3-0.2.9-linux-x86_64.tar.gz sqlite3 drop-in shim, Linux
SHA256SUMS checksums for the above
mpedb-0.2.9-macos-arm64.tar.gz mpedb CLI, macOS Apple Silicon — uploaded shortly
libmpedb_sqlite3-0.2.9-macos-arm64.tar.gz sqlite3 drop-in shim, macOS — uploaded shortly

v0.2.7 — the per-statement gap, closed layer by layer

Choose a tag to compare

@punnerud punnerud released this 03 Aug 01:21

Verified on Linux x86-64, macOS/Apple Silicon and Windows x86-64 — tests, clippy and the crash harnesses green on all three. Prebuilt binaries attached (CLI for all three platforms + the sqlite3 drop-in shim; sha256sums below).

The 0.2.6 known issue, closed — and two more the fix's review caught

  • The :memory: savepoint engine bug is fixed (0.2.6's named known issue, and the swap route's N4 report: btree corruption after ~4000 autocommit-plus-savepoint cycles, with silent wrong answers after). Private in-memory backing adopts committed pages in place — a third page state the savepoint machinery's COW dichotomy never covered. Savepoints now track adopted pages explicitly; the consumer's exact 4000-cycle repro is a regression test, judged by the page-accounting verifier.
  • A pre-existing silent wrong answer, found and fixed: a large (extent-backed) value deleted after a SAVEPOINT and overwritten by a later insert in the same scope could survive ROLLBACK TO with the new payload under the old row — committed, no error, in every release since extents shipped. Freed extent runs are now parked while a savepoint is open. Found by the adversarial review run on this release's savepoint work; reproduced byte-for-byte on 0.2.6 before fixing.
  • The same review caught the new savepoint journal's own blind spot (freed-then-recycled tree pages) before it ever merged — that repro is pinned too.

Performance: the per-statement gap, attacked layer by layer

The swap route's profiling put mpedb ~17–19 % behind stdlib on their suite under equal load, attributed to per-statement execute latency (345 µs vs 135 µs through Django). This release closes the structural share of that gap; every change was measured in isolation, A/B'd against its parent commit:

  • The in-transaction fast path (every ORM statement): the text-plan memo is now checked FIRST — a hit skips three per-statement text re-inspections — and a session revalidates a plan's table facts once instead of opening a fresh read snapshot per statement. Session-path overhead measured at ~2.1 µs/statement went to ~0; an in-transaction point-SELECT is no longer slower than its autocommit twin.
  • Transaction control never compiles: SAVEPOINT/RELEASE/ROLLBACK TO dispatch through the real grammar without the compile pipeline. Django mints a unique savepoint name per use — each one used to pay a full compile, churn the 512-entry text memo against the hot statements, and grow the plan cache without bound. Savepoint statements: 2× faster, and unique names now cost the same as repeated ones.
  • Savepoints capture lazily: SAVEPOINT used to copy every dirty page eagerly — Django's fixture pattern (fixture in the outer transaction, savepoint per test) paid O(fixture) per test. Pre-images are now journaled at first touch; the big-fixture savepoint cycle went from 3× the small-fixture cost to parity.
  • The Python DB-API layer dispatches in one pass (no per-statement lowercase scans or redundant copies), and executemany runs its rows on the Rust side of the GIL in 1024-row chunks.
  • LIKE scans: patterns with a wildcard-free core (%x%, x%, %x, exact) now run as allocation-free byte searches, and the general matcher no longer allocates per row — the LIKE '%x%' full-scan cell went from 5.4× to 3.0× stdlib. A 20 000-case differential test holds every fast path to the old matcher, verbatim.

Where mpedb already led — point/range reads on disk, empty transactions — nothing moved (those cells are the null controls).

Attached binaries

file contents
mpedb-0.2.7-linux-x86_64.tar.gz mpedb CLI, Linux x86-64
mpedb-0.2.7-macos-arm64.tar.gz mpedb CLI, macOS Apple Silicon
mpedb-0.2.7-windows-x86_64.zip mpedb.exe, Windows x86-64
libmpedb_sqlite3-0.2.7-linux-x86_64.tar.gz sqlite3 drop-in shim, Linux
libmpedb_sqlite3-0.2.7-macos-arm64.tar.gz sqlite3 drop-in shim, macOS
SHA256SUMS checksums for everything above

v0.2.6 — savepoints route true

Choose a tag to compare

@punnerud punnerud released this 02 Aug 17:27

Verified on Linux x86-64, macOS/Apple Silicon and Windows x86-64 — tests, clippy and the crash harnesses green on all three. Prebuilt binaries attached (CLI for all three platforms + the sqlite3 drop-in shim; sha256sums below).

The one root cause behind the remaining test failures (N1)

With v0.2.5, the Django 4.2 project driving this series ran its full migrate natively and deleted every workaround — and ~all remaining test failures traced to one defect: the Python module's transaction interception matched statements on the head word, so ROLLBACK TO SAVEPOINT s executed as a bare ROLLBACK — the whole transaction silently discarded (writes before the savepoint lost), the paired RELEASE then found no transaction, and SAVEPOINT in autocommit was refused instead of starting one. Django's TestCase puts the class fixture in an outer transaction and each test in a savepoint, so the first rollback ate the fixture and every subsequent test in the class died.

All three shapes now match the stdlib, pinned end to end including Django's fixture-plus-savepoint-per-test cycle. The engine's savepoint machinery was correct all along — the fix is pure statement routing.

Also from the report: connect() now sweeps shared-memory stores left behind by crashed processes (SIGKILL skips atexit), so /dev/shm no longer accumulates orphaned 64 MiB files.

Known issue (tracked, fix in progress)

On the plain :memory: backing only: a savepoint taken as the first statement of a transaction, on a connection with previously committed transactions, can roll back nothing. A deterministic reproduction is in the repo and the mechanism is under adversarial review. File-backed databases — including the shared-memory databases Django's test runner uses — are unaffected.

Attached binaries

file contents
mpedb-0.2.6-linux-x86_64.tar.gz mpedb CLI, Linux x86-64
mpedb-0.2.6-macos-arm64.tar.gz mpedb CLI, macOS Apple Silicon
mpedb-0.2.6-windows-x86_64.zip mpedb.exe, Windows x86-64
libmpedb_sqlite3-0.2.6-linux-x86_64.tar.gz sqlite3 drop-in shim, Linux
libmpedb_sqlite3-0.2.6-macos-arm64.tar.gz sqlite3 drop-in shim, macOS
SHA256SUMS checksums for everything above

v0.2.5 — the swap route's W1–W5, closed

Choose a tag to compare

@punnerud punnerud released this 02 Aug 15:03

Verified on Linux x86-64, macOS/Apple Silicon and Windows x86-64 — tests, clippy and the crash harnesses green on all three. Prebuilt binaries attached (CLI for all three platforms + the sqlite3 drop-in shim; sha256sums below).

The swap route's third field report, closed (W1–W5)

With v0.2.4, the Django 4.2 project driving this series got a full migrate through (238 tables, 571 indexes) and real test execution — behind five workarounds. This release closes their sources, so the workarounds can be deleted:

  • W1 — cursor chaining: Cursor.execute()/executemany() return the cursor (the stdlib contract). Django chains cursor.execute(...).fetchone() on a raw cursor in its query-logging path, unshimmable from above.
  • W2 — sqlite_master queries: the WHERE parser takes conditions split across lines (Django's json-field introspection writes one per line), and bare ? placeholders consume positions — two or more bound parameters used to silently answer zero rows.
  • W3 — PRAGMA index_list / index_info answer from the schema, including sqlite's sqlite_autoindex_… naming for unnamed constraint indexes (origin u, partial flag). This is the walk Django's get_constraints is built on; empty answers made every constraint invisible and killed AlterUniqueTogether migrations.
  • W4 — faithful sql text: the reconstructed CREATE TABLE keeps composite UNIQUE (…) clauses and marks AUTOINCREMENT; a named index's sqlite_master row carries its CREATE INDEX text (autoindexes stay NULL, sqlite's own shape).
  • W5 — mortal shared memory: file:…?mode=memory&cache=shared databases are now scoped to the process and their backing files removed at interpreter exit — sqlite's shared cache dies with the process; ours outlived it, so the next test run inherited a half-migrated store, and 64 MiB per name leaked in /dev/shm.

The report's leftover item (BEGIN after autocommit DML on a default connection) is pinned as stock-identical: measured side by side, the stdlib raises the same OperationalError.

Attached binaries

file contents
mpedb-0.2.5-linux-x86_64.tar.gz mpedb CLI, Linux x86-64
mpedb-0.2.5-macos-arm64.tar.gz mpedb CLI, macOS Apple Silicon
mpedb-0.2.5-windows-x86_64.zip mpedb.exe, Windows x86-64
libmpedb_sqlite3-0.2.5-linux-x86_64.tar.gz sqlite3 drop-in shim, Linux
libmpedb_sqlite3-0.2.5-macos-arm64.tar.gz sqlite3 drop-in shim, macOS
SHA256SUMS checksums for everything above

v0.2.4 — the pip module speaks Django's sqlite3

Choose a tag to compare

@punnerud punnerud released this 02 Aug 10:57

Verified on Linux x86-64, macOS/Apple Silicon and Windows x86-64 — tests, clippy and the crash harnesses green on all three. Prebuilt binaries attached (CLI for all three platforms + the sqlite3 drop-in shim; sha256sums below).

The pip install mpedb module speaks Django's sqlite3

The first real Django 4.2 project switched onto the Python module (the sys.modules swap route, as opposed to the LD_PRELOAD shim) logged five precise blockers. All five close in this release, each verified against the log's own repro and pinned in the module's suite (28 → 34 checks):

  1. Connection and Cursor are subclassable, and cursor(factory=...) instantiates the subclass — Django's backend defines SQLiteCursorWrapper(Database.Cursor) at import time and could not even load before.
  2. mpedb.adapt() exists, and the adapter chain (registered adapter for the exact type → __conform__ → named refusal) runs at bind time for non-base types — Django's own date/datetime/Decimal adapters win, exactly as in the stdlib.
  3. file:X?mode=memory&cache=shared (with uri=True) is now a real shared in-memory database — it used to silently become a preallocated 64 MiB file on disk in the consumer's working directory. Unsupported URI parameters refuse by name.
  4. Reads see your own uncommitted writes on the native engine: the connection's transaction is a real write session now (the old design buffered statements until commit, so a SELECT inside a transaction returned nothing for rows the same connection had just written — fatal for Django's TestCase, which wraps every test in a transaction it rolls back). Errors surface at execute(); holding the writer lock across the transaction is sqlite's own model.
  5. The missing surface: create_function (with deterministic), create_aggregate, create_collation, executescript (quote/comment-aware script splitting; trigger bodies carved whole), isolation_level (None = autocommit, explicit BEGIN/COMMIT/ROLLBACK drive the real transaction), in_transaction, cursor.lastrowid, and mpedb.Row + row_factory.

The PyPI wheels of this version are the first where the swap route can run a Django project's test suite; the LD_PRELOAD shim route was already there (see v0.2.2/v0.2.3).

Attached binaries

file contents
mpedb-0.2.4-linux-x86_64.tar.gz mpedb CLI, Linux x86-64
mpedb-0.2.4-macos-arm64.tar.gz mpedb CLI, macOS Apple Silicon
mpedb-0.2.4-windows-x86_64.zip mpedb.exe, Windows x86-64
libmpedb_sqlite3-0.2.4-linux-x86_64.tar.gz sqlite3 drop-in shim, Linux
libmpedb_sqlite3-0.2.4-macos-arm64.tar.gz sqlite3 drop-in shim, macOS
SHA256SUMS checksums for everything above

v0.2.3 — license fee correction (USD 0.07)

Choose a tag to compare

@punnerud punnerud released this 01 Aug 23:19

Identical to v0.2.2 in every line of code — see its notes for the release content (the spurious-corrupt read race fix, Django 4.2 catalog lookups, the pypi/README improvements).

The one change: the mpedb License 1.0 device fee for large enterprises reads USD 0.07 (seven US cents) per physical device, in 2026 US dollars — correcting the first cut's NOK figure so the fee and the USD 5 billion threshold share one currency and one inflation anchor. This version exists so that every artifact carrying the 0.2.3 number — PyPI wheels included — was built from a tree with the corrected terms.

Attached binaries

file contents
mpedb-0.2.3-linux-x86_64.tar.gz mpedb CLI, Linux x86-64
mpedb-0.2.3-macos-arm64.tar.gz mpedb CLI, macOS Apple Silicon
mpedb-0.2.3-windows-x86_64.zip mpedb.exe, Windows x86-64
libmpedb_sqlite3-0.2.3-linux-x86_64.tar.gz sqlite3 drop-in shim, Linux
libmpedb_sqlite3-0.2.3-macos-arm64.tar.gz sqlite3 drop-in shim, macOS
SHA256SUMS checksums for everything above

v0.2.2 — spurious corrupt fixed at the read; Django 4.2 catalog lookups; mpedb License 1.0

Choose a tag to compare

@punnerud punnerud released this 01 Aug 22:54

Verified on Linux x86-64, macOS/Apple Silicon and Windows x86-64 — tests, clippy and the crash harnesses green on all three. Prebuilt binaries attached (CLI for all three platforms + the sqlite3 drop-in shim; sha256sums below).

A spurious "database corrupt" under load, found by our own fuzz harness

The map-collide SIGKILL harness caught a reader dying with no valid meta page (both checksums invalid) on a perfectly healthy file — about 4 % of runs on a loaded box at durability = none. The adversarial review pinned the mechanism, and it was not the obvious suspect:

  • The commit protocol was not at fault: no ordering, fence, or publication change was needed, and the durability gate plus crash recovery came out of the review exactly as design/DESIGN.md contracts them.
  • The read tears, not the write: the meta reader loads a slot's checksum, then its body. Commits alternate between the two meta slots and cost microseconds at durability = none, so a reader descheduled inside one read can straddle two consecutive commits — slot A torn by commit N+1, slot B by commit N+2 — and a single-pass read reports both slots invalid.
  • The gated (durable) read path has always carried a bounded retry loop for its own version of this window, documented as load-bearing. The ungated path (durability = none/async) had a single pass. It now has the twin loop, with the same termination argument: retry while the observed slot state moves; a pass that sees the same state as the pass before admits nothing new, so the failure is genuine corruption.

Measured: 5 of 120 runs failed before the fix, 0 of 240 after, same loaded box (p < 1e-4 under the old rate). No SIGKILL is actually required to trigger it — the race is reader-vs-two-commits — so long-running read-heavy workloads at durability = none could hit it in production. Upgrading is recommended.

Django 4.2's catalog lookups answer now

The first Django 4.2 project switched onto the shim stalled at migration: 4.2's get_constraints() queries sqlite_master with a double-quoted name (WHERE type='table' and name="tblname") — sqlite's double-quote misfeature, where a quoted string that names no column falls back to a string literal. Django 5.x parameterizes the same lookup, which is why the 18,214-test Django suite never met the form. The shim's catalog evaluator now implements exactly the measured rule: literal fallback when no column matches (tables, indexes, IN-lists), and a refusal — never a guess — for the identifier half (name="name" is sqlite's always-true column self-compare, with no real-world consumer).

Two more Django 4.2 catalog forms are measured and queued, not yet closed: the recursive references_graph CTE its sql_flush walks, and UPDATE sqlite_master under writable_schema.

Also in this release

  • The pypi workflow got ~10 minutes cheaper per ordinary push: the wheel fan (including QEMU-emulated ARM builds) now runs only when the version is new to PyPI; the Python suite still runs on every push.
  • README: download, don't build — the Install section now leads with the prebuilt release binaries and spells out the LD_PRELOAD route for unchanged code (Django's manage.py test in one line), versus pip install mpedb for new code.

License change: the mpedb License 1.0

From this release the project is licensed under the mpedb License 1.0 (previously MIT): source-available, free of charge for every person and every organization, with one exception — a corporate group whose consolidated revenue or valuation exceeds USD 5 billion (2026 dollars, whichever is higher) owes a one-time USD 0.07 (seven US cents) per physical device (per connected device for server deployments, whichever count is higher), with a five-year tail once the threshold is crossed. See LICENSE for the exact terms.

Attached binaries

file contents
mpedb-0.2.2-linux-x86_64.tar.gz mpedb CLI, Linux x86-64
mpedb-0.2.2-macos-arm64.tar.gz mpedb CLI, macOS Apple Silicon
mpedb-0.2.2-windows-x86_64.zip mpedb.exe, Windows x86-64
libmpedb_sqlite3-0.2.2-linux-x86_64.tar.gz sqlite3 drop-in shim, Linux
libmpedb_sqlite3-0.2.2-macos-arm64.tar.gz sqlite3 drop-in shim, macOS
SHA256SUMS checksums for everything above