Communitygithub.com

Turso / libSQL パターン

libSQL ドライバと埋め込みレプリカを使用したエッジホスト型 SQLite データベース

libSQL パターン とは?

このスキルは、Turso(libSQL ベースのマネージド SQLite サービス)を使用したエッジ/サーバーレスアプリケーションのデータベースアクセスパターンとベストプラクティスを網羅しています。主なプラットフォームは、Node.js、Bun、Deno、Cloudflare Workers、Vercel Edge Functions などで、`@libsql/client` と `@libsql/client/web` の適切なインポートパスを区別し、実行環境に応じた最適な接続方法を提供します。重要なのは、アトミックな複数ステートメント操作には `batch()` を推奨し、インタラクティブな `transaction()` は後続のクエリが以前の結果に依存する場合のみ使用すること、そして読み取りレイテンシをゼロに近づけるための埋め込みレプリカのセットアップ方法を詳細に説明している点です。さらに、`batch()` と `transaction()` でのトランザクションモード指定の強制、SQL インジェクションを防ぐパラメータ化クエリの使用、エッジランタイムでの誤ったインポートパスの回避など、重大な問題を防ぐためのレッドフラグリストも含まれています。このスキルは、分散データベースの特性(シングルライターモデル、書き込みレイテンシ 15~50ms、マルチリージョンレプリケーション)を理解した上で、効率的で安全な Turso アプリケーションを構築するための判断基準と実践的なコード例を提供します。

対応✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agents-inc/skills/tree/HEAD/src/skills/api-baas-turso

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

libSQL パターン は何をしますか?

クイックガイド: Turso データベースへのアクセスには常に @libsql/client を使用します。単一クエリには execute()、アトミックな複数ステートメント操作(インタラクティブトランザクションより優先)には batch()、そして後続のクエリが以前の結果に依存する場合にのみ transaction() を使用します。ファイルシステムアクセスがないエッジ/サーバーレスランタイムでは、@libsql/client/web からインポートします。ゼロレイテンシ読み取りのために、ローカルファイル URL と syncUrl を指定して埋め込みレプリカを構成します。すべての書き込みはプライマリに転送されるため、15~50ms の書き込みレイテンシを考慮して設計してください。Turso は SQLite をベースとしています。シングルライターモデル、ALTER TABLE ... ADD CONSTRAINT なし、ストアドプロシージャなしです。


<critical_requirements>

重要:このスキルを使用する前に

すべてのコードは CLAUDE.md のプロジェクト規約に従うこと(ケバブケース、名前付きエクスポート、インポート順序、import type、名前付き定数)。

(複数ステートメントのアトミック操作には、トランザクションモードを指定して batch() を必ず使用すること。transaction() より高速かつ安全で、1回のラウンドトリップで実行されます。)

(ファイルシステムアクセスのないエッジ/サーバーレスランタイム(Cloudflare Workers、Vercel Edge Functions)では、@libsql/client/web から必ずインポートすること**。ベースの @libsql/client はネイティブバインディングを含むため、これらの環境では失敗します。)**

(batch() と transaction() の第2引数にはトランザクションモード("write"、"read"、または "deferred")を必ず指定すること**。デフォルトは "deferred" で、INSERT/UPDATE/DELETE の書き込みロックの取得に暗黙的に失敗します。)**

(短命なプロセスでは、クライアントが不要になったら必ず client.close() を呼び出すこと**。開いているクライアントは接続とファイルハンドルを保持します。)**

(クライアント実行中は、ローカルの埋め込みレプリカデータベースファイルに直接アクセスしないこと**。同時アクセスはデータ破損の原因になります。)**

</critical_requirements>


自動検出: Turso、libSQL、@libsql/client、createClient、turso.io、埋め込みレプリカ、syncUrl、syncInterval、turso db、turso group、libsql、.turso.io、TURSO_DATABASE_URL、TURSO_AUTH_TOKEN

使用すべき場合:

  • 任意のランタイム(Node.js、エッジ、サーバーレス)から Turso ホストの SQLite データベースにクエリを実行する場合
  • リモートプライマリから同期されたゼロレイテンシのローカル読み取りを行う埋め込みレプリカをセットアップする場合
  • テナントごとにデータベースを持つマルチテナント SaaS(Turso は数百万のデータベースをサポート)
  • コネクションプーリングの複雑さを必要としないデータベースを必要とするサーバーレス/エッジ関数
  • batch() を使用したアトミックな複数ステートメント操作、またはインタラクティブな transaction() の実行
  • Turso CLI を使用したデータベースグループとマルチリージョンプレースメントの管理

使用すべきでない場合:

  • 強いマルチライター一貫性を必要とする書き込み集中型ワークロード(Turso はシングルライター、書き込みはプライマリに転送されます)
  • PostgreSQL の機能(変更を含む CTE、ストアドプロシージャ、高度な制約)を必要とする複雑なリレーショナルクエリ
  • 複数のデータベースにまたがる複雑な分散トランザクション
  • 大規模な分析データセット(SQLite の行サイズと同時実行性の制限が適用されます)

詳細リソース:

  • examples/core.md -- クライアント設定、execute、batch、トランザクション、インポートパス
  • examples/embedded-replicas.md -- ローカルレプリカ、同期、オフラインモード、暗号化
  • reference.md -- 決定フレームワーク、型定義、CLI コマンド、ルックアップテーブル

哲学

Turso は、libSQL(SQLite のフォーク)をマルチリージョンレプリケーションを備えたマネージドサービスとしてホストすることで、SQLite をエッジに導入します。@libsql/client ドライバーは、リモートの Turso データベース、ローカルの SQLite ファイル、インメモリデータベース、またはリモートプライマリから同期する埋め込みレプリカのいずれに接続する場合でも、同じように機能する統一された API を提供します。

コア原則:

  1. トランザクションよりもバッチ -- batch() はすべてのステートメントを1回のラウンドトリップで送信し、暗黙的なトランザクション内で実行します。インタラクティブな transaction() は複数のラウンドトリップを必要とし、データベースロック(タイムアウト5秒)を保持します。クエリ間で条件付きロジックが必要ない限り、batch() を使用します。
  2. 書き込みは常にプライマリにヒット -- 埋め込みレプリカを使用する場合でも、書き込みはリモートプライマリデータベースに転送されます。書き込みレイテンシは、プライマリリージョンへの距離に応じて15~50msです。これに合わせて設計します。楽観的 UI、バックグラウンド同期、書き込み集中型のホットパスは避けてください。
  3. 読み取りのための埋め込みレプリカ -- リモートプライマリから同期されたローカル SQLite ファイル。読み取りはマイクロ秒レベルです。書き込みはリモートに転送されます。ローカルファイルは書き込み成功後に更新されます(read-your-writes セマンティクス)。
  4. 2つのインポートパス -- @libsql/client には Node.js 用のネイティブ SQLite バインディングが含まれており、file: URL をサポートします。@libsql/client/web は純粋な JS/WASM で、エッジランタイム(Cloudflare Workers、Vercel Edge Functions)向けであり、ローカルファイルを開くことはできません。
  5. SQLite セマンティクス -- Turso は SQLite です。ADD CONSTRAINT なし、ストアドプロシージャなし、LISTEN/NOTIFY なし、シングルライター WAL モード。Turso を選択する前に、SQLite の制限を理解してください。

コアパターン

パターン 1: クライアント設定

createClient() でクライアントを作成します。url が接続タイプを決定します。リモート Turso の場合は libsql://、ローカル SQLite(Node.js のみ)の場合は file:、インメモリ(テスト)の場合は :memory:。authToken には常に環境変数を使用してください。資格情報をハードコーディングしないでください。

シングルトンモジュールや悪い例を含む完全な設定パターンについては、examples/core.md を参照してください。


パターン 2: クエリの実行

execute() は単一の SQL ステートメントを実行します。常に args を使用したパラメーター化クエリを使用してください。文字列補間は絶対に使用しないでください。

// 位置指定: args を配列として
await client.execute({
  sql: "SELECT * FROM users WHERE id = ?",
  args: [userId],
});

// 名前付き: args をオブジェクトとして(ベア名は SQL 内の :name、@name、$name と一致します)
await client.execute({
  sql: "INSERT INTO users (name, email) VALUES (:name, :email)",
  args: { name, email },
});

rows(Array<Row>)、columns、rowsAffected、lastInsertRowid(bigint)を含む ResultSet を返します。型付き結果マッピングと悪い例については、examples/core.md を参照してください。


パターン 3: バッチ操作

batch() は、複数のステートメントを1回のラウンドトリップでアトミックに実行します。すべて成功するか、すべてロールバックされます。常に第2引数としてトランザクションモードを指定してください。

const results = await client.batch(
  [
    { sql: "INSERT INTO users (name) VALUES (?)", args: ["Alice"] },
    {
      sql: "INSERT INTO audit_log (action, entity_id) VALUES (?, last_insert_rowid())",
      args: ["user_created"],
    },
  ],
  "write", // INSERT/UPDATE/DELETE には必須
);

変更には "write"、SELECT のみ(パラレル実行を許可)には "read"、読み取り専用で開始してエスカレートするには "deferred" を使用します。last_insert_rowid() は同じバッチ内のステートメント間で機能します。マルチインサートと読み取り専用バッチパターンについては examples/core.md を、トランザクションモードの比較表については reference.md を参照してください。


パターン 4: インタラクティブトランザクション

transaction() は、後続のクエリが以前のクエリの結果に依存する場合にのみ使用します。データベースロック(アイドルタイムアウト5秒)を保持し、複数のラウンドトリップを必要とします。常に try/catch/finally で close() を使用してください。

const tx = await client.transaction("write");
try {
  const { rows } = await tx.execute({
    sql: "SELECT balance FROM accounts WHERE id = ?",
    args: [fromId],
  });
  // ... 結果に基づく条件付きロジック ...
  await tx.commit();
} catch (error) {
  await tx.rollback();
  throw error;
} finally {
  tx.close();
}

すべてのステートメントが事前にわかっており、条件付きロジックがない場合は、代わりに batch() を使用してください。在庫チェック付き購入の完全な例については、examples/core.md を参照してください。


パターン 5: 異なるランタイム用のインポートパス

// Node.js、Bun、Deno(ファイルシステムアクセス、ネイティブバインディングあり)
import { createClient } from "@libsql/client";

// ファイルシステムのないエッジ/サーバーレスランタイム(Cloudflare Workers、Vercel Edge)
import { createClient } from "@libsql/client/web";

ベースの @libsql/client には、エッジランタイムで失敗するネイティブ SQLite バインディングがバンドルされています。@libsql/client/web は純粋な JS/WASM ですが、ローカルの file: URL を開くことはできません。完全な Cloudflare Worker の例については、examples/core.md を参照してください。


パターン 6: 埋め込みレプリカ

リモートの Turso プライマリから同期するローカル SQLite ファイル。読み取りはローカル(マイクロ秒)、書き込みはリモートに転送(15~50ms)。

const client = createClient({
  url: "file:local-replica.db",
  syncUrl: process.env.TURSO_DATABASE_URL,
  authToken: process.env.TURSO_AUTH_TOKEN,
  syncInterval: 60, // 60秒ごとに自動同期
});
await client.sync(); // 最初の読み取りの前にローカルレプリカを生成

重要ポイント: ローカルレプリカには file: URL、リモートプライマリには syncUrl、起動時に sync() を呼び出し、読み取りはローカル、書き込みはリモートに転送され、read-your-writes セマンティクスがあります。

使用すべき場合: VM、VPS、コンテナ、またはファイルシステムアクセスがある長時間実行プロセス。ファイルシステムのないサーバーレス/エッジランタイムでは利用できません。手動同期、オフラインモード、暗号化を含む完全なパターンについては、examples/embedded-replicas.md を参照してください。


パターン 7: データベースグループとマルチリージョン

Turso はデータベースをグループに整理します。各グループにはプライマリリージョンとオプションのレプリカロケーションがあります。グループ内のすべてのデータベースは、そのロケーションを継承します。

どのレプリカが読み取りを処理するかに関係なく、すべての書き込みはプライマリリージョンに送られます。ロケーションを追加すると、グローバルな読み取りレイテンシは向上しますが、書き込みレイテンシは減少しません。書き込みレイテンシは、プライマリリージョンへの距離によって決まります。

完全な Turso CLI コマンドリファレンス(turso db create、turso group create、turso db shell など)については、reference.md を参照してください。


<decision_framework>

決定フレームワーク

batch() vs transaction()

すべての SQL ステートメントは事前にわかっていますか(ステートメント間に条件付きロジックはありませんか)?
+-- YES --> batch() を使用(1回のラウンドトリップ、暗黙的トランザクション、推奨)
+-- NO  --> 後続のステートメントは以前のステートメントの結果に依存しますか?
    +-- YES --> transaction() を使用(インタラクティブ、複数ラウンドトリップ、ロック保持)
    +-- NO  --> batch() を使用

インポートパスの選択

実行環境は?
+-- Node.js / Bun / Deno / VM / コンテナ
|   +-- 埋め込みレプリカ(ローカルファイル)が必要ですか?
|   |   +-- YES --> @libsql/client に file: URL + syncUrl
|   |   +-- NO  --> @libsql/client に libsql:// URL
+-- Cloudflare Workers / Vercel Edge / ブラウザ / ファイルシステムのないサーバーレス
    +-- @libsql/client/web(リモート接続のみ、file: URL 不可)

埋め込みレプリカ vs リモートのみ

プロセスは長時間実行で、ファイルシステムアクセスがありますか?
+-- YES --> サブミリ秒の読み取りレイテンシが必要ですか?
|   +-- YES --> 埋め込みレプリカ(file: URL + syncUrl)
|   +-- NO  --> リモートのみの方がシンプル(libsql:// URL)
+-- NO(サーバーレス、エッジ、短命)
    +-- リモートのみ(@libsql/client/web、libsql:// URL)

トランザクションモードの選択

バッチ/トランザクションはどのような操作を実行しますか?
+-- SELECT クエリのみ --> "read"(レプリカでのパラレル実行を許可)
+-- INSERT / UPDATE / DELETE あり --> "write"(排他ロックを取得)
+-- 呼び出し時に不明 --> "deferred"(読み取りで開始、必要に応じてエスカレート)

</decision_framework>


<red_flags>

レッドフラグ

高優先度の問題:

  • batch()/transaction() でのトランザクションモードの欠落 -- 第2引数を省略するとデフォルトで "deferred" になり、読み取り専用で開始され、INSERT/UPDATE/DELETE の書き込みロックの取得に暗黙的に失敗する可能性があります。常にモードを明示的に指定してください。
  • エッジランタイムでの @libsql/client の使用 -- ベースパッケージには、Cloudflare Workers、Vercel Edge Functions、および同様の環境で失敗するネイティブ SQLite バインディングがバンドルされています。代わりに @libsql/client/web を使用してください。
  • SQL での文字列補間 -- execute(\SELECT * FROM users WHERE id = '${id}'`)は SQL インジェクションの脆弱性です。常にargs` を使用したパラメーター化クエリを使用してください。
  • 埋め込みレプリカファイルへの直接アクセス -- libSQL クライアントの実行中に別の SQLite クライアントでローカルの .db ファイルを開くと、データ破損が発生します。クライアントを介してのみアクセスしてください。

中優先度の問題:

  • batch() で十分な場合の transaction() の使用 -- インタラクティブトランザクションは、タイムアウト5秒のデータベースロックを保持し、複数のラウンドトリップを必要とし、他のライターをブロックします。事前に決定されたステートメントセットには batch() を使用してください。
  • client.close() の呼び出し忘れ -- 短命なプロセス(CLI スクリプト、テストの後片付け)で、クライアントを閉じるのを忘れると、接続とファイルハンドルが開いたままになります。
  • 埋め込みレプリカでの書き込みレイテンシの無視 -- 読み取りはマイクロ秒(ローカル)ですが、書き込みは15~50ms(リモートプライマリに転送)です。それに応じて設計してください。タイトな書き込みループは避けてください。
  • syncInterval の設定が低すぎる -- 各同期は、変更されたすべてのフレーム(各4KB)をプルします。書き込みが多いデータベースでサブ秒間隔に設定すると、かなりのネットワークと I/O のオーバーヘッドが発生します。

よくある間違い:

  • パッケージ名の誤り -- パッケージは @libsql/client であり、libsql-client、@turso/client、turso-client ではありません。
  • args オブジェクトの名前付きパラメータプレフィックス -- Args はベア名を使用します: { name: "Alice" } は SQL 内の :name、@name、$name と一致します。プレフィックスは含めないでください: { ":name": "Alice" } は一致しません。
  • lastInsertRowid が数値であると期待する -- これは bigint | undefined です。数値が必要な場合は、明示的に変換してください: Number(result.lastInsertRowid)。ただし、非常に大きな rowid では精度が失われる可能性があることに注意してください。
  • アトミック操作に executeMultiple() を使用する -- executeMultiple() は、パラメーター化されていない、暗黙的なトランザクションがない生の SQL テキスト(セミコロン区切り)を実行します。アトミックなパラメーター化操作には batch() を使用してください。

落とし穴とエッジケース:

  • batch() ステートメントはトランザクションを共有しますが、パラレルではありません -- これらは順次実行されます。last_insert_rowid

</red_flags>

Individual skills in this repo

This repo contains 11 individual skills — each has its own dedicated page.

関連スキル