Versions and refs
A version is a frozen catalog with a record; a ref is a name that points at one. How they relate to DuckLake snapshots and to Git commits.
Versions
A version is one frozen DuckLake catalog and a record of how it came to be. It lives in its own folder and is never changed once written.
versions/019a3c4e-7b21-7f3a-9c55-2f0d8e1a4b6c/
version.json the record: parents, author, message, checksums
dataset.json what it holds: tables, columns, license
ducklake.duckdb the frozen catalog
The record says:
id: the version ID, a UUIDv7. IDs sort by creation time, and two writers never make the same oneseq: the number shown to people, the parent’sseqplus oneparents: the version it was built on. Empty for the firstauthor,messageandcreated_atcatalog: the size and sha256 of the catalog file, so a reader can check what it fetchedducklake: the DuckLake format of the catalog and the snapshot it was frozen at
The ID is not a hash of the content, as a Git commit’s is. The same catalog can be written as different bytes by two DuckDB releases, so a content hash would not be stable. The sha256 in catalog checks the content instead.
The full list of fields is in Versions in the specification.
Snapshots and versions
DuckLake makes a snapshot on every write: each INSERT, CREATE TABLE or ALTER in its own transaction. A day of work can make hundreds. A version is made when you decide the work is ready, and it takes the last of those snapshots.
A snapshot is to a version what a commit on a work branch is to the squashed commit that lands on main. The snapshots stay inside the catalog, with their commit_message and commit_extra_info, so the detail is not lost. version.json records the range of snapshots the version adds after its parent, but does not copy them.
Refs
A ref is a file that holds a version ID and a newline. It is how a reader finds a version without knowing its ID.
refs/heads/main 019a3c4e-7b21-7f3a-9c55-2f0d8e1a4b6c
refs/tags/2025-final 019a2f10-0c4d-7e88-a1b2-55c0d9e3f7aa
HEAD ref: refs/heads/main
| Kind | Path | Moves |
|---|---|---|
| branch | refs/heads/<name> |
yes, forward |
| tag | refs/tags/<name> |
never |
| request | refs/requests/<number> |
as the host decides |
| HEAD | HEAD |
rarely |
main is the branch readers get unless they ask for another. HEAD names it, as in Git. A tag pins a version that readers can rely on, such as the final figures of a year. refs/requests/ is for hosts: a version waiting for approval can be held there without being on main.
Refs are the only files in a repository that change. Everything else is written once.
Moving a branch
A branch moves forward only. A new version can become main when its parents include the version main holds now. This is a fast-forward in Git’s terms.
main → A
Alice checks out A, commits B (parent A), pushes: main → B
Bob checked out A too, commits C (parent A), pushes: refused
Bob’s push is refused because C is not built on B. If it were accepted, B would vanish from the history of main with no one deciding that. Bob checks out B and makes his change again.
FDL does not merge rows. Merging two DuckLake catalogs would mean deciding which row wins when both sides changed it, and DuckLake gives tables no identity across catalogs that would make that safe. Redoing the change on the new version is usually a matter of running the same SQL again.
Moving a branch back, to undo a version, is allowed only when asked for explicitly. The version that was undone stays in the repository and in the history.
History
fdl log follows the first parent from the version a ref holds back to the first one:
3 019a4b… 2026-11-01T09:00:00Z alice
Correct the 2026 figures
2 019a3c… 2026-10-15T09:00:00Z bob
Add 2026
1 019a2f… 2026-10-01T09:00:00Z alice
First towns
Versions that were taken out of retention stay in the history. Their record is kept, only their catalog is gone. See Retention.