書いてから読まれるまで
1 つの変更を作業カタログから読み手まで追い、各段階のリクエストと、FDL の各部分が Git の何に当たるかを示します。
このページでは、1 つの変更を書き手のマシンから読み手まで追い、各段階で送るリクエストを示します。各段階でリポジトリに残るファイルはリポジトリの中身に、決まりは仕様にあります。
各部分と Git での呼び名
FDL は Git の役割と名前をそのまま使い、保存の仕方は独自に決めています。Git がコミットをどう持つかを知っていれば、この表でリポジトリを読めます。
| FDL | Git | 補足 |
|---|---|---|
| リポジトリ(ストアの 1 つのプレフィックス) | サーバー上のベアリポジトリ | サーバーのプロセスは要りません |
fdl.json |
config |
形式、リポジトリの id、data_dir を持ちます |
versions/<ID>/version.json |
コミット | 親、作者、メッセージ、カタログの sha256 を持ちます |
versions/<ID>/ducklake.duckdb |
ツリー | どのテーブルがあり、それぞれがどのデータファイルでできているかを持ちます |
| データファイル(Parquet) | ブロブ | 一度書いたら変えません。1 つのファイルを多くのバージョンが指せます |
refs/heads/<name> |
ブランチ | 条件付き書き込みで、前にだけ進みます |
refs/tags/<name> |
タグ | 一度作ったら動かしません |
HEAD |
HEAD |
既定のブランチの名前を持ちます |
refs.json |
info/refs |
素の HTTP の読み手が ref を名前で探せるようにします |
versions/<ID>/files.json |
到達可能性ビットマップ | カタログから作る索引です。読み手はこれが無くても読めます |
version.json.sig |
署名付きコミット | ssh-keygen -Y sign で作る SSH 署名です |
| 作業カタログ(SQLite) | 作業ツリーとインデックス | バージョンから作り、次のバージョンにします |
POST <root>/?batch |
Git LFS の batch API | ストレージへ直接アップロードする URL を渡します |
expired.json |
なし | バージョンにカタログが無くなった理由を書きます |
次の 3 つは、意図して Git と変えています。
- バージョンの ID は UUIDv7 で、中身のハッシュではありません。DuckDB のリリースが違うと、同じカタログでも違うバイト列で書かれることがあり、中身のハッシュは道具によって変わってしまうためです。中身の確認には
version.jsonの sha256 を使います - packfile はありません。ファイルはそれぞれストアの 1 つのオブジェクトなので、読み手は Parquet ファイルのうち要る部分だけを取りに行けます
- マージはありません。バージョンの親は 1 つです。push を断られた書き手は、新しいバージョンの上で作り直します
バージョンを書く
checkout
fdl checkout は HEAD を読み、それが指す ref を読み、そのバージョンの version.json を読んでから、カタログを書き手のマシンの SQLite ファイルに写します。これが作業カタログです。バージョンに expired.json があれば、checkout は止まります。
作業カタログのデータパスは、ストアによって変わります。
- フォルダか S3: リポジトリのデータフォルダです。DuckDB は新しい Parquet ファイルをリポジトリへ直接書きます
- HTTP: 書き手のマシンのフォルダです。リポジトリに既にあるファイルは
data_dirの下の URL で指し、HTTP で読みます
書く
書き手は作業カタログに対して、DuckDB で直接、または dbt などの道具を通して SQL を流します。文ごとに作業カタログに DuckLake のスナップショットができます。読み手から見えるものはまだ何も変わりません。新しい Parquet ファイルがリポジトリに書かれていても、どのバージョンもそれを指していないためです。
commit
fdl commit は作業カタログを DuckDB ファイルに写し、その data_path をリポジトリのデータフォルダにし、そこから dataset.json を作り、新しい ID と、親として前のバージョンを持つ version.json を書きます。テーブルの行が親と同じなら、コミットしません。新しいバージョンは、push するまで書き手のマシンにしかありません。
push
push は、読み手を守る順にファイルを書きます。あるファイルが指す先は、そのファイルより先に書きます。
- リポジトリに無い新しいデータファイル
versions/<ID>/ducklake.duckdbversions/<ID>/dataset.jsonとfiles.jsonversions/<ID>/version.json- ref。
If-Match: <1 の前に読んだ ETag>で書きます。最初のバージョンならIf-None-Match: *です refs.json。HEADとすべての ref を読み直して書き直します
読み手は ref から読み始め、ref は 5 で書きます。5 より前で止まった push は、どの ref も指さないファイルを残すだけで、読み手からは見えません。1 のあとに別の書き手が ref を動かしていれば、ETag が合わず、ストアは 5 を断ります。断られた書き手は checkout し直し、新しいバージョンの上で作り直します。
refs.json は ref のあとに書くので、少しの間は ref より古いことがあります。そのため、今のバージョンが要る読み手は ref 自体を読みます。
HTTP での push
HTTP では、すべてのキーがリポジトリの URL の下のパスになり、ホストがそこへの GET や PUT などに答えます(HTTP での pushを参照)。順番は上と同じで、2 つの段階が変わります。
- 1 の前に、書き手はこれから上げるファイルのキーと大きさを
POST <root>/?batchで送ります。ホストはファイルごとに、アップロード用の URL を返すか、既にあると答えるか、ホスト経由で送らせるために 413 を返します。URL をもらったファイルはPUTでストレージへ直接送られ、データはホストを通りません。batch の無いホストは 404・405・501 のどれかを返し、すべてのファイルがホストを通ります - 5 で、ホストは ref を動かす前に push を確かめます。バージョンがそろっていること、その親が今 ref の指すバージョンであること、カタログが
version.jsonと合うこと、カタログが指すデータファイルがすべてリポジトリにあることです。確認に通らなければ、理由を付けて 409 を返します。ref を自分で動かすホストはrefs.jsonも自分で書き、書き手が書こうとすると 403 を返します
ホストは、書き手に ref を一切動かさせないこともできます。その場合、書き手はバージョンを上げて 4 で止まり、ホストがあとで、たとえば誰かが変更を承認したときに ref を動かします。
バージョンを読む
読み手に要るのは GET だけです。誰がリポジトリを書いたかも、前にホストがいるかどうかも知らなくて構いません。
HEADか、求められた ref を読みます。HEADはref: refs/heads/mainを持つので、次にrefs/heads/mainを読みます。ref はバージョンの ID を持ちますversions/<ID>/version.jsonを読みます。idが求めた ID と同じで、fdlのメジャーバージョンを読み手が知っていることを確かめますversions/<ID>/expired.jsonがあるか確かめます。あれば止まります。そのバージョンは保持期間を過ぎたか取り下げられていて、カタログはもうありません- リポジトリのデータフォルダをデータパスにして、カタログを読み取り専用で ATTACH します
ATTACH 'ducklake:https://example.org/lake/versions/019a3c4e-…/ducklake.duckdb' AS lake (
DATA_PATH 'https://example.org/lake/data/', OVERRIDE_DATA_PATH true, READ_ONLY
);
ここから先は DuckDB がカタログを読み、クエリに要る Parquet ファイルの部分を取りに行きます。fdl url は 1 から 3 までを行い、4 で使うカタログの URL を出します。
読み手はカタログに書かれたデータパスを使わず、自分で渡します。カタログが持つのは書き手が公開した場所の URL で、ミラーは同じファイルを別の場所から配るためです。読み手がデータパスを渡せば、同じ文が元のホストでもどのミラーでも動きます。
バージョンのカタログは変わらないので、ずっとキャッシュできます。HEAD・refs/・refs.json は Cache-Control: no-store で配るので、読み手は ref が今どこを指しているかを必ず知ることができます。
FDL を知らない読み手
ホストは、HEAD が指すバージョンのカタログを <root>/ducklake.duckdb で配ってもかまいません。DuckLake しか知らない読み手は、その 1 つの URL を ATTACH できます。ホストは FDL-Version ヘッダーでバージョンの ID を送るので、読み手はどのバージョンを受け取ったかを知ることができます。ストアのそのキーにはファイルがありません。ホストが versions/ のカタログを配ります。
バージョンを消す
fdl expire は古いバージョンを保持期間から外します。先に expired.json を置き、次にカタログを消し、version.json は残します。そのため fdl log はそのバージョンと履歴の中の位置を表示し続けます。どの生きているバージョンも指さないデータファイルは、猶予期間のあとで消します。書き手がそのファイルを指すバージョンをこれからコミットするかもしれないためです。
取り下げは、間違って公開した個人データなど、公開してはいけないデータを、それを持つすべてのバージョンから消します。書き手はまず、そのデータを除いた新しいバージョンを作り、影響を受けるブランチをそこへ前に進め、影響を受けるバージョンを指すタグを消します。次に、そのデータを持つデータファイルを猶予期間を置かずにすぐ消し、影響を受けるバージョンに "reason": "withdrawn" の expired.json を置いてカタログを消します。手順はデータファイルにあります。
保持期間を過ぎたか取り下げられたバージョンにたどり着いた読み手は、読む手順の 3 で止まり、理由を伝えます。