# datalog-dafsa $ dl versions

Time Travel

From the CLI

// publish · versions · --version N

Time-travel is a first-class feature, not an add-on: because the engine is built around a publish-then-serve lifecycle and keeps every published snapshot by default, the database is a versioned point-in-time history. Any past state can be queried as if it were current.

That buys real capabilities on a batch-write / read-heavy workload:

  • Audit & as-of analysis — answer “what did this derived relation contain two publishes ago?” without a replay or a full rebuild.
  • Immutable history — every version is a frozen, reproducible view; later writes can never retroactively change it.
  • Diff / evolution — compare the same goal across versions to see exactly how the database evolved between publishes.
  • Zero-cost snapshotting — the snapshot is the normal read path (mmap views); keeping history is just not deleting the old version directories.

The dl CLI exposes the timeline directly — record a point-in-time with publish, list the history with versions, and run queries as-of a past snapshot with --version N:

$ ./dl -d /tmp/db publish
Snapshot published.

$ ./dl -d /tmp/db versions
1
2
3

# Query as-of a past snapshot — full-text and semantic search both support --version.
$ ./dl -d /tmp/db search 'gpu rental' --top 10 --version 2
$ ./dl -d /tmp/db vsearch 'GPU rental' --k 10 --version 2

The version-aware C API is dl_query_version, dl_search_version, and dl_vector_search_version (see the C API).

Versioned snapshots

// dl_snapshot_versions — the two-call idiom

Each successful publish produces a monotonically increasing version number starting at 1. Enumerate the available versions ascending with dl_snapshot_versions, using the two-call idiom:

long total = dl_snapshot_versions(db, NULL, 0);   /* size */
uint32_t *vers = malloc((size_t)total * sizeof(*vers));
dl_snapshot_versions(db, vers, (size_t)total);    /* fill */
free(vers);

It returns the total number of versions even when the output buffer is smaller (filling at most cap entries), returns 0 when no snapshot has been published, and -1 on a NULL db.

As-of queries

// dl_query_version · dl_query_bound_version

Query a relation as of a specific published version with dl_query_version, or bind leading columns with dl_query_bound_version:

long n = dl_query_version(db, version, "edge", cb, user);
long m = dl_query_bound_version(db, version, "edge", leading, k, cb, user);

Semantics and guarantees:

  • As-of reads use an explicit version and read that version’s manifest + mmap view from disk; db->snap_version is never mutated, so live/current routing is untouched.
  • As-of views are immutable: add/delete operations after a publish never change an earlier version’s view. The live dl_query stays on the current version until the next publish.
  • A nonexistent version (including 0), a NULL goal/relation absent from that version, or a NULL callback is a loud -1 — never a silently-empty result.
  • Variadic relations are supported (arity-mixed tuples are returned).

Retention

// dl_set_snapshot_retain — prune-to-N, n==0 keeps all

By default every version is kept forever. To bound disk usage, opt in to prune-to-N with dl_set_snapshot_retain: after each successful publish, the oldest versions beyond the most-recent n are pruned. n == 0 (the default) restores keep-all.

dl_set_snapshot_retain(db, 5);   /* keep the 5 most-recent versions */
dl_set_snapshot_retain(db, 0);   /* back to keep-all */

A pruned version is gone — querying it returns -1 (loud), matching the nonexistent-version contract.

Concurrency model

// single-writer / multiple-reader on mmap

This fits the engine’s single-writer / multiple-reader model. Readers hold mmap views and keep reading valid data even after a retention prune unlinks the underlying snapshot directory. A reader holding an mmap of snapshots/<V>/<rel>.dafsa keeps reading valid data after the pruner removes the directory — the unlink does not disturb the open mapping.