---
title: 書いてから読まれるまで
description: 1 つの変更を作業カタログから読み手まで追い、各段階のリクエストと、FDL の各部分が Git の何に当たるかを示します。
---

このページでは、1 つの変更を書き手のマシンから読み手まで追い、各段階で送るリクエストを示します。各段階でリポジトリに残るファイルは[リポジトリの中身](/ja/internals/inside-a-repository)に、決まりは[仕様](/ja/spec)にあります。

## 各部分と 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 は、読み手を守る順にファイルを書きます。あるファイルが指す先は、そのファイルより先に書きます。

1. リポジトリに無い新しいデータファイル
2. `versions/<ID>/ducklake.duckdb`
3. `versions/<ID>/dataset.json` と `files.json`
4. `versions/<ID>/version.json`
5. ref。`If-Match: <1 の前に読んだ ETag>` で書きます。最初のバージョンなら `If-None-Match: *` です
6. `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](/ja/spec/http)を参照）。順番は上と同じで、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` だけです。誰がリポジトリを書いたかも、前にホストがいるかどうかも知らなくて構いません。

1. `HEAD` か、求められた ref を読みます。`HEAD` は `ref: refs/heads/main` を持つので、次に `refs/heads/main` を読みます。ref はバージョンの ID を持ちます
2. `versions/<ID>/version.json` を読みます。`id` が求めた ID と同じで、`fdl` のメジャーバージョンを読み手が知っていることを確かめます
3. `versions/<ID>/expired.json` があるか確かめます。あれば止まります。そのバージョンは保持期間を過ぎたか取り下げられていて、カタログはもうありません
4. リポジトリのデータフォルダをデータパスにして、カタログを読み取り専用で ATTACH します

```sql
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` を置いてカタログを消します。手順は[データファイル](/ja/spec/data-files#取り下げ)にあります。

保持期間を過ぎたか取り下げられたバージョンにたどり着いた読み手は、読む手順の 3 で止まり、理由を伝えます。
