Macrame
A bitemporal graph ledger for knowledge management — embedded, single-file, no server.
Macrame stores concepts linked by typed, weighted relationships — where both concepts and relationships change over time, and the history of those changes is itself a first-class asset. Everything lives in one .db file on disk. No database server, no network protocol, no external service.
Why Macrame
| Strength | What it means |
|---|---|
| Bitemporal by design | Two independent clocks per row — valid time (when a fact held in the world) and transaction time (when the database learned it), each addressable on its own. as_of_valid(ts) answers "what did the world look like?"; as_of_recorded(ts) and reconstruct(ts) answer "what did we believe?"; setting both asks what we believed then about what was true then (D-174, 0.13.2). Historical subgraph loads hydrate belief-at-instant when the mode asks for it (D-289, 0.19.1). reconstruct_as_of folds the whole state on the valid axis, and a concept holds one belief per valid start with at most one open interval (D-291, D-292, 0.20.0). |
| Single file, embedded | The entire database is one file on the local filesystem. Link it directly into your application. Run on Windows desktop, Linux, or macOS — the Rust suite runs on all three in CI. |
| Graph + vectors + search | Recursive CTE traversal, native DiskANN vector search, FTS5 keyword search, and hybrid RRF fusion — all in one crate, no external graph library. |
| Five in-memory analytics | Dijkstra, A*, SCC, k-core, and Louvain — operating on a typed Subgraph with zero external dependencies. |
| Branching | A fork is a lineage of belief, not a copy: one row in branches, no ledger table read or written. A branch reads its ancestry as of its fork point, writes its own rows beside its parent's rather than over them, answers diff(a, b) against another lineage, and is abandoned in one transaction that takes its links, its concepts, its log entries and its own register row. Merge is refused, in writing — Doctrine III has no neutral answer to which assertion supersedes which (D-213 … D-232, 0.15.0). |
| Rebuildable materialization | links_current is a cache of current belief, always rebuildable from the append-only transaction_log. Drift is detectable by audit, recoverable by atomic or chunked rebuild. |
| Archival path | Closed intervals move to a cold database inside atomic sessions. Point-in-time reconstruction composes from snapshots plus anchored folds — fast because it doesn't fold from genesis. |
| Runtime safety | One Write Actor serialises all writes; read connections carry PRAGMA query_only = ON enforced at the engine level. No raw SQL escapes the guard. |
as_of became as_of_valid and as_of_recorded in 0.13.2, and this is a break.
Through 0.13.1 a single ts was compared against links.valid_from/valid_to — valid
time — for the graph's shape and against transaction_log.recorded_at — transaction time —
for concept attributes under AttributeMode::AtTime. So as_of("2020-06-01") returned the
edges that held in 2020 labelled with what was believed in 2020: a title corrected today
to fix a 2020 typo came back uncorrected. Right answer to "what did we believe"; wrong
answer to "what was true", and the name promised the second.
The semantics were written down a release before they were changed, deliberately, so the change could be reviewed against a stated position rather than argued in the commit that made it (D-160, 0.12.17 → D-174, 0.13.2).
To migrate: as_of(t) for topology-at-t under current belief is now
as_of_valid(t). as_of(t) paired with AttributeMode::AtTime, if what you wanted was
belief as of t, is now as_of_recorded(t). Setting both is new and asks the bitemporal
question. reconstruct() is unchanged.
as_of_recorded folds the hot transaction_log, so it raises RecordedInstantUnreachable
on a database whose log has been archived — it takes a connection and no archive path, and
answering from a partial fold would return nearly the right topology. AttributeMode::AtTime
folds the same log for the text and raises the same error for the same reason (0.13.16).
reconstruct() takes the path and answers the same question.
Quick Start
Rust
[dependencies]
macrame-db = "0.22"
use macrame::prelude::*;
async fn main() {
let db = Database::open("knowledge.db").await?;
db.upsert_concept(ConceptUpsert::new("quantum", "Quantum Computing")
.valid_from("2026-01-01T00:00:00.000000Z"))
.await?;
db.upsert_concept(ConceptUpsert::new("entanglement", "Quantum Entanglement")
.valid_from("2026-01-01T00:00:00.000000Z"))
.await?;
db.assert_edge(EdgeAssertion::new("quantum", "entanglement", "ENTAILS")
.valid_from("2026-01-01T00:00:00.000000Z")
.weight(1.0))
.await?;
let subgraph = db.traverse()
.start_node("quantum")
.max_depth(3)
.execute(db.read_conn(), None)
.await?;
}
Python
pip install macrame-db
import macrame
T0 = "2026-01-01T00:00:00.000000Z"
with macrame.Database.open("knowledge.db") as db:
db.write_concepts([
macrame.ConceptUpsert("quantum", "Quantum Computing", valid_from=T0),
macrame.ConceptUpsert("entanglement", "Quantum Entanglement", valid_from=T0),
])
db.assert_edge(
macrame.EdgeAssertion("quantum", "entanglement", "ENTAILS", valid_from=T0)
)
graph = db.load_subgraph("quantum", 3, 1 << 20)
print(graph.dijkstra("quantum"))
Architecture Highlights
Eight Doctrine Invariants
Every design decision derives from these invariants:
- The boundary is sacred — Everything above libSQL is ours; everything below it is upstream. Never patch the engine.
- Two clocks, never mixed — Valid time and transaction time are independent axes. No code path derives one from the other.
- Assertions are immutable — Rows in
linksare never updated in place. The past is never rewritten; it is only ever superseded. - The ledger is a table, not the log — Transaction-time reconstruction reads
transaction_log, not WAL or CDC frames. - No physical deletion in hot tables — Rows leave through the archive path only. Ad-hoc
DELETEaborts at the trigger layer. - Derivative state is disposable —
links_currentis a rebuildable materialization. Drift is detectable, recoverable by rebuild. - Embeddings are immutable per version, excluded from the ledger — Vectors live in per-model tables; they never appear in
transaction_logpayloads. - Fidelity is a parameter, never a silent default —
as_of_valid(ts),as_of_recorded(ts)andreconstruct(ts)say what they mean in their signatures.
Concurrency Model
- One writer — a dedicated Tokio task holds the sole write-capable connection
- Many readers — WAL journaling; readers never block on writer
- Two-tier priority channels — high-priority (user-driven) preempts low-priority (background)
- Shared as
Arc<Database>, not cloned — every method butclose()takes&self, andclose()takesselfbecause shutdown has exactly one owner;Arc::into_inner(db).expect("last handle").close().awaitis the pattern.Cloneis refused rather than missing:cadence_stopis a cloneablewatch::Senderwhose drop is what stops the snapshot task, so a second copy keeps it running against a closing database with nothing raising an error (D-203) - Cooperative chunking — bounded to ~3 ms per chunk, sized from each chunk's measured hold rather than from a constant, under four per-path ceilings (90 edges, 70 concepts, 600 annotations, 30 embeddings) and a 35-row floor
Operational Controls (0.13.0)
Everything below defaults to leave it alone. None of them spells "off" as the absent value, which is the failure the tri-state types exist to prevent (D-155) — a default that silently disables a mechanism reaches every caller who never heard of the knob.
| Control | What it does |
|---|---|
Tuning / open_tuned |
One struct for the open-time options, so the set can grow without breaking callers. The three existing constructors still compile unchanged. Python takes the same options as keyword arguments — it has defaults in the language, so there is no type to import (D-164) |
checkpoint() |
Moves WAL frames back into the main file and reports what it moved. Runs FULL then TRUNCATE, because a truncating checkpoint reports zeros on success and cannot describe its own work. Check busy before treating the file as self-contained (D-156) |
wal_autocheckpoint |
Default / Disabled / EveryPages(n). Disabling it is only correct paired with an explicit checkpoint(), and the cost is deferred rather than removed: ~8,400–9,100 frames in 41–45 ms at the end, against ~860 in 5.5–6.2 ms along the way (D-157). On a bulk import the threshold is the dial (measured, D-275): a 16,000-edge random-order import runs 5.0 s at the default, 3.7 s at EveryPages(10_000) with a ~43 MB WAL, 3.6 s disabled with a ~554 MB WAL — the recipe, numbers and shape caveat live in Database::bulk_import's docs |
writer_cache_size / reader_cache_size |
Split, because the writer is one connection and the readers are several — one number would mean starving the writer or multiplying the readers' footprint (D-158) |
writer_temp_store |
Memory (default) / File. Where the write connection keeps SQLite's temporary storage. In memory, an edge insert's statement journal never spills to a temporary file, which takes the 90-edge chunk from ~4 ms to ~2.2 ms; File gives the writer's whole-table sorts (rebuild_current, a migration's index builds) back to disk for a file too large to sort in RAM. Consistency is the same either way (D-306). |
analyze() / optimize() |
Planner statistics. Before 0.12.4 nothing here ever ran ANALYZE, so every query was costed against SQLite's built-in guesses. optimize() is a no-op when nothing has moved, which is why close() calls it (D-149) |
analyze() misses the ~3 ms chunk budget by ~6× on a populated ledger, and that is
reported rather than hidden. Measured: 19.1 ms at 40,000 edges. ANALYZE is one
indivisible statement, and PRAGMA analysis_limit = 400 damps its cost 3–4× without making
it independent of the table — the claim that it did was corrected in
D-166 after §8's acceptance asked for a
measurement instead of an argument. metrics().budget_violations() names analyze after
every call and is meant to. The exemption could not be decided while one kind covered both
analyze() and the optimize() that close() runs unprompted
(D-168); that split shipped in 0.13.24,
and both kinds stayed budget-counted for opposite reasons — Analyze's miss is structural and
constant, Optimize's is rare and therefore the informative one
(D-197).
Schema Versioning
| Version | Feature |
|---|---|
| v2 | Legacy-free baseline |
| v3 | analytics_annotations table |
| v4 | FTS5 external-content index |
| v5 | Overlap guard index |
| v6 | Overlapping closed intervals refused in actor |
| v7 | CHECK (weight >= 0.0) on links.weight |
| v8 | concepts.rowid_pk, the third FTS trigger, and the two unread indices dropped |
| v9 | trg_concepts_guard_delete becomes conditional on an archive session, so concepts can be archived (D-129) |
| v10 | trg_concepts_log_insert becomes conditional on the same marker, so rehydration mints no transaction-time facts (D-131) |
| v11 | idx_links_recorded_at and idx_links_target on the links ledger, so neither archive predicate scans it (D-151) |
| v12 | The branches register, branch_id on all four ledger tables with a real foreign key, links_current re-keyed per lineage, three log triggers redefined and four guards added (D-214…D-217) |
| v13 | trg_branches_frozen_delete becomes conditional on an archive session, so a lineage can be abandoned, and cold.branches arrives with it (D-230) |
| v14 | idx_lc_lineage_cut on links_current, the index the branched read seeks and the trunk walk does not (D-231) |
| v15 | links' primary key gains branch_id, last, and cold.links takes the same key (D-232) |
| v16 | log_integrity — one row, one bit: has anything ever been deleted from transaction_log? — seeded from the log it describes and maintained by trg_txlog_mark_gap, so the reach guard reads a bit instead of scanning the log on every recorded-time read (D-249) |
| v17 | idx_txlog_fold_partition, the index the fold's own partition asks for and not the one the review asked for (D-254) |
| v18 | idx_links_branch and idx_txlog_branch, partial over branch_id <> 'main', so the indexes archive_branch scans hold only what branches wrote and the trunk's write path pays nothing (D-273) |
| v19 | trg_links_single_open's branch predicate carries a unary +, so on a database without statistics the probe seeks idx_lc_open_interval instead of scanning a lineage. Trigger-only (D-274) |
| v20 | kv_store: operational state with no log trigger, no archive membership and no branch_id (D-280) |
| v21 | concepts.extra, a JSON object logged like every other column; concepts payload v3 (D-278) |
| v22 | blobs and idx_blobs_put_at, with an update guard and an archive-gated delete guard; cold.blobs arrives with the first archive session (D-281, D-288) |
| v23 | idx_txlog_concept_starts, partial over the log's concept rows, so the concept interval guard seeks one row per valid start; idx_txlog_entity dropped. Index-only (D-300, D-301) |
| v24 | The state rule: concept_disorder, the marks a backfill leaves and the concept reads probe; idx_txlog_concept_starts rebuilt with valid_from DESC so the correction's winner rides the index; every projection a backfill overwrote re-derived from its rule row (D-303, D-305) |
| v25 | edge_kinds, the register of functional edge kinds: one CREATE TABLE nothing references and which references nothing; an unregistered kind keeps its old behaviour (D-309) |
| v26 | the concept overlay, additive (D-314): concept_versions (WITHOUT ROWID, PK (id, branch_id, valid_from)) and append-only concept_cuts, their write-log triggers and guards, and per-model embeddings_* beside every registered model — the rung sketch was byte-checked against a populated v25 file first (M6-P1) |
| v27 | the overlay's row laws as guards (D-314, revision 3): a version row's recorded_at is monotonic and its start (id, branch_id, valid_from) is immutable; a cut refuses UPDATE (a cut is a decision, not a window) |
| v28 | the version pool's search machinery (M6-c): the rowid-bearing shadow and the FTS index over it from the same constants the baseline issues, the import pair on concept_versions, the shadow's maintenance triggers, and a backfill of the shadow from any version rows a pre-v28 file already holds — one INSERT ... SELECT per row so the shadow's FTS import fires per row (D-314) |
| v29 | trg_concept_versions_single_open reissued from the widened constant: a bounded insert written under an open own row refuses — the pre-v29 files carried the start-only check, which is the letter of the guard's sentence without the law's shape (0.22.0's post-review fix, the confirmed M6 finding); the trunk walk folds its past for a named instant (D-316) — current |
open() climbs the ladder one rung at a time, each in its own BEGIN IMMEDIATE transaction. A database stamped newer than the build is refused, never downgraded.
v15 is the last rung that changed a primary key before the 1.0 freeze, and v8 was the one before it. Since then, v16, v20, v22, v24, v25, v26 and v28 add tables, v21 adds a column, v19 redefines a trigger, v27 and v29 reissue triggers from the baseline's constants, and v17, v18 and v23 change indexes. None of them touches a key. D-036 forbids a primary-key diff after 1.0, and D-032 is what reserves the pre-1.0 window for exactly this: v15 widens links because two lineages are allowed to believe different things about one edge, and the old key refused that pair with a bare UNIQUE error naming a storage key the caller has never seen. branch_id goes last, so the five-column covering seek the archive sweep runs per candidate row survives the change. On v8: rowid_pk INTEGER PRIMARY KEY costs id the primary key, and D-036 forbids a primary-key change after 1.0 (D-119). It also drops idx_annotations_label and idx_lc_tgt_active, which shipped in the v7 baseline with no query that seeks on them — measured at −7.9% off assert_edge (D-089, D-118).
Rust Implementation
| Detail | Value |
|---|---|
| Edition | Rust 2021 |
| MSRV | 1.88 (verified, not declared) |
| Runtime | tokio async, single process |
| Engine | libSQL 0.10.0 (MIT, unmodified) |
| Schema version | 29 |
| Test suite | 1,048 Rust · 1,026 with --no-default-features · 1,074 with property-tests · 752 Python — all green on the 0.22.0 release sweep (scripts/run_rust_suite.py on the cycle's branch, 2026-11-10: default across 80 targets, feature-off across 79, the quarantined property arm across 85, Python in full with the rebuilt wheel; every sweep finished at attempt 1) and replicated on CI at the 0.21.0 release commit, run 41299841786: 904 on ubuntu, macOS and Windows, 882 on ubuntu, the quarantined property arm 928 across 72 targets on all three, every step at attempt 1 — the first release at which the property arm passed on Windows, on libSQL 0.10.0; Python 696 / 3 skipped on all three runners (run 41299841820; one box reads 697 / 2). 0.19.0 closed at 817 / 795 / 681. 0.20.0 closed at 873 / 851 / 695, and 0.21.0's +31 / +31 is the state-rule suite's eighteen, the v24 rung's eight migration cases, gate 7's two composed-path cases, and D-306's three temp-store cases — no feature-gated test is new, so both Rust figures move together. 0.18.0 closed at 797 / 775 / 666, and 0.19.0's +20 / +20 / +15 are all P4's (D-281); 0.18.0's per-item deltas — 769 → 770 → 781 → 794 → 797 — are in the register at D-279, D-280 and D-278 and D-286, each measured at its own commit. The replicated figures before these were 0.17.0's — 769 / 747 / 616, measured 2026-09-11 and replicated on all three CI runners at that release commit itself — run 34626309278: 769 on ubuntu and macOS at attempt 1/3, 769 on Windows at attempt 2/8, the feature-off 747 on ubuntu at attempt 1/3, and the quarantined property arm 793 across 64 targets at attempt 1/6 on ubuntu and macOS with crashed-R15 at 6/6 on Windows, which is D-236's read and the reason that step is non-blocking. Those figures are replicated; the ones this line carried through 0.16.3 were the 0.16.0 release's — 757 / 736 / 607, run 34155656904 — and the patch releases between carried their own counts in the register (D-274…D-277) rather than re-arguing this line per release. Read D-234 and D-262 before quoting any earlier release's version of this line as replicated: D-243 … D-261 shipped with no CI run of any kind, because the branch filter did not reach dev/** and the draft pull request the policy relies on was never opened). metrics is a default feature since 0.12.11, so the first figure is a plain cargo test; the second is the same suite with the counters compiled out, and the 22-test gap is actor_metrics_tests (14, the whole target), src/metrics.rs's five unit tests, one gated test in checkpoint_tests, one gated test in bulk_deferred_tests (D-277's exemption assertion; D-276's exemption assertions are gated inside vector_tests, whole-test count unchanged), and the doc-test on Database::metrics — enumerated at 0.14.23 by diffing cargo test -- --list against the same list under --no-default-features, which is the only way this sentence stays true as the metrics target grows. The second figure is a CI gate since 0.14.24 (D-241): the feature-off suite is run on ubuntu, not merely compiled, because until then nothing could falsify the number this line publishes. Regenerate rather than trust this line: python scripts/run_rust_suite.py, and python scripts/run_rust_suite.py --no-default-features for the second — a bare cargo test --no-default-features stops at the first R15 crash and reports the partial count as the total |
| Dependencies | libsql, tokio, serde, serde_json, bincode, zstd, thiserror, tracing, ulid, sha2 |
Module Map
| Module | Responsibility |
|---|---|
schema |
DDL, triggers, migrations |
graph |
CTE compilation, subgraph loading, vector filters |
temporal |
as_of_valid(), as_of_recorded(), reconstruct(), reconstruct_as_of(), snapshots, archive, rehydrate |
vector |
Model registration, embedding upsert, DiskANN search, hybrid RRF |
integrity |
Audit, atomic rebuild, chunked shadow-swap rebuild |
branch |
BranchId, Branch, BranchView, fork, diff, lineage resolution |
connection |
Database handle, Write Actor, priority channels |
error |
DbError enum, error classification |
Python Bindings (v0.16.0)
| Detail | Value |
|---|---|
| Engine | pyo3 0.29 + maturin |
| Surface | Synchronous (Write Actor serialises all writes) |
| GIL | Released via Python::detach around Runtime::block_on |
| Distribution | macrame-db on PyPI, import macrame |
| Wheels | abi3-py310 — one per platform (Linux x86_64/aarch64, macOS universal2, Windows x86_64) |
| Python | CPython 3.10+ |
| Type stubs | Ship with wheel, py.typed set, mypy --strict in CI |
Key design decisions
- Synchronous surface — The Write Actor serialises every write through one channel, so exposing
awaitadvertises concurrency the architecture does not grant. - Opaque
Subgraph— A#[pyclass]with forwarded accessors;.to_dict()for callers who want the copy. It paid for itself in 0.8.0: the crate re-representedEdgeRefand no binding signature moved, because there is no converted copy whose layout had to follow (D-101, D-123). - Open intervals cross as
None— Not a sentinel datetime, becausedatetime.maxcannot survive.astimezone()east of UTC. - Absent
contentcrosses asNone—load_subgraphdoes not fetch document text unless asked (content=True).""cannot mark not loaded, because it is a valid value of the type (D-116, D-123). - Every error is typed — 58 exception classes under
MacrameError(59 including the base), of which seven are intermediate groups for catching sets —IntegrityError,ValidationError,VectorError,TemporalError,WriterError,BudgetError,BranchError— leaving 51 leaves.BranchErroris 0.15.0's, over the six refusals a lineage can raise.DbError::kind()returns one of twelveErrorKindvalues — the same seven groups plus the five ungrouped failures, so the Rust and Python taxonomies are one thing rather than two — and its match has no wildcard arm, so a variant added without a classification does not compile (D-242). The exception class is a step further: a variant added without one is caught bybinding_parity_tests, which runs rather than compiles:DbErroris#[non_exhaustive]since 0.13.34, so the mapping's wildcard arm is mandatory and the compiler no longer checks completeness (D-207). metricsshipped on — The wheel ships with themetricsfeature enabled because feature flags do not survive into binary artifacts. It has been a Rust default since 0.12.11 too, so the two sides no longer differ (D-154).- Parity is a wave, not a side effect — 0.13.0 closed six gaps at once (W6): the archive-session and chunk-row constants,
registered_models()/declared_dimension(), the maintenance calls and tuning keywords above, and a clock seam for tests. A gap opened in the release that created the feature is the one that never becomes a convention — the constants had gone eight releases for want of being anyone's next task. - The chunk-row constants are ceilings, not sizes —
CHUNK_ROWS_EDGESand its three siblings bound the adaptive loop from above; a populated database converges below them. Dividing a batch by one to predict transaction count reads a 0.11.0 fact (D-161). - Three methods are deliberately absent —
raw(),read_conn()andshadow_step. The first two would dissolve the single-writer property or hand out a shared connection; the third is safe in Rust but its epoch obligation would cross as a convention where it is currently a type, on a method whose failure mode is a stale projection swapped over a live one without erroring (D-165). A test asserts all three stay absent, so adding one means answering the register rather than deleting a line.
Performance (measured, not gated)
Measured at 0.21.0 on the reference machine (Windows 11, NVMe SSD, 32 GB RAM, release build):
the median of three criterion sessions of benches/budgets.rs, with the control/select_1 row
reading 1.45–1.78 µs in every group. Each figure is the operation alone; fixture setup and
teardown sit outside the timed region
(D-302). The bench fixtures are smaller than
the populations some budgets name (2,000 concepts, a few thousand edges and log rows), so a met row
is met at bench scale.
| Operation | Budget | Measured | |
|---|---|---|---|
| Single edge assertion | ≤ 5 ms | 186 µs | Met |
| Single edge assertion into 0 / 2,000 / 8,000-edge tables | ≤ 5 ms | 327 / 251 / 290 µs | Met; flat in out-degree |
| Single concept upsert | ≤ 3 ms | 360 µs | Met; flat in the concept's history |
| Chunk commit, edges, 90 rows | ≤ 3 ms | 2.22 ms | Met |
| Chunk commit, edges, 90 rows into an 8,000-edge table | ≤ 3 ms | 3.01 ms | At the bound; see below |
| Chunk commit, concepts, 70 rows | ≤ 3 ms | 2.64 ms | Met |
| Chunk commit, annotations, 600 rows | ≤ 3 ms | 1.31 ms | Met |
| Chunk commit, embeddings, 30 rows | ≤ 3 ms | 1.28 ms | Met |
| Three-hop traversal, warm | ≤ 10 ms | 1.61 ms | Met |
as_of_valid traversal |
≤ 15 ms | 753 µs | Met |
| Vector top-10 | ≤ 20 ms | 267 µs | Met |
| Hybrid top-10 (vector + FTS5 + RRF) | ≤ 50 ms | 1.80 ms | Met |
Full fold (reconstruct) |
≤ 100 ms | 16.8 ms | Met |
| Snapshot + delta composition | ≤ 100 ms | 3.00 ms | Met |
audit_current |
≤ 200 ms | 12.1 ms | Met |
rebuild_current |
≤ 500 ms | 26.5 ms | Met |
| Snapshot write | ≤ 2 s | 1.83 ms | Met |
| Rehydrate, 1 concept | ≤ 5 ms | 2.87 ms | Met |
| Rehydrate, per concept after the first | ≤ 300 µs | ~68 µs to n=1,000; 108 µs at n=10,000 | Met |
The 90-row edge chunk meets its budget on an empty table and sits at it on a populated one.
Until 0.21.0 it took ~4 ms, and 9–12 ms into 8,000 edges: every edge insert fires three triggers,
and the undo journal SQLite keeps for that one statement spilled to a temporary file on every
row. The writer now keeps its temporary storage in memory (Tuning::writer_temp_store, default
Memory, D-306), which takes the two rows to 2.22 ms and 3.01 ms (2.88–3.12 ms
across the three sessions) with consistency and durability unchanged. 90 is still the ceiling
bulk_import's chunk loop starts from, not the size it commits: the actor times each transaction
and sizes the next from it (D-143, D-146), so a chunk that runs over shrinks
the one after it.
Three costs are shapes rather than points, and those are what to rely on. A single edge
assertion does not grow with out-degree: the rows above span hub out-degrees of 0, 666 and 2,666.
Its real cost is the number of versions per edge key, which archival caps
(D-134). A concept upsert does not grow with the
concept's history: examples/concept_guard_cost_probe.rs measures 0.27 ms for a concept with
10,000 versions (D-300). Rehydration is linear
to about 1,000 concepts per call and dearer per concept beyond that, because FTS5's merge cost is
per transaction.
How to read the numbers. Session-to-session spread on this machine reaches ~29%
(D-070), so read shapes and orders of
magnitude, and treat a smaller move between two runs as noise. The budgets are deliberately not
CI gates (D-055): an absolute ≤ 5 ms on a
shared runner is a claim about whichever machine picked up the job. Regressions are found by
comparing a machine with itself:
cargo bench --bench budgets -- --save-baseline before
then --baseline before after the change. Hardware-independent properties, such as plan shape,
operation counts and growth ratios, are gated in tests/. The full budget table, with each row's
mechanism and the figures earlier releases measured, is
§9 of the architecture docs.
Figures before 0.20.0 include fixture teardown and do not compare directly with these.
A second harness answers a different question. The budgets above are about operations this crate bounds; benchmarks/ runs a TPC-BiH-style workload taxonomy (TPCTC'13 T/K/R classes) over a generated bitemporal history, on the trunk and on a divergent lineage, in Python and in Rust — 13 correctness assertions gate every run, because a benchmark that can time a wrong answer is worse than none. It is a separate package: nothing there is built by an ordinary cargo test here, and none of it ships in the crate. Its headline shapes are that fork latency is flat across 3× the data (empirical O(1)), that transaction-dimension work is an order pricier than valid-dimension work, that branch cost is ancestry rather than divergence — a fork with no writes costs what a diverged one costs — and that chain reads stay flat from depth 1 to 5 and cost 15% more at depth 10. A third instrument lives beside them: benchmarks/diagnostics/ is the spike ladder that attributed and fixed the fresh-file bulk-import defect (D-274, D-275), and it is where the WAL recipe on the bulk path was measured; the vector-build dissection that produced bulk_embeddings (D-276) ran as examples/vector_build_probe.rs. The materialization-skipping bulk (D-277) was scoped by examples/bulk_skip_probe.rs — the mirror trigger is 2.2× of the random-pair bulk, and the chunked rebuild re-derives the projection for 191 ms per 16k.
Known Risks
| Risk | Mitigation |
|---|---|
R15: cumulative connect() → access violation (libSQL 0.9.30; quieter on 0.10.0) |
An intermittent STATUS_ACCESS_VIOLATION in the engine after many opens in one process; it is the engine, not the tests, and it reproduces through Python. Keep one open per database. The test runners classify each run and retry only a crash; a binary that fails when run alone is a real failure (D-147). In 0.20.0's suite runs it took out one or two test binaries per attempt and never failed an assertion; 0.21.0's five-by-five confirmed the rate moves between releases — clean release builds, same machine and load: 0.20.0 at 0.6 crash-runs per suite, 0.19.1 at 0.2 (the 0.21.0 plan's P3, measurement-only); the fix is Road to 1.0 W1. 0.21.0 moved the engine to libSQL 0.10.0: chunk_scaling/edges/1000, the budgets bench that reproduces R15 on demand, crashed 6/10 on 0.20.0 and 6/10 on 0.21.0 built against libSQL 0.9.30, and 0/10 against 0.10.0, and three full bench sessions ran clean. .cargo/config.toml documents the R15 measurements and the harness settings they led to |
| Property tests fault mid-suite | Each generated case opens a database, the most R15-exposed shape in the repository, so --features property-tests runs as its own CI step behind the feature gate (D-148) |
| Overlaps already in a ledger are reported, not repaired | A ledger written before the interval guard, or by raw SQL, keeps its overlaps. verify_concept_intervals() names them, and a read refuses only the instants both beliefs claim |
| The 90-row edge chunk sits at 3 ms on a populated table | 2.22 ms on an empty table and 3.01 ms into 8,000 edges (2.88–3.12 ms across sessions) since the writer's statement journal stopped spilling to disk (D-306); with TempStorePolicy::File the earlier ~4 ms and 9–12 ms apply. bulk_import sizes chunks on elapsed time from that ceiling, so a chunk over the bound shrinks the next (D-143, D-301). Re-priced index by index in 0.21.0 (D-304): the three transaction_log indexes are load-bearing on the write itself (dropping one costs up to 2.5 ms), and the cheapest real candidate saves 0.84 ms against a read whose cheaper alternative was once 68 s — none of it worth trading |
| Covering index wins over selective | EXPLAIN QUERY PLAN assertions on every index-sensitive query, plus pins on the plans ANALYZE statistics have moved |
| Snapshot chain divergence | verify_snapshot_chain() reports but does not repair (snapshots are disposable) |
--all-features is not a configuration this project supports or gates, and 0.10.0 stopped publishing a test count for it. --all-features is metrics + property-tests together, which puts the R15-prone binaries back inside the main run — the exact arrangement the step above exists to avoid. Measured 2026-08-07: 4 of 4 runs crashed at one attempt, and 4 of 5 still went red at the six-attempt retry budget the quarantined step uses. A required job that fails four times in five is not a gate, it is noise that teaches people to re-run CI without reading it. Run --features metrics and --features property-tests as the two separate steps CI does (D-140).
Minimum Supported Rust Version
1.88, verified rather than declared — cargo +1.88.0 check --all-features --all-targets passes and 1.85 does not. The constraint comes from libsql-ffi's build dependency chain (bindgen → which → home), not from this crate's own code (which needs only 1.73).
Documentation
- Architecture specification — normative surfaces: §4 (schema) and Appendix A (API)
- Architecture Quick Reference — API, schema, decisions, performance. Current to v0.22.0: the header and footer name the release, the schema ladder §4 reads v29 against
SCHEMA_VERSIONinsrc/schema/migrations.rs, and §5's API prose carries the surface through the overlay and the adoption law (D-289…D-316).tests/doc_currency_tests.rsgates those four claims — header, footer, ladder rung, and how far the newest decision cited may trail the register — andtests/doc_sync_tests.rsandtests/perf_claim_tests.rspin the analytics types, theSubgraphaccessors and the performance numbers. None of them can check that a passage is true, so where this disagrees with the architecture set, the architecture set wins — it is the normative one. - Python bindings — §14: async→sync boundary, error tree, stubs
- Decision register — D-001…D-313 with rationale
- Release notes — one document per minor, most recently v0.22.0 (the overlay, the walk and adoption: D-307…D-316, per-lineage concept versions behind the single-lineage switch, adoption across lineages, the change feed, one-write supersession and the conditional pointer move, schema v25…v29, two breaking changes), v0.21.0 (one state rule for concepts: D-303…D-305,
concept_disorderand the rung that repairs 0.20.0's projections, old snapshots skipped, one breaking change the user spent), v0.20.0 (the valid axis for state: D-291…D-302,reconstruct_as_of, the concept interval guard andverify_concept_intervals, one runtime refusal that is new), v0.19.0 (the blob store: D-281, D-287 and D-288, content-addressed bytes reclaimed by the archive rather than a refcount) and v0.18.0 (the flexible ledger: D-278…D-286,concepts.extra, the KV sidecar, the relaxed edge-kind charset and opt-in attributes on the subgraph loader)
Naming
Distribution macrame-db, import macrame — on both crates.io and PyPI. The Rust side has no caveat: a crate's [lib] name is namespaced per build graph, so macrame-db providing macrame collides with nothing. site-packages is flat.
The PyPI package macrame is an unrelated, effectively abandoned build tool (0.0.1, 2021). If it installs a top-level macrame/, then installing both leaves two distributions contending for one directory — pip warns on file conflicts, so this is a known and non-silent risk. Importing as macrame_db is the fallback if it ever matters.
License
See LICENSE for details.
Metadata
Release files for macrame-db 0.22.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 | Provenance | Actions |
|---|---|---|---|---|
| macrame_db-0.22.0.tar.gz | 3.2 MB | With provenance | Details for macrame_db-0.22.0.tar.gz |
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| macrame_db-0.22.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details for macrame_db-0.22.0-cp310-abi3-win_amd64.whl |
| macrame_db-0.22.0-cp310-abi3-manylinux_2_28_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.28+ x86-64 | Details for macrame_db-0.22.0-cp310-abi3-manylinux_2_28_x86_64.whl |
| macrame_db-0.22.0-cp310-abi3-manylinux_2_28_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.28+ ARM64 | Details for macrame_db-0.22.0-cp310-abi3-manylinux_2_28_aarch64.whl |
| macrame_db-0.22.0-cp310-abi3-macosx_10_12_universal2.macosx_10_12_x86_64.macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64, macOS 10.12+ universal2 (ARM64, x86-64), macOS 10.12+ x86-64 | Details for macrame_db-0.22.0-cp310-abi3-macosx_10_12_universal2.macosx_10_12_x86_64.macosx_11_0_arm64.whl |
Total release size: 33.9 MB
Release files / macrame_db-0.22.0.tar.gz
| Download URL | macrame_db-0.22.0.tar.gz |
|---|---|
| Size | 3.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c3aacc80dc552eaeae645c687468951e1855903e33ce205b19f4eb30cc7a27ce
|
|
BLAKE2b-256 checksum How to use checksums |
04206245f2b5d9efcea9985909bfd87bc7146907978f1c486f35ece9cc988c5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logRelease files / macrame_db-0.22.0-cp310-abi3-win_amd64.whl
| Download URL | macrame_db-0.22.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 6.2 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
2208ff7258ad873e50c1b49775efd6ea109f4010cbc7079e3f4ac3173fd00b87
|
|
BLAKE2b-256 checksum How to use checksums |
b0096e88b27a0f6e5dab60f677cab849930254e5e516ffd7e1e08422c18892e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logRelease files / macrame_db-0.22.0-cp310-abi3-manylinux_2_28_x86_64.whl
| Download URL | macrame_db-0.22.0-cp310-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 6.6 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
209a244877836fbfae9c661370e1845c98a003d34dcf059b793e25428e1ca983
|
|
BLAKE2b-256 checksum How to use checksums |
1cfc91efce5e8617bd79e4eaf17e303fc8c414fcef94ea4defb5e8673a37092b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logRelease files / macrame_db-0.22.0-cp310-abi3-manylinux_2_28_aarch64.whl
| Download URL | macrame_db-0.22.0-cp310-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 6.3 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
7c6b7a1f606313a17ce5187531ac55e8df0637d6a5a2397ab58c69d43a08acf9
|
|
BLAKE2b-256 checksum How to use checksums |
da15786943d595801248398e7b50c7144533b7b61e79fc75fce3dafdb3d91623
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logRelease files / macrame_db-0.22.0-cp310-abi3-macosx_10_12_universal2.macosx_10_12_x86_64.macosx_11_0_arm64.whl
| Download URL | macrame_db-0.22.0-cp310-abi3-macosx_10_12_universal2.macosx_10_12_x86_64.macosx_11_0_arm64.whl |
|---|---|
| Size | 11.8 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
8ece93a6afc94f6adc80c12674a89dca9ef949823f1749b562f3770a0c334a34
|
|
BLAKE2b-256 checksum How to use checksums |
182eb6af0fd0fb2d900c5e45a10a9b25404d5086d09e594703f7a110cfaaf690
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logSecurity for macrame-db 0.22.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 5 files in this release have been uploaded with provenance metadata.
Attestation for macrame_db-0.22.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.