はじめに
やりたかったことは、本当にこれだけでした。
本番の Cloudflare D1 に、ある条件を満たすレコードが何件あるか知りたい。
SQL にすれば SELECT COUNT(*) ... の一行です。5分で終わるはずでした。
ところが、ここから数日が溶けました。しかも最後まで、SQL は一文字も間違っていませんでした。溶けた原因は全部、SQL の外側 —— wrangler のファイル渡しの挙動、PowerShell から外部コマンドを呼ぶときの実行文脈、そして言語仕様の伏兵 —— にありました。
この記事は、その数日で順番に踏んでいったものを、踏んだ順に並べた失敗談です。
同じ構成(Cloudflare D1 + wrangler CLI + Windows PowerShell)で作業している人が、同じ穴に落ちないように、各場面で「で、どうすればよかったのか」を残します。
記事中のデータベース名・ユーザーID・テーブル名などはすべてダミーに置き換えています。実際の本番識別子ではありません。
環境
追試の前提として明記します。CLI の挙動はバージョンで変わる可能性があるためです。
- Cloudflare Workers + D1
- wrangler CLI:
X.Y.Z - Node.js:
vXX.X.X - Windows + PowerShell 7(pwsh)
後半で「wrangler のこのバージョンではこう動いた」という話が出てきますが、少なくとも私の環境では、という前提で読んでください。
1. 「件数を見たいだけ」だったはずが
最初はこれで十分でした。
npx wrangler d1 execute app-db --remote --command "SELECT COUNT(*) AS n FROM users WHERE created_at > '2026-06-01'"
問題は、この件数チェックを何度か回したくなったことです。
条件を変えながら再実行したいし、毎回長いコマンドを手で打つのも事故のもとです。そこで「.ps1 にして再実行できるようにしよう」と思いました。
これが、長い旅の入り口でした。
2. --file にしたら、行が返ってこない
スクリプト化するなら、SQL はファイルに分けて --file で渡すのが素直だろう、と考えました。
npx wrangler d1 execute app-db --remote --file ".\\count.sql"
ところが、返ってきたのは欲しい件数ではなく、実行サマリでした。
(概略)
Total queries executed: 1
Rows read: 0
Rows written: 0
Database size (MB): 1.10
行が、ない。COUNT の結果が、どこにもありません。
さらに不穏だったのが、ただの SELECT のはずなのに changed_db: true と報告されたことです。
「read-only のつもりの SELECT が、DB を変更した判定になっている」—— もし changed_db を見て読み取り専用かどうかを判定するような安全弁を自分で組んでいたら、ここで誤検知します。
少なくとも私の環境では、--file 経由だと欲しかった SELECT 行を安定して取得・パースできず、実行サマリ側に引っ張られて混乱する原因になりました。
学び: 行(SELECT の結果)をスクリプト側で扱いたいなら、
--fileより--commandに寄せる。--fileはバッチ/マイグレーション用途と割り切るのが無難。
3. じゃあ --command だ、と思ったら今度は EXIT 1
--command に戻します。ただ、せっかくなので SQL を見やすく整形して、複数行のまま渡しました。
$sql = @"
SELECT COUNT(*) AS n
FROM users
WHERE created_at > '2026-06-01'
"@
npx wrangler d1 execute app-db --remote --command $sql
落ちました。EXIT 1。
複数行の SQL をそのまま --command に渡すと、うまく解釈されずに失敗することがあります。改行を畳んで一行にしたら通りました。
学び:
--commandに渡す SQL は、単一行に正規化してから渡す。
ここまでで「行が欲しいなら --command」「SQL は一行」という二つの前提が揃いました。
手元の端末で叩く分には、これで件数が返ってきます。
問題は、これを .ps1 にした瞬間に起きました。
4. ここからが本番 — 端末では通るのに、スクリプトだと落ちる
同じコマンドです。
同じ DB、同じ --remote、同じ --command、同じ一行 SQL。
- 端末に直接
npx wrangler ...と打つ(interactive)→ 通る。件数が返る。 - それを
.ps1に書いてpwsh -File .\\count.ps1で実行する → 落ちる。
最初は「スクリプトの書き方をどこか間違えたんだろう」と思いました。
が、いくら見直しても、叩いているコマンド自体は端末で成功したものと同じです。
スクリプト実行は、毎回ぴったり同じバイト数で落ちていました。出力の長さが常に一定で、しかも JSON 配列の開き括弧 [ すら含まれていません。
これは「SQL を実行した結果がエラーだった」のではなく、SQL を実行する前段で、毎回同じ場所で止まっているサインと考えられます。クエリが DB に届く前に転んでいる可能性が高い。
そこから、容疑者を一つずつ消していきました。
容疑者1:SQL が悪い?
シロでした。
端末では、もっと複雑な SQL(相関サブクエリ + LIMIT)も、ダミー値を並べた IN 句も、ちゃんと通ります。単純な COUNT が通らないわけがありません。
容疑者2:IN 句の組み立てが壊れている?
これもシロでした。
スクリプト内で組み立てた IN リストを実行前に検査したところ、要素数も、各要素の形式も、長さも、すべて期待どおりでした。文字列としては正しく組めていました。
容疑者3:--file と --command の違い?
これもシロでした。
--command 版でも --file 版でも、スクリプト経由なら同じバイト数で落ちます。フラグの違いではありません。
SQL でもない。IN 句でもない。フラグでもない。--remote も同じ。端末では通る。
消去法で残ったのは、ただ一つでした。
端末で直接打つか、スクリプトの中から外部コマンドとして呼ぶか。
この違いです。
スクリプトの中では、だいたいこう書いていました。
$out = & npx wrangler d1 execute app-db --remote --command $sql --json 2>&1 | Out-String
& npx ... で外部コマンドを呼び、2>&1 で標準エラーを混ぜ、Out-String で文字列化する。一見ふつうです。
ただ、問題が起きていた境界は、このあたりだと考えられます。
PowerShell から npx(実体は .cmd)を呼ぶときの解決のされ方、標準エラーの取り込み方、TTY ではない出力 —— このあたりの呼び出し境界のどこかで、wrangler が件数を出す前に転んでいたようです。
「毎回同じバイト数」「[ すら出ない」は、その見立てと整合します。
切り分けの教訓は、技術そのものよりやり方でした。
学び: 「端末では動くのにスクリプトだと動かない」ときは、まったく同じコマンドを、端末とスクリプトの両方で実行して対比する。SQL・フラグ・接続先をすべて固定したまま実行文脈だけを変えれば、原因が SQL 側なのか呼び出し側なのか、一手で絞れる。
最終的にどう直したかは後述します。先に、この道中で踏んだもう一つの伏兵を紹介します。
5. 道中の伏兵 — PowerShell の変数名は大文字小文字を区別しない
スクリプトをいじっている最中、書き込み先のファイルが意図しない名前で生成されるという別の事故も踏みました。
切り出すと、こういう構図です。
$PAYLOAD = "C:\\work\\out.json" # 書き込み先パスのつもり(大文字)
# ... 途中で ...
$payload = [ordered]@{ count = 0 } # 中身のオブジェクトのつもり(小文字)
[System.IO.File]::WriteAllText($PAYLOAD, ($payload | ConvertTo-Json))
「大文字 $PAYLOAD はパス、小文字 $payload は中身。別の変数」
そのつもりでした。
ところが、PowerShell の変数名は大文字小文字を区別しません。$PAYLOAD と $payload は同じ変数です。
二行目の代入で、パス文字列はオブジェクトに上書きされていました。
結果、WriteAllText の第一引数(書き込み先パス)に渡っていたのはオブジェクトの方で、それが文字列化されて、
System.Collections.Specialized.OrderedDictionary
という名前のファイルを作りにいっていました。
パスがオブジェクトの型名に化ける、という地味に気づきにくい事故です。
直し方はシンプルで、名前を「大文字小文字違い」ではなく別物にし、書き込み前に型を確かめるだけです。
$PayloadPath = "C:\\work\\out.json"
$payloadObj = [ordered]@{ count = 0 }
if ($PayloadPath -isnot [string]) { throw "PayloadPath must be a string" }
[System.IO.File]::WriteAllText($PayloadPath, ($payloadObj | ConvertTo-Json))
学び: パス文字列と、その中身のオブジェクトは、prefix/suffix を変えて明確に別名にする(
...Pathと...Objなど)。そして書き込み前に「これは文字列か」を型チェックする。大文字小文字だけで区別したつもりになると、ここで刺さる。
6. ついでに踏んだ小さな穴
主役級ではないですが、同じ数日で踏んだので供養しておきます。
-
ダウンロードした
.ps1が実行ブロックされる
Mark-of-the-Web が付いていて弾かれることがあります。Unblock-File .\\count.ps1で解除しました。 -
日本語コメントが文字化けする
powershell.exe(Windows PowerShell 5.x)とpwsh(PowerShell 7)でデフォルトのエンコーディングが違い、BOM なし UTF-8 のスクリプトが化けることがあります。pwshに統一し、必要なら BOM 付きで保存します。 -
exitでシェルごと落ちる
対話セッションに貼り付けたコードにexitを入れると、スクリプトではなく PowerShell セッション自体が終了します。そもそも「スクリプト化したい」と思った動機の一つでもあります。
7. 最終的に落ち着いた形
長い回り道の末、件数チェックは最終的にこの形に落ち着きました。
以下は最小化した考え方です。実運用では、stdout / stderr / exit code を分けて取り、失敗時にどこで落ちたかを見えるようにしました。
# SQL はファイルに置く。読み込んで「改行を畳んで一行」にしてから --command に渡す。
$sql = ((Get-Content -LiteralPath ".\\count.sql") -join " ").Trim()
npx wrangler d1 execute app-db --remote --command $sql --json
ポイントは三つです。
-
行が欲しいので
--commandに寄せる
少なくとも私の環境では、--file経由だと SELECT 結果を安定して扱えず、実行サマリ側に引っ張られました。 -
ファイルから読んで一行に正規化する
整形した複数行 SQL をそのまま渡さない。-join " "で改行だけ畳みます。\s+で全空白を潰すと文字列リテラル内のスペースまで壊すので、行連結にとどめるのが無難です。 -
本番は
--remote必須
付け忘れるとローカル dev DB を見て、件数が合わずにまた悩みます。
これで、最初にやりたかった「件数を一発で見る」が、ようやく安定して返ってくるようになりました。
8. おわりに
やりたかったのは SELECT COUNT(*) の一行だけでした。
それが、ファイル渡しの仕様、実行文脈の差、言語仕様の伏兵と、何層も罠を抱えていました。SQL は最後まで正しかったのに、です。
教訓を一つだけ持ち帰るなら、これです。
「端末では動くのにスクリプトだと動かない」ときは、同じコマンドを文脈だけ変えて対比する。SQL を疑う前に、呼び出し方を疑う。
余談ですが、こうした「ローカルと本番の環境差分に振り回されたくない」「本番DBやAI APIへのアクセスを、後から安全に観測・制御したい」という動機もあって、現在 AI API ゲートウェイ qzira を開発しています。AI API の利用量・予算・安全弁を扱うゲートウェイで、気軽に試せる無料プランも用意しています。
- 予算管理まわりの機能を紹介した前回記事はこちら
- qzira 公式サイトはこちら → https://qzira.com


