HybridDB 0.6.0 — versioned tables, after the reliability war
Before we could promise 'history you can verify,' we first had to make the journal boring enough to trust. This is that release, and the war that preceded it.
There’s a moment in every database project where you realize you’re about to promise something dangerous. For HybridDB, it was this: every change, verifiable, forever. Agent memory was the use case we were actually building for, and agent memory can’t have a delete that forgets it deleted — an audit trail with holes isn’t an audit trail.
But we couldn’t ship hash-chained history on top of a journal we didn’t fully trust. So 0.6.0 is really two releases wearing one number: the versioned-table system, and the quiet war that made it possible to build it at all.
The reliability war (0.5.5 and 0.5.6)
The journal is the thing that keeps ChromaDB, FTS5, and SQLite honest with each other — and in 0.5.x, it was the thing that lied most. Two releases fixed the lies:
- v0.5.5 hunted a family of graph bugs: synced node ids were namespaced by table (
{table}:{pk}), because two tables sharing a primary key value silently overwrote each other’s graph nodes — a data-loss bug you only catch by comparing node counts you weren’t measuring. Label templates rendered every{column}at last, ghost nodes got swept, andsearch_graph_pprstopped routing PageRank through the directed graph where connected nodes scored ~0. - v0.5.6 made the journal chronological with last-op-wins. The bug it killed is the kind that lives in your nightmares: delete-then-reinsert on a TEXT-PK table reused a rowid, produced duplicate vector keys, and wedged the journal permanently — every search after that raised. It also fixed the silent ones: FTS5 indexes that stopped returning results after a rebuild, DuckDB mirrors that crashed on custom primary keys,
read_querythat could write through aWITHclause. The reliability release, honestly named.
Then: versioned tables
With the journal behaving, 0.6.0 added the good stuff:
create_table(..., versioned=True, hash_chain=True)— every insert/update/delete appends a post-image or tombstone to a shadow__historytable, chained withSHA256(prev_hash | op | pk | row_json).upsert,log,history(key),as_of(seq),diff(from, to)— time-travel you can show a compliance officer.checkpoint(label)/rollback(...)— and the design decision that matters: rollback re-applies state as new versions. The chain never rewinds, so rewinding your agent is itself an auditable event.verify_chain()detects someone editing the history store directly.archive()exports to jsonl or parquet with chain anchors so pruning never breaks verifiability.
That last one deserves emphasis: you can prune old history without losing the ability to verify what remains. Retention and auditability are usually at war; here they’re both honored.
The cost of all this: ~13% write overhead at 100k rows. That’s the number we were willing to pay, measured and published, not assumed.
Two lessons from this release: trust is a prerequisite for features that promise trust — fix the journal before you build on it. And when you sell “verifiable history,” somebody will try to break the chain. Ship verify_chain in the same release.
Full detail: changelog · API reference — and the 0.7.0 follow-up that made everything around it faster: filters before the scan.