Cloudflare D1を読み書きできる、JetBrains IDE向けのJDBCドライバーをKotlinで作ってみた

Cloudflare D1を使ったアプリを作っていると、D1の中身を確認したり更新したりしたくなることがあります。

その際、使い慣れているJetBrains IDEやDataGrip(以降は DataGrip )で行えるとよいです。

 
ただ、執筆時点では、Cloudflare公式のD1向けJDBCドライバーを見つけられませんでした。

また、コミュニティ系のJDBCドライバーは存在しますが、DataGripでの表形式によるデータ編集には対応してなさそうでした。

 
そこで、Claude Codeとともに、Cloudflare D1向けに、JetBrains IDE専用のJDBCドライバーをKotlinで書いてみたことから、メモを残します。

 
目次

 

環境

  • mac
  • Claude Desktop 1.30096.1
  • OpenSpec 1.8.0
  • JDK 21
  • Kotlin 2.4.10
  • Kotest 6.2.3
  • Gradle 9.7.0

 
なお、今回はClaude DesktopのSSHセッションを使ってdevcontainerへと接続し、devcontainerの中でClaude Codeを実行しています。

 

技術スタックについて

実装はClaude Codeに任せることもあり、可能な限り依存の少ないパッケージ構成を選びました。

  • プロダクションコード
    • Kotlinの標準ライブラリ ( kotlin-stdlib ) のみ
      • Cloudflare D1と通信するときに必要なJSONパーサーなどは自作
  • テストコード
    • Kotlinの標準ライブラリに加え、Kotestも利用
      • Kotestではプロパティベーステストも扱えることから、自分でプロパティベーステスト基盤を作るよりは手間がかからなさそうと判断

 
また、同じJVMに別バージョンのkotlin-stdlibが同居することによる衝突を避けるため、 kotlin-stdlibのrelocationを行っています。

 

Cloudflare D1向けJDBCドライバーの注意点

以下のことに気をつけて作りました。

 

アクセス時のURLに秘匿情報を乗せない

Cloudflare D1へはAPIを経由してアクセスします。その時、 User API TokenとAccount API Tokenのどちらかが必要です。

Account API Token は /user/* 系の API を呼べないことから、今回のドライバーは D1 の /accounts/ 配下の API だけを使うことにしました。この結果、User API Token と Account API Token のどちらでも動くようになりました。

例えば、Account API Tokenは、以下のドキュメントに従って発行できます。
Account API tokens · Cloudflare Fundamentals docs

 
APIトークンは秘匿情報にあたることから、JetBrains IDEの画面に平文で閲覧できないようにします。

そこで、アクセス時のURLにAccount IDやAPIトークンを設定する代わりに、以下の値として渡すようにしました。

  • Account ID
    • userに設定する
  • APIトークン
    • passwordに設定する

 
これにより、URLは jdbc:cloudflare:d1://<database_id> となり、データベースIDだけが乗ることになります。

 

対話的トランザクションが無くても、DataGripでの編集を可能にする

Cloudflare D1では、 BEGINCOMMIT を複数リクエストにまたがって行う、対話的トランザクションがサポートされていないようです。

 
一方、今回DataGripの挙動を確認してみたところ、DataGripで編集するときは Tx: Auto であっても、

  • setAutoCommit(false) で、トランザクションを開始
    • 合わせて setTransactionIsolation() も呼ばれる
  • rollback() で、セッションをきれいにするための防御的なrollbackを実施
  • DML を実行
  • commit() でDMLの結果を確定

という順で動くように見えました。

 
そのため、トランザクションがないからと言って、

  • setAutoCommit(false)
  • setTransactionIsolation()
  • rollback()
  • commit()

を実装せずに「未対応エラー」としてしまうと、DataGripでの編集でのSubmitが失敗してしまいます。

 
今回のドライバーは JetBrains IDE専用のため、JDBC の一般的な作法に従うよりは、JetBrains IDEで動くことを優先しました。

そこで、 writesInManualMode というフラグで状態を管理して画面操作に対応できるようにしています。

タイミング writesInManualModeの値
setAutoCommit(...) が呼ばれた false にリセット
自動コミット OFF 中に「INSERTなど、書き込みの可能性がある文」を実行した trueにする
commit() が呼ばれた false にリセット(確定したので取り消し対象は消えた)

 
そのうえで、 commit()rollback() を次のように扱います。

  • commit()
    • 何もせずに成功を返す
      • D1では各文は送信した時点で確定済みなので、「成功」という戻り値で問題なく、何もしなくて良いため
  • rollback()
    • writesInManualModefalse なら、取り消すべき書き込みが無いので何もせず成功とする
    • true なら、書き込みの可能性がある文がすでに D1 で確定していて取り消せないため、SQLFeatureNotSupportedException を投げる
      • エラーメッセージとして「実行済みの文の効果は取り消されず、残っています」を渡す

 
また、DataGripの挙動として、以下もあったのでメモしておきます。

  • Tx: Auto であっても、1セルの変更であっても、無条件にトランザクション操作となる
  • supportsTransactions()false と設定していても参照されない
  • Test Connection では、 Connection.getWarnings() を呼び、 0A000 が返るとそこで失敗扱いになる

 

DBMS名をSQLiteにする

DataGripでデータを編集する場合、DMLの生成はDialect設定に従うのではなく、JDBCの getDatabaseProductName() で決まるようでした。

これにより、Dialect設定を SQLite にしていたとしても、DBMS名( getDatabaseProductName() )を返信しない( getMetaData が非対応エラーを返す)状態だと、SQLiteでは非サポートの as のないテーブル別名が付いた状態でUPDATEなどが発行されてしまいます。

そのSQLをD1へ送ったとしても、D1からはエラーが返ってきてしまいます。

 
そこで、ワークアラウンド的に以下の対応をしています。なお、D1の実体はSQLiteなので問題なく動くようです。

  • DBMS名は SQLite
  • バージョンは 3.45.1
    • 3.45の jsonb はD1がサポートしていたものの、 3.48 の if() はサポートしていなかったことから、 3.45.1 を採用した
    • なお、 sqlite_version()PRAGMA compile_options は D1 が拒否したため、それらは使えなかった
  • 接続先はD1
    • getDriverName()Cloudflare D1 JDBC Driver for JetBrains IDEs と返すことにより、識別できるようにした

 

スキーマは main にしないと、DataGrip上にツリーが出ない

getCatalogs()getSchemas() が両方空だと、DataGripがテーブルの置き場所(ノード)を作ることができず、 getTables() が呼ばれませんでした。

この結果、ツリーが空のままで表示されてしまいました。

そこで、スキーマ名として main を返すことで、この事態を回避しました。

 

動作確認

環境設定

ドライバーの設定

DataGripのDatabaseツールウィンドウにある + をクリックし、 Driver を選択します。

次に、Driver Filesの + をクリックし、 ./build/libs/d1-kotlin-jdbc-driver-0.0.3-all.jar を選択します。

その後、Classで dev.thinkami.d1jdbc.D1Driver を選択します。

 

データソースの設定

Cloudflareのダッシュボードを参照しながら、以下を設定します。

  • User
    • Account ID
  • Password
    • Account API Token
  • URL
    • jdbc:cloudflare:d1://<database_id>

 
あとは Test Connection ボタンで接続できることを確認し、その後データが表示されればOKです。

 

動作確認時のスクリーンショット

今回はidが 750 のBreezeリンゴの部分を編集します。

編集する前は、 [リンゴ]今日は... の間に半角スペースが入っています。

 
続いて、半角スペースを削除します。これにより、 Submit ボタンが有効になっています。

 
Submitすると、更新が完了しました。リロードしても更新後の値のままになっていることから、成功しているようです。

 

謝辞

リトライ条件・D1 非対応 PRAGMA への静的応答・内部テーブルの除外・meta フィールドと JDBC API の対応付けといった「挙動の仕様」は、前述のコミュニティ版JDBCドライバーのJava実装から多くを学びました。ありがとうございました。

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi/d1-kotlin-jdbc-driver