月次統計のSSOTをGitのTSファイルからCloudflare D1へ移す:Turso推奨を過去の検討資料で改めた日

開発mdx-playground

月次統計のSSOTをGitのTSファイルからCloudflare D1へ移す

前日分の日記を作ってもらっている途中で、報告の一文に引っかかった。 再発防止のガードを足したと書いてあるのに、同じ報告に「データの差分なし」とも書いてある。 直すべきことがあったのに、なぜデータには差分が無いのか。 この疑問を口にしたところから、月次統計の置き場所を動かす一日が始まった。

発端は前日の日記への2つの指摘

1つめは、日記の材料に個人のメールアドレスが実名のまま出ていたことだ。 「実名のままやめといてくださいね」と頼み、2月の記事に残っていたアドレス5か所も伏せてもらった。

2つめが、さっきの疑問である。 「再発防止が意味がわからないんですけど、なんでこれデータの差分がないんですか」と聞いた。

返ってきた答えでは、「データの差分なし」は「何も起きなかった」という意味ではなかった。 報告のほうが、まぎらわしい書き方をしていたのだという。 ガードの中身は、外銷訂単(台湾の輸出受注)の取得元が巻き戻ったときに SSOT を書き換えない、というものだ。 その日の古い CSV で実際のスクリプトを回し、再現も取ってもらった。

そして、説明のついでに出てきた事実のほうが大きかった。 統計データは全部が DB に入っているわけではなく、外銷訂単は Git の TS ファイルにしか無い。

「そうなんだ、Gitの.tsファイルなんだ」と返した。

Turso か D1 か、過去の計画を読むまで

そこで「DBに全部入れませんか?そろそろ」と持ちかけた。 候補は Cloudflare の DB(D1)か Turso で、どちらがいいかを聞いた。

最初の答えは Turso だった。 統計データはサイトの表示時に DB へ問い合わせるわけではなく、手元で TS ファイルを作るときに読むだけなので、D1 の強みが活きない、という理由である。

筋は通っている。 ただ、この話は前にも検討した覚えがあった。 非公開のドキュメントを作った気がするので、それも踏まえてほしいと頼んだ。

非公開記事と memo の両方を探してもらうと、過去の検討が3件見つかった。 本命は9月25日の「make-diary の統計取得を Cloudflare Workers へ移す計画」だった。 状態は進行中で、Phase 0 まで終わっていた。 その計画のために、統計用の D1 データベースもすでに作ってあった(テーブルは0本)。

取得の実行場所を Cloudflare へ寄せる計画が動いていて、器の D1 まで用意済みだった。 これを読んだ結果、Turso という最初の答えは改まり、推奨は D1 に変わった。

計画書は Codex に3回レビューさせ、3回目で「実装に進める」という判定をもらった。 判断事項の3件は、Chrome に開いた計画書のボタンで選んで送った。 3件とも推奨どおり A にした。 DB の置き場所は D1 にし、ビルドが読むのはこれまでどおりコミット済みの TS にする、といった内容である。

引き継ぎを書き出して、続きを始める

ここで一度区切ることにした。 引き継ぎのプロンプトを、ダウンロードディレクトリのテキストファイルに書き出してもらった。 push も許可した。 このセッションの5本に加えて、セッション開始時点で未 push だった2本も一緒に送られ、計7本になった。

そのあと /compact で文脈を畳み、書き出したテキストファイルのパスを渡して続きを始めた。

Phase 0:D1 の器と書き込みルールを作る

最初に、1本にまとまっていた統計のエクスポートファイルを系列ごとのファイルに分けてもらった。 分割の前後で export される値が一致することと、dev で各系列が同じ期間で表示されることを確かめてから、コミットした。

続いて D1 側の器を作った。

  • スキーマは3テーブルと1インデックスで、再適用しても同じ結果になる
  • 書き込みの判定は純粋関数(planUpserts)に閉じ込め、SQL の組み立ても純粋関数にした
  • d1.mjs は wrangler を呼ぶだけの薄いシェルにした

途中で、D1 への読み取りが認証エラーで通らないことがあった。 試し直す前に issue を作ってもらい、そのあと d1.mjs が動くこと、数値のリテラルが崩れずに戻ることを確かめた。

テストで1件だけ落ちたが、原因はテストの側だった。 空の値だけの取得結果は「巻き戻し」として扱うルールがあり、テストがそれに引っかかっていた。 同じ月の金額の行を足し、本来確かめたいルールだけを見るテストに直してもらった。

コミットのところで、もう1つつまずいた。 コミット自体は通ったのに、ゲートの Codex レビューが途中で失敗し、未レビューのまま保存されていたのだ。 原因は Codex 側のモデルの容量不足(Selected model is at capacity)だった。 issue に記録したうえで、同じ差分に手でレビューをかけ直してもらった。

かけ直したレビューから修正点が出てきた。 主なものは次の2つである。

  • 月や系列が消えていたら、生成を止めるチェックを足す
  • 往復のテストを、D1 の行の形を通して確かめる

Phase 0 は、計画書のチェック8項目がすべて埋まり、全テスト(265ファイル、21,133件)が通ったところで終えた。 コミットは4本である。

完了の報告が英語で来たので、「日本語で返して」と頼んだ。 この日はこれを2回言った。

Phase 1:取得スクリプト4本を D1 経由に切り替える

対象は、外銷訂単、シンガポールの NODX、日本の半導体輸出、機械受注の4本だ。 どれも「取得するたびに TS を丸ごと作り直す」型だったので、先に D1 へ書き込み、TS は D1 から生成する流れに変えた。

同期の処理(sync.mjs)と、CLI(seed / generate / set-field / status)を作ってもらった。 Windows ではコマンドライン引数で日本語を渡すと化けることがあるため、set-field は値をファイルからも読めるようにした。

初回の投入では、TS ファイルのハッシュを前後で記録し、ファイルが変わらないことを確かめた。 外銷訂単は768行を入れて、ハッシュは変わらなかった。 残りの3ファイルも同じ手順で入れた。

実データでも、安全装置が効くかを見た。

  • 手で TS を編集した状態だと、手編集の検知が exit 4 で止める(ファイルは戻し、ハッシュが元と一致した)
  • 取得元が巻き戻った系列は D1 に書かれず、TS のハッシュも変わらない(exit 3)
  • NODX は、ヘッダのコメント2行と生成時刻だけが変わり、値の差は無かった

一方で、スクリプトによる文字列の置換は何度も空振りした。 一致しなかったり、エスケープで失敗したり、ファイルが CRLF のせいで複数行の一致が取れなかったりした。 いずれも何も書き込まれていなかったので、Edit で1か所ずつ直してもらった。

数分前の作業で、0 から 0 への無意味な「改訂」記録が4件できていた。 これは消してもらい、再実行しても0件のままであることを確かめた。

機械受注は形式が変わったので、ページも目で見た。 グラフと表は分割前と同じに描画され、最新月の 2026-07 は1兆102億円、+61.1% だった。

コミットゲートでは何度も止まった。 2回目は、最新月をデータから導くことと、set-field のチェックを書き込みの前に移すことを指摘された。 5回目は、手順書が exit 3 を一律に「変更なし」として扱っている点だった。 その合間には、コミットメッセージに入れたバッククォートがテンプレート文字列を壊し、構文エラーになった。 テスト用に書き換えた値(103.95)が外銷訂単の TS に残っていたことも見つかり、TS はバックアップから戻してもらった。

ようやくコミットできたのが 6d613308 で、全テスト(266ファイル、21,138件)も通った。

Codex のレビューは済んでいるのか

push のあと、実装に Codex のレビューがちゃんと通っているかが気になって聞いた。

答えは「ほぼ済んでいる」だった。 今日の実装コミット5本のうち3本はコミットゲートで正規に通っていた。 残りの2本は、Codex 側が混雑していて、ゲートのレビューが取れないままコミットされていた。 抜けていた 09b4bfa0 にも事後レビューをかけてもらい、5本すべてが Codex のレビューを経た状態にした。

残りを片付ける

このセッションの未了事項を聞くと、6件あった。 手順書の push と、走らせたままの dev サーバーの停止を頼み、あわせて公開記事に残っていたものを消すことにした。

残っていたのは、Workers のサブドメインと個人の Gmail である。 伏せ字に置き換えると、コードブロックの外にある裸の URL 3つが、存在しないドメインへのリンクになってしまう。 そこで、この3つはバッククォートで囲んでもらった。 リポジトリ自体は非公開なので、memo や非公開記事は外から見えない。 今回直したのは公開記事だけで、8ファイルになった。

学び

  • 「データの差分なし」という報告は、「何も起きなかった」ことを意味しない。引っかかった一文は聞き返す。今回はそこから、統計の正本が Git の TS ファイルだと分かった
  • 朝の「Turso」という答えは、9月25日の計画と、すでに作ってあった D1 を読んだら改まった。過去に検討した覚えがあるなら、資料を探させてから決める
  • データを移し替えるときは、TS ファイルのハッシュを前後で記録しておけば、ファイルが変わっていないことを確かめられる

これから

Phase 1 には、まだ1つ項目が残っている。 10月12日までの1週間、朝の統計チェーンの中で様子を見る。 新しいデータが無い日に TS の差分が出ないか、手編集の検知が誤ってチェーンを止めないか。 D1 を正本にした仕組みが本当に静かに回るのかは、この1週間を過ぎないと分からない。 メモリ価格を移す Phase 2 は、そのあとになる。

#Cloudflare D1#Turso#SSOT#統計データ#Claude Code#Codex