mnestic
GitHub

Migrating from CozoDB

Moving from CozoDB to mnestic is a dependency change. The query language (CozoScript) and the engine semantics are unchanged, and the importable library name stays cozo, so your application code does not change.

mnestic

mnestic forks CozoDB at upstream 481af05 (2024-12-04). Your data files, schemas, and queries are compatible.

Swap the dependency

The published crate is mnestic, but its [lib] name is cozo. Use Cargo's rename idiom so the dependency key stays cozo — that keeps both cozo/feature references and use cozo::… working unchanged:

# Cargo.toml
[dependencies]
cozo = { package = "mnestic", version = "0.13.1" }
 
# with the RocksDB backend:
# cozo = { package = "mnestic", version = "0.13.1", features = ["storage-rocksdb"] }

If you depend on a local checkout instead of crates.io:

cozo = { package = "mnestic", path = "../mnestic/cozo-core" }

No source changes are required — use cozo::DbInstance; and friends resolve to mnestic.

What you get immediately

  • Unreleased upstream fixes for free. The fork point is 30 commits ahead of the published 0.7.6 crate, including the stored_prefix_join correctness fix.
  • Faster lookups. Equality post-filters on stored relations now compile to keyed prefix lookups — see Equality pushdown.
  • Faster, non-blocking HNSW builds on RocksDB — see Non-blocking HNSW builds.
  • A keyword-prefixed identifier parser fix (nullable_column, trueValue, falsey now parse in value positions).
  • A float in a validity stops corrupting the valid-time axis. Upstream coerced the float seconds returned by now() / parse_timestamp() into a validity's integer microseconds, storing the fact at 1970; mnestic 0.12.2 rejects it instead. This is the one migration step that can surface as a new error: a schema carrying Validity default [floor(now()), true] still compiles, but its next write raises eval::float_validity. Write default [to_int(now() * 1000000), true] (or default 'ASSERT'), and see If you used a Validity column below plus the 0.12.2 notes — rows already stamped at 1970 cannot be repaired by the upgrade.

If you used a Validity column

Skip this section if none of your relations declare a Validity column. If any of them do, read it before you write to them: the upgrade will surface a new error, and — the part that is easy to miss — some of the rows already on your disk are probably wrong.

The bug is not one the fork introduced. Validity columns and @ time travel are a CozoDB feature that predates the fork point, and the faulty coercion sits in upstream's own code: a validity stores an integer count of microseconds, while now() and parse_timestamp() return float seconds, and upstream's Num::get_int quietly accepted any whole-numbered float as an integer. So the two units were interchangeable and nothing said a word. Every CozoDB database with a Validity column has this. Upstream's own HNSW test wrote Validity default [floor(now()), true] and had been stamping every row it wrote at 1970 for as long as the test existed; it never asserted on the value, so it never noticed. mnestic 0.12.2 rejects the float.

Where it bites: the schema compiles, the next write fails

This catches people out because the :create is not what breaks. A schema carrying the broken idiom still compiles exactly as before:

:create emp {name: String, at: Validity default [floor(now()), true] => title: String}

It is the next write that now raises an error:

?[name, title] <- [['ada', 'CTO']] :put emp {name => title}
eval::float_validity
 
  × when executing against relation 'emp'
  ├─▶ when processing tuple ["ada", "CTO"]
  ╰─▶ a Validity column stores an integer timestamp, got the float 1783955642
  help: Validity timestamps are integer MICROSECONDS since the Unix epoch. …

Any spelling that produces a whole-numbered float does this — floor(now()), round(now()), or parse_timestamp() on a whole second. (A bare [now(), true] already failed before 0.12.2, but only by luck: now() returns a fractional float, and the old coercion accepted whole-numbered ones only.) The fix is to give the column integer microseconds:

:create emp {name: String, at: Validity default [to_int(now() * 1000000), true] => title: String}

default 'ASSERT' works too, and the string forms ('2024-06-01T12:00:00Z', 'ASSERT', 'RETRACT') remain the safe way to write a validity by hand.

The quiet half: your existing rows may already be wrong

The error is the loud half, and it is the half that fixes itself the moment you change the schema. The quiet half is that every row your Cozo build already wrote through that idiom is on disk at a valid time a million times too small, and upgrading does not touch it. Those rows are plain integers; 0.12.2's float rejection guards the door, not the room. It cannot detect them and will not repair them.

A row you meant to stamp 2024-06-01 was stored as 1717200000 — the count of seconds — and a validity reads that as microseconds, i.e. 28 minutes past the Unix epoch, 1970-01-01T00:28:37Z. The consequence is that the fact leaks backwards in time. Take a relation where bob was CTO from 2020, ada took over on 2024-06-01, and ada stepped down in 2024-10 — with ada's assertion and bob's retraction both written through the broken idiom. Asking who was CTO in 2020:

?[name, title] := *emp{name, title @ '2020-01-01'}

returns ada — whose appointment did not begin for another four years — alongside bob, who really did hold the job then. The same corruption breaks retractions in the mirror image: bob's retraction also landed in 1970, which is before the 2020 assertion it was meant to cancel, so the later assertion masks it and the retraction never takes effect at all. bob is CTO forever.

as ofbefore repairafter repair
2019-01-01ada
2021-01-01ada, bobbob
2024-07-01ada, bobada
2025-01-01bob

Ordinary queries look fine throughout — the damage is visible only under time travel, which is exactly where a temporal database is supposed to be trustworthy.

Before you repair anything

Caution

The threshold in the repair below is a heuristic, and it is yours to authorize — one relation at a time. Run it on the wrong relation and it will destroy your data.

The repair assumes the relation's valid time is wall-clock microseconds, and treats every stamp below 1000000000000 as corrupt. That assumption is not always true, and mnestic cannot make it for you.

Valid time in CozoDB is a deliberately abstract, user-set logical clock. Cozo's own tutorial queries @ 2019; the engine's own tests use @ 250. On a relation whose validity is an abstract clock rather than wall-clock microseconds, every stamp is legitimately below the threshold — and this recipe would dutifully "repair" 250 into 250000000 and destroy your data.

So: take a backup (::backup), run the detection query on its own, read the rows it returns, and only then repair — one relation at a time, each one a decision you have actually made.

The threshold works for wall-clock relations because the two units do not overlap: 1000000000000 microseconds is 1970-01-12, a genuine microsecond stamp from this century is above 1000000000000000 (2001-09-09), and a stamp mistakenly written in seconds is down around 1700000000. Pick a different threshold if your data warrants it; the number is a judgement, not a constant.

Detection

Find the candidate rows and look at them:

?[name, ts] := *emp{name, at}, ts = to_int(at), ts < 1000000000000

A bare binding like *emp{name, at} sees every validity version of every key, not just the current one, so this scan is complete.

Repair

Two scripts, in this order. The corrected rows go in first, so the fact is never absent from the relation:

bad[name, ts, ok, title] := *emp{name, at, title},
                            ts = to_int(at), ok = to_bool(at), ts < 1000000000000
?[name, at, title] := bad[name, ts, ok, title], at = [ts * 1000000, ok]
:put emp {name, at => title}
?[name, at] := *emp{name, at}, to_int(at) < 1000000000000
:rm emp {name, at}

Two things this gets right that a hastier version does not, both tested:

  • It preserves is_assert. to_bool(at) carries the assert/retract flag across, so a corrupt retraction is moved to its correct instant and stays a retraction. Rewriting everything as an assertion would silently resurrect deleted facts.
  • It handles a key with both a corrupt and a correct version. Repairing ada (corrupt assertion, correct retraction) does not disturb the good row, and the two end up correctly ordered.

Re-run the detection query afterwards; it should return nothing, and the time-travel answers should agree with the "after repair" column above.

Why mnestic cannot just ship a ::repair for this

Because a stored 1717200000 is genuinely ambiguous. On a wall-clock relation it is a broken 2024 timestamp; on an abstract-clock relation it is a perfectly legitimate value — and nothing in the schema distinguishes the two, because CozoDB deliberately lets valid time mean whatever you need it to mean. Only you know which clock a given relation keeps.

This is the same reason 0.12.2 rejects a float but still accepts an integer in seconds: @ 1704067200 remains valid CozoScript and still quietly returns nothing, because no magnitude check inside the engine could tell a wrong-unit timestamp from a small legitimate one without being wrong somewhere else. A guess in a database engine is worse than a refusal. The real answer is a typed path — dt_to_validity plus @ <Validity>, so the unit is carried by the type and never inferred — and that landed with the datetime library in 0.13.0. Integer microseconds remain the low-level form and the string forms the safe ones.

What is new (opt-in)

These are additive — existing code is unaffected until you call them:

What stays the same

  • CozoScript syntax and semantics.
  • The storage backends (in-memory, SQLite, RocksDB) and their on-disk formats.
  • The DbInstance / Db API surface you already use.

Note

mnestic is an independent fork and is not affiliated with or endorsed by the original CozoDB authors. See License & attribution.