Skip to content
FDL
Esc
↑↓navigate↵open⌘Jpreview
On this page

Getting started

Make a repository, write a first version with DuckDB, and read it back.

This page makes a repository in a folder, writes a table into it and reads the result as a reader would. The same commands work on S3 and R2: pass s3://bucket/prefix/ instead of a folder.

Install

pip install 'fdl-cli[s3]'

The package installs the fdl command and the fdl Python package. Python 3.10 or later is needed. Leave out [s3] if you only use folders.

Make a repository

fdl init ./towns/

init writes two files and nothing else:

towns/
  fdl.json      {"fdl": "0.1", "id": "5f0c…", "data_dir": "data/"}
  HEAD          ref: refs/heads/main

HEAD names the default branch, main. The branch itself does not exist yet: there is no version for it to point at.

Check out a working catalog

You never write to the repository directly. You write to a working catalog, a DuckLake catalog in a SQLite file on your own disk, and turn it into a version when you are done.

fdl checkout ./towns/ work/lake.sqlite

Before the first version, checkout makes an empty catalog. Later it copies the catalog of the version main holds. It prints the statement to attach the working catalog with:

ATTACH 'ducklake:work/lake.sqlite' AS lake (
  DATA_PATH '/abs/path/towns/data/', OVERRIDE_DATA_PATH true,
  META_TYPE 'sqlite', DATA_INLINING_ROW_LIMIT 0,
  META_JOURNAL_MODE 'WAL', BUSY_TIMEOUT 5000
);

Beside the working catalog, work/lake.sqlite.fdl.json remembers the repository, the branch and the version the checkout was made from.

Write

Use any DuckDB client. The Parquet files go straight into the repository’s data/ folder.

CREATE TABLE lake.main.towns AS
SELECT * FROM 'towns.csv';

DATA_INLINING_ROW_LIMIT 0 makes DuckLake write every row to Parquet. Left at its default, DuckLake keeps small tables inside the catalog, and a version has to carry its rows in data files that a reader can fetch.

Describe

Every version carries a dataset.json that says what it holds. Write one next to where you run fdl commit:

{
  "name": "towns",
  "title": "Towns",
  "tables": [{ "schema": "main", "name": "towns", "title": "Towns and their codes" }]
}

Only name and title are required. fdl commit checks the file against the catalog and fills in each table’s row count and each column’s type.

Commit

fdl commit work/lake.sqlite -m "First towns" --author alice

commit freezes the working catalog into a version, uploads it and moves main to it. It prints refs/heads/main -> 019a3c4e-… (version 1). The repository now holds:

towns/
  fdl.json
  HEAD
  refs/heads/main                       019a3c4e-…
  versions/019a3c4e-…/
    ducklake.duckdb
    dataset.json
    version.json
  data/main/towns/ducklake-….parquet
fdl log ./towns/
   1  019a3c4e-…  2026-11-01T09:00:00Z  alice
      First towns

Read

fdl url ./towns/

prints the catalog of the version main holds. Attach it read-only, and tell DuckDB where the data files are:

ATTACH 'ducklake:towns/versions/019a3c4e-…/ducklake.duckdb' AS towns (
  DATA_PATH 'towns/data/', OVERRIDE_DATA_PATH true, READ_ONLY
);
SELECT count(*) FROM towns.main.towns;

A version’s catalog never changes, so a reader may cache it forever. Only HEAD and the files under refs/ change.

Make a second version

fdl checkout ./towns/ work/lake.sqlite
INSERT INTO lake.main.towns SELECT * FROM 'towns-2026.csv';
fdl commit work/lake.sqlite -m "Add 2026" --author alice

The new version lists the first as its parent. Both catalogs name the first Parquet file; only the second names the new one.

If someone else committed in between, the commit is refused, because the new version would not be built on what main holds now. Check out again and redo the change. Versions and refs explains why.

Was this page helpful?