mirk-store
The Python port of @mirk/store: substrate-agnostic key-value and collection
storage primitives: the KV, collection, vector, search, and graph ports over two
backends, an in-memory reference and SQLite.
The SQLite adapter opens files the TypeScript adapter wrote and writes files it can read. Same tables, same pragmas, same JSON encoding, and the same atomic bookkeeping rows on every write.
Install
uv add mirk-store
The import package is mirk.store, inside the shared mirk namespace. The unrelated mirk
distribution on PyPI uses the same top-level name, so do not install it in the same environment.
Zero runtime dependencies. Every port, the vector one included, needs nothing beyond the standard library.
Namespace package
mirk is a PEP 420 namespace package. There is no src/mirk/__init__.py, so a
later mirk-fixtures distribution can install mirk.fixtures beside
mirk.store the way the npm @mirk/* scope works. Do not add one.
Use
from mirk.store import InMemoryStore, SqliteStore, namespace_store
store = SqliteStore("data.db")
store.set("greeting", {"hello": "world"})
store.put("things", {"id": "t1", "weight": 2})
store.list("things", {"sortBy": "weight", "sortDir": "desc"})
store.close()
Method names keep the TypeScript camelCase spelling (getById, listWhereIn)
so a corpus op string dispatches identically in both languages.
Threads and transactions
One connection per thread; sync by design. SqliteStore is thread-affine, the
way sqlite3 opens a connection by default and the way the TypeScript adapter's
single-threaded model works. A store used from a second thread raises
sqlite3.ProgrammingError. Build a second store for a second thread.
The store owns transaction semantics on whatever connection it is given. Every
write runs inside an explicit BEGIN IMMEDIATE, so a connection you supply is
switched to isolation_level = None (SQLite autocommit). That commits any
transaction you left pending on it.
connection = sqlite3.connect("data.db")
store = SqliteStore("data.db", connection=connection) # takes over autocommit
File layout and schema version
A SQLite file carries a table registry, _mirk_tables(kind, name, table_name),
that maps a logical name to its physical table. kind is collection or
search. The physical name is still derived from the logical one, sanitized and
hashed, but that derived name is only the first candidate: when a different
logical name already holds it, the next candidate appends _2, _3, and so on,
past every candidate another name holds or an unregistered table already sits on.
Two collections whose names sanitize and hash alike therefore get two tables
instead of silently sharing one, and a stray table is never absorbed by a
suffixed candidate. The FTS index for a search collection is named
after its docs table, so one registry row governs both.
A file written before the registry existed keeps working. The first open records the table it already has under its logical name, in place, with no rewrite.
_mirk_meta holds schema_version, currently 2. Opening a file whose version
is higher than this adapter understands raises rather than reading it by rules
that no longer apply. The TypeScript adapter uses the same registry, the same
candidate sequence, and the same version, so both languages resolve a shared file
to the same tables. When two connections race to register the same new logical
name, the loser's _mirk_tables insert raises a constraint violation; both
languages catch it and restart resolution, up to five attempts, so the loser
ends up reading the winner's row as a registry hit instead of surfacing the
error from an ordinary write.
The contract
The corpus at conformance/ in the repository root is the contract. Both the
TypeScript suite and this package replay every scenario against every backend
they implement. A behavior that is not in the corpus is not contractual, and a
behavior that differs between backends is a bug in one of them.
Known differences from TypeScript
These are outcomes a user of both languages can actually hit. Where the contract picked a side, the TypeScript side listed here is the one that changed to match; where it says "not pinned," both languages may keep differing.
- Mutation after
put/getis undefined. The Python in-memory store copies records on write and on read; the TypeScript in-memory store hands out live references. A consumer that mutates an object afterput, or mutates a value returned fromget/list, gets different results by language today. Do not rely on it either way. - Integers above 2^53 are out of the shared contract: a Python
intbeyond that range is stored as its nearest float64, matching how numbers round-trip through the TypeScript adapter's JSON. Not pinned in the corpus. NaNand infinite floats are rejected, not converted. Python's encoder raises rather than silently writingnull(which is what TypeScript'sJSON.stringifydoes with those values).- Lone surrogates (an unpaired UTF-16 code unit) are contractual as
stored VALUES on both languages, escaped the same way in JSON. They are not
contractual as identifiers: a key, record id, collection name, or filter
value containing one is bound as SQLite TEXT. Python's
sqlite3refuses to encode it; TypeScript's better-sqlite3 silently replaces it with U+FFFD. keys()ordering,countwithlimit/offset, negativelimit,where/listWhereInon non-scalar values, and boolean vs.1distinguishing all had a TypeScript backend disagree with itself (memory vs. SQLite) before this port; the corpus now pins one answer for both languages and both backends:keys()sorts by code point;countignoressortBy/limit/offset; a negativelimitreturns nothing;where/listWhereInon an object or array value throwsStore filters only support JSON scalar values.; andwhere {v: true}never matches a stored1.- Sort ties land in insertion order on both backends; SQLite achieves
this with
rowidas the finalORDER BYkey, which is equivalent to insertion order for every observable case. - Mixed number/string values in one sort field are unspecified — not pinned in the corpus, and JavaScript's coercion and SQLite's type ranking can disagree. Don't sort a field holding both types and expect a particular order.
- Search tokenization diverges deliberately and is not fixed. The SQLite
FTS5 tokenizer strips diacritics (
caféindexes ascafe); the in-memory tokenizer keeps them, in both languages. No document mixing the two is in the corpus. Don't compare indexed diacritics across backends. - The vector facet has no accelerated path.
SqliteAdapter.vectoralways computes exact float64 cosine similarity; there is nosqlite-vecextension to install and novecextra.meta.acceleratedis alwaysfalse. - Physical table naming can collide only in principle, not in practice.
Two collection or search names that sanitize and hash to the same physical
name still get two distinct tables: the registry described above appends
_2,_3, and so on for a name that would otherwise collide.
Adding a port
The conformance runner resolves a scenario's port to a target by convention, so
a port author never edits src/mirk/store/conformance/runner.py. The names
store, kv and collection mean the backend store itself. Any other port
name p is resolved by importing mirk.store.<p> and calling its module-level
factory:
def conformance_target(backend: str, connection: object) -> object: ...
backend is "memory" or "sqlite". connection is that backend's open store
handle, so a SQLite facet shares the connection the runner already opened
instead of opening a second one against the same file. A missing module or a
missing factory makes the scenario a recorded skip, counted per port in the test
summary. ALLOWED_SKIPPED_PORTS in tests/test_conformance.py lists the ports
that may still be missing; any other skip is a failure.
tests/test_sqlite_compat.py exports run_node_script for cross-language
tests: hand it ESM source and argv, get back the JSON its last stdout line
printed.
Tests
uv sync --group dev
uv run pytest -q
uv run pyright
uv run ruff check .
The cross-language tests need node and a built packages/store/dist. Build it
with pnpm --filter @mirk/store build from the repository root.
Release files for mirk-store 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mirk_store-0.1.0.tar.gz | 99.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mirk_store-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 167.2 kB
Release files / mirk_store-0.1.0.tar.gz
| Download URL | mirk_store-0.1.0.tar.gz |
|---|---|
| Size | 99.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2ca4540f0fe9c6b8280d53541dcd80baca0d62fabceab625fc3985d8e56f13c5
|
|
BLAKE2b-256 checksum How to use checksums |
a5c11280afc1b205f318c314fa20a8cffe2b82e8a0601913c3703d7716c088dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / mirk_store-0.1.0-py3-none-any.whl
| Download URL | mirk_store-0.1.0-py3-none-any.whl |
|---|---|
| Size | 67.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ca8dbd622e7bd21c877ff2c9306a645a2d16774e640be9e7c701380a8a8ddcfc
|
|
BLAKE2b-256 checksum How to use checksums |
7bd4fa80070c0540c1ad954c22cc0f3f738b8cd90976213eb9e7cc3e4d620efb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|