System ops
System ops start with a double colon :: and must be the entirety of their
statement — alone in a plain script, or as their own braced statement in a
multi-statement script.
They work on the machinery around your data rather than the data itself: the
relation catalog, indexes, triggers, access levels, running queries, cached
graph projections, and the transaction-time history of TxTime relations.
Results come back as rows, exactly like a query; ops that only change state
return a single status column with the value 'OK'.
The examples on this page run against the small agent-memory schema used throughout these docs:
memory { id => kind, text, importance, at, v } # notes, decisions, insights
entity { id => name, kind } # people, tools, projects
mentions { memory, entity } # which memories involve whom
recalls { from, to => strength } # association edgesInspecting the database
::relations
List all stored relations in the database. The output columns are name,
arity, access_level, n_keys, n_non_keys, n_put_triggers,
n_rm_triggers, n_replace_triggers and description:
::relations['entity', 3, 'normal', 1, 2, 0, 0, 0, '']
['memory', 6, 'normal', 1, 5, 0, 0, 0, '']
['mentions', 2, 'normal', 2, 0, 0, 0, 0, '']
['recalls', 3, 'normal', 2, 1, 0, 0, 0, '']Index relations (created by the ops under
Managing indexes) are listed as well, with index in the
access_level column.
::columns <REL_NAME>
List all columns of the stored relation <REL_NAME>, with each column's key
status, position, type, and default:
::columns memory['id', true, 0, 'String', false, null]
['kind', false, 1, 'String', false, null]
['text', false, 2, 'String', false, null]
['importance', false, 3, 'Float', false, null]
['at', false, 4, 'Float', false, null]
['v', false, 5, '<F32;4>', false, null]The name may also be an index, written <REL_NAME>:<INDEX_NAME> — useful for
seeing exactly which columns an index stores.
::fixed_rules
List the names of every registered fixed rule — the built-in utilities and
graph algorithms, plus any custom rules the hosting application has
registered. These are the names usable on the right of <~ in a query:
::fixed_rules['BFS']
['BetweennessCentrality']
['BreadthFirstSearch']
['BudgetedTraversal']
['ClosenessCentrality']
...
['StronglyConnectedComponents']
['TopSort'](31 rows in a default build; the exact list depends on compile-time features and host registrations.)
::explain { <QUERY> }
A single query is enclosed in curly braces. The query is not executed; its
query plan is returned instead. There is no formal specification for the
format, but the columns — stratum, rule_idx, rule, atom_idx, op,
ref, joins_on, filters/expr, out_relation — read naturally after
Query execution:
::explain { ?[text, importance] := *memory{text, importance}, importance > 0.8 }[0, 0, '?', 1, 'load_stored', ':memory', null, ['gt(importance, 0.8)'], ['~1', '~2', 'text', 'importance', '~3', '~4']]
[0, 0, '?', 0, 'out', null, null, null, ['text', 'importance']]Query options are accepted. Most are ignored, since nothing runs — but options
that shape compilation change the plan you see: :reorder written, for
example, shows the plan without the greedy join reorder.
mnestic
Since mnestic 0.10.5, the plan ::explain shows reflects the deterministic
greedy join reorder (a residual Cartesian product
is annotated (cartesian)), and an eligible count()-over-join carries a
factorization advisory.
Managing stored relations
::describe <REL_NAME> <DESCRIPTION>?
Attach a human-readable description to the stored relation <REL_NAME> and
store it in the metadata. If <DESCRIPTION> is given, it is stored as the
description, otherwise the existing description is removed. It serves as
documentation and signpost for humans and AI agents working out an unfamiliar
schema:
::describe memory 'Episodic memories of a coding agent, with toy 4-dim embeddings'::relations now reports it in the description column:
...
['memory', 6, 'normal', 1, 5, 0, 0, 0, 'Episodic memories of a coding agent, with toy 4-dim embeddings']
...mnestic
Upstream CozoDB defines ::describe in its grammar and runtime but never
wires the rule into the script entry point, so the op always failed to
parse. It works in mnestic since 0.8.5, and — since
it writes relation metadata — is correctly rejected in read-only mode.
::remove <REL_NAME> (, <REL_NAME>)*
Remove stored relations. Several can be specified, joined by commas:
:create scratch { k: String }::remove scratch::rename <OLD_NAME> -> <NEW_NAME> (, <OLD_NAME> -> <NEW_NAME>)*
Rename the stored relation <OLD_NAME> into <NEW_NAME>. Several may be
specified, joined by commas:
::rename mentions -> memory_mentions::access_level <ACCESS_LEVEL> <REL_NAME> (, <REL_NAME>)*
Set the access level of one or more stored relations. The levels are:
normalallows everything,protecteddisallows::removeand:replace,read_onlyadditionally disallows any mutations and setting triggers,hiddenadditionally disallows any data access (metadata access via::relations, etc., is still allowed).
The access level functionality is there to protect data from mistakes of the programmer, not from attacks by malicious parties. Protecting a relation makes a destructive op a loud error instead of a quiet disaster:
::access_level protected entity::remove entityInsufficient access level protected for relation removal on stored relation 'entity'::access_level normal entityTriggers
::set_triggers <REL_NAME> ...
Set triggers for the stored relation <REL_NAME>: any number of on put,
on rm and on replace clauses, each containing a full query that runs in
the same transaction as the triggering write. Inside a clause the affected
rows are available as _new[...] (and _old[...], depending on the clause
kind); the full contract is explained in
Stored relations & transactions. Here every write to
entity is mirrored into a log relation:
:create entity_log { id: String => name: String }::set_triggers entity
on put { ?[id, name] := _new[id, name, kind] :put entity_log { id => name } }::show_triggers <REL_NAME>
Display the triggers associated with the stored relation <REL_NAME> — one
row per trigger, with its kind, position, and stored script:
::show_triggers entity['put', 0, '?[id, name] := _new[id, name, kind] :put entity_log { id => name }']All of a relation's triggers are set together: running ::set_triggers again
replaces the whole set, and calling it with no clauses removes all triggers
from the relation:
::set_triggers entityManaging indexes
All four index kinds share the same op shape: ::<kind> create <REL_NAME>:<INDEX_NAME> { ... } to create, and ::<kind> drop <REL_NAME>:<INDEX_NAME> to drop. Index relations appear in ::relations
under the name <REL_NAME>:<INDEX_NAME>, with index in the access-level
column.
::index create / ::index drop
Create or drop a plain covering index: a reordering of the base relation's columns, kept in sync automatically, that you can also query directly like a read-only stored relation. The engine only chooses an index on its own when a prefix of the index's key is bound; selection rules and key completion are explained in Stored relations & transactions:
::index create memory:by_kind { kind, id }::columns on the index name shows what it stores:
::columns memory:by_kind['kind', true, 0, 'String', false, null]
['id', true, 1, 'String', false, null]mnestic
Since mnestic 0.9.0, ::index create no longer panics when the base
relation contains a corrupt (truncated) tuple. The bad tuple is skipped with
a loud logged error naming the relation, the index, and the arity mismatch,
and the build completes — pair with
::repair_corrupt to delete the damaged rows themselves.
::hnsw create / ::hnsw drop
Manage HNSW (vector proximity) indices. All options — dimensions, distance functions, index-time filters — are documented in Proximity search:
::hnsw create memory:semantic {
dim: 4,
dtype: F32,
fields: [v],
distance: Cosine,
m: 16,
ef_construction: 50
}mnestic
On the RocksDB backend, ::hnsw create builds the index without holding the
base relation's write lock (mnestic 0.8.2), so concurrent reads do not stall
during a long build — see
Non-blocking HNSW builds.
::fts create / ::fts drop
Manage full-text search indices. A full-text index lets a query match stored text by tokens rather than by exact value:
::fts create memory:by_text {
extractor: text,
tokenizer: Simple,
filters: [Lowercase]
}Extractors, tokenizers, filters and the scoring options (BM25 by default since mnestic 0.8.3) are documented in Proximity search.
::lsh create / ::lsh drop
Manage MinHash LSH (locality-sensitive hashing) indices. An LSH index supports near-duplicate detection over text by Jaccard similarity of shingles; options are documented in Proximity search:
::lsh create memory:dedup {
extractor: text,
tokenizer: Simple,
filters: [Lowercase],
n_perm: 200,
target_threshold: 0.7
}::indices <REL_NAME>
List all indices on the stored relation <REL_NAME>, of all four kinds, with
each one's full configuration. With the four indexes above in place:
::indices memory['by_kind', 'normal', ['memory:by_kind'], {'indices': [1, 0]}]
['semantic', 'hnsw', ['memory:semantic'], {'distance': 'Cosine', 'dtype': 'F32', 'ef_construction': 50, 'extend_candidates': false, 'keep_pruned_connections': false, 'level_multiplier': 0.36067376022224085, 'm_max': 16, 'm_max0': 32, 'm_neighbours': 16, 'vec_dim': 4, 'vec_fields': [5]}]
['by_text', 'fts', ['memory:by_text'], {'extractor': 'text', 'tokenizer': {'args': [], 'name': 'Simple'}, 'tokenizer_filters': [{'args': [], 'name': 'Lowercase'}]}]
['dedup', 'lsh', ['memory:dedup', 'memory:dedup:inv'], {'extractor': 'text', 'n_bands': 20, 'n_gram': 1, 'n_rows_in_band': 10, 'num_perm': 200, 'threshold': 0.7, 'tokenizer': {'args': [], 'name': 'Simple'}, 'tokenizer_filters': [{'args': [], 'name': 'Lowercase'}]}]::reindex <REL_NAME>
mnestic
Specific to mnestic 0.12.1 — not present in upstream CozoDB.
Rebuild every HNSW, FTS and LSH index on a stored relation in place, from the index configuration the database already stores. This is the repair path when an index's contents have drifted from its base relation:
::reindex memory['semantic', 'hnsw', 3]
['by_text', 'fts', 3]
['dedup', 'lsh', 3]One row per rebuilt index — its name, its kind, and the number of base rows it was rebuilt from. Reach for it in three situations:
- After the 0.12.1 full-text fix. Full-text postings leaked on every in-place update of a relation carrying only an FTS index (see Release history). The fix stops new leakage but cannot evict postings already written, so an affected relation must be rebuilt once.
- After a bulk load.
import_relationsandimport_from_backupdo not maintain these indexes — that is what makes them fast — so rows they load stay invisible to vector, text and similarity search until a rebuild. - After any drift, whatever the cause.
Each index is rebuilt against its own stored manifest, not a config
reconstructed from ::indices output. This is why ::reindex exists rather than
a drop-and-recreate: an LSH manifest keeps the derived band geometry
(n_bands, n_rows_in_band, perms) but not the weights that produced it, so
recreating it by hand silently recomputes that geometry from defaults and hands
back an index with a different recall profile than the one you asked for. Rows
are re-derived through the same per-row maintenance path :put uses, so a
rebuilt index is identical to an incrementally-maintained one.
Caution
::reindex is a maintenance operation, not an online one. It runs in one
write transaction and holds the relation's write lock for the whole rebuild —
on a large relation, that is minutes during which the relation is blocked. A
crash mid-rebuild rolls back to the intact old index; re-running is always the
cure. Nothing auto-invokes it: the import paths point at it, they do not run it.
A relation with no HNSW/FTS/LSH index is a loud no-op rather than an error, which
keeps ::reindex scriptable across a set of relations without the caller having
to know which ones carry search indexes:
['no HNSW/FTS/LSH index on this relation — nothing to rebuild']Monitor and kill
::running
Display currently running queries: one row per query, with its numeric id
and started_at timestamp.
::kill <ID>
Kill a running query by the ID obtained from ::running. Returns 'KILLING'
when the query was found and poisoned, or 'NOT_FOUND' when no query with
that ID is running (for instance because it already finished):
::kill 42['NOT_FOUND']mnestic
Since mnestic 0.10.5, ::kill genuinely interrupts a running query.
::running and ::kill dispatch before opening a storage transaction (so a
kill no longer queues behind the very query it is trying to stop), and the
per-query poison flag is checked throughout evaluation, so even a long join
that emits no rows aborts promptly with an eval::killed error. The same
release added the :timeout wall-clock budget. See
Interruptibility & query budgets.
Graph projections
A projection is a named, in-memory adjacency structure over stored relations
that the graph
algorithms
reuse across queries via their graph: option, instead of rebuilding it on
every call.
A projection is always fresh: it never serves a transaction data that differs from what that transaction's own scan of the source relations would return, and writing to a source frees what was built from it. Projections are kept in memory only and are not persisted — re-create them after a restart.
mnestic
The ::graph ops are specific to mnestic 0.11.0+. Design detail, measured
speed-ups, and the list of algorithms that accept graph: live on
Cached graph projections.
::graph create <NAME> { edges: <REL>, nodes: <REL> }
Registers a projection. nodes is optional; both source names may be written
bare or quoted. The relations must exist and edges must have arity of at
least two. Index relations, temporary relations, and relations with a TxTime
column cannot be sources.
Nothing is built at create time. Adjacencies materialize on first use, one per
(direction, weighted) combination actually requested.
Naming a nodes relation makes isolated vertices real: a vertex that appears
there but in no edge becomes a genuine degree-0 vertex, ranked by PageRank
and emitted as its own component by ConnectedComponents.
::graph create g { edges: recalls }::graph list
Returns one row per built adjacency — name, edges, nodes, variant,
est_bytes, built_at, last_used — and a single row with null variant
columns for a projection that has not been used yet. The variant label is
one of directed, directed+weighted, undirected, undirected+weighted.
Right after the create above, nothing is built:
::graph list['g', 'recalls', null, null, null, null, null]The first algorithm call over the projection builds the variant it needs —
here ConnectedComponents requests an undirected, unweighted adjacency:
?[id, component] <~ ConnectedComponents(graph: 'g')
:order id::graph list['g', 'recalls', null, 'undirected', 1512, 1783806736.976928, 1783806736.976928]Total resident size is capped at 512 MiB by default, evicting
least-recently-used adjacencies first. Hosting applications can change the cap
(Db::set_graph_projection_capacity in Rust,
set_graph_projection_capacity on the Python CozoDbPy); 0 disables
caching while leaving ::graph create, ::graph list and ::graph drop
working.
::graph drop <NAME>
Forgets the projection and frees every adjacency built from it:
::graph drop gMaintenance
::compact
Instructs the engine to run a compaction job. Compaction makes the database smaller on disk and faster for read queries.
::repair_corrupt <REL_NAME>
Surgically deletes tuples in <REL_NAME> whose stored value is shorter than
the schema demands — the residue of an interrupted write. Where a database
that fails an integrity check would otherwise have to be dropped wholesale,
this repairs the one damaged relation in place. It requires write access and
cannot run in read-only mode. The single output column reports how many tuples
were removed — on a healthy relation, none:
::repair_corrupt memory[0]mnestic
::repair_corrupt is specific to mnestic (0.9.0), added alongside the
non-panicking ::index create above — before it, a single truncated tuple
could make a database that fails integrity checks effectively unusable.
Time travel
A relation whose schema declares a TxTime column (a mnestic 0.10.0 type —
see Types) keeps the full history of its rows: every write
appends a new record stamped by the engine's transaction-time commit clock,
rather than overwriting. Reads default to the current belief; the @ selector
and the :as_of query option time-travel (see Queries,
Bitemporality for the two-axis model, and
Time travel for the older valid-time axis). The ops below
inspect and prune that history.
To have something to inspect, record a belief, revise it, then retract it — three separate transactions, each stamped at commit:
:create belief { claim: String, tt: TxTime => confidence: Float }?[claim, confidence] <- [['pg-timeout-is-30s', 0.6]]
:put belief { claim => confidence }?[claim, confidence] <- [['pg-timeout-is-30s', 0.9]]
:put belief { claim => confidence }?[claim] <- [['pg-timeout-is-30s']]
:rm belief { claim }::history <REL_NAME> <KEYS> <LIMIT>? <OFFSET>?
Returns every record of the given keys of a TxTime relation — the whole
belief timeline, not just current belief. <KEYS> is a list of key-lists,
e.g. [['a'], ['b']]; <LIMIT> and <OFFSET> are optional bare integers for
paging. Read-only; requires at least read access.
The output columns are the plain key columns, vt_ts (on bitemporal relations
that also declare a Validity column; absent here), op (assert or
retract), tt (transaction time, integer microseconds), then the value
columns. Rows are ordered key-ascending, then newest belief first:
::history belief [['pg-timeout-is-30s']]['pg-timeout-is-30s', 'retract', 1783806736982465, 0.9]
['pg-timeout-is-30s', 'assert', 1783806736981242, 0.9]
['pg-timeout-is-30s', 'assert', 1783806736980187, 0.6]The :rm did not delete anything: it appended a retract record, carrying the
values the key held at that moment. Keys coerce through the relation's column
types, so a mistyped key is an error rather than an empty match.
::history_gc <REL_NAME> <CUTOFF>
Drops superseded historical records below the transaction-time <CUTOFF> (in
microseconds) and persists a garbage-collection floor, reclaiming space while
leaving current belief and any history at or above the cutoff intact. Per key
(and per valid-time group on bitemporal relations) it keeps exactly the record
an as-of read at tt = cutoff would resolve to, so reads at or above the
cutoff are unchanged; an as-of read below the persisted floor errors instead
of silently returning a reconstruction as if it were the historical belief.
Requires write access.
The output reports how many records were dropped and the effective floor. A cutoff below every superseded record deletes nothing — and a no-op run does not raise the floor, since the floor is irreversible:
::history_gc belief 1751328000000000[0, null]A cutoff in the future is rejected, and the op refuses to run in a transaction that holds pending writes to the same relation.
::evict <REL_NAME> <KEYS> unredacted?
Hard-deletes every record — all history — of the given keys, for data-erasure
obligations such as GDPR. This is the one deliberate break of append-only. The
op returns one row per distinct key — relation, key, rows_deleted, tt
— and writes an audit row recording the same facts into the reserved companion
relation mnestic_evict_audit in the same transaction. The key marker is a
salted hash by default, since storing the key itself would re-enshrine the
data the eviction removes; the optional unredacted keyword records the
actual keys instead. Requires write access.
::evict belief [['pg-timeout-is-30s']] unredacted['belief', '["pg-timeout-is-30s"]', 3, 1783806736984312]A ::history on the evicted key now returns no rows. Duplicate keys in one
call are deduplicated, and like ::history_gc, the op refuses to run in a
transaction with pending writes to the same relation.
mnestic
TxTime, the commit clock, and the ::history / ::history_gc / ::evict
ops landed in mnestic 0.10.0 as part of bitemporality. A relation declaring
TxTime cannot be opened by upstream CozoDB builds.
Adapted from the CozoDB documentation by Ziyang Hu and the Cozo Project Authors, used under CC‑BY‑SA‑4.0. Adaptations for mnestic are released under the same license. mnestic is an independent fork and is not affiliated with or endorsed by the original authors.