HN 日本語サマリー

← 一覧へ戻る
プログラミング

Show HN: SQLBraid – クエリビルダーDSLなしでTypeScriptで直接SQLを記述

Show HN: SQLBraid – Write SQL directly in TypeScript without a query-builder DSL (github.com)

3 pointsby Clickin0 コメント

要約

SQLBraidは、TypeScript内で直接SQLクエリを記述できるツールです。クエリビルダーDSLを介さずに、型安全な方法でSQLを扱えるように設計されています。

全文翻訳

SQLBraid

한국어

クエリビルダー不要。TypeScriptで直接SQLを記述。

SQLBraidは、TypeScript向けのSQLファーストなデータアクセスライブラリで、タグ付きテンプレート内で生のSQLを記述できます。補間された値は常にバインドパラメータとして分離され、MyBatisにインスパイアされた動的な条件はSQLコメント内に直接配置され、各クエリは返り値の行型または検証スキーマを宣言します。

PostgreSQL、MySQL、MariaDB、SQLite、Oracle、SQL Serverの6つのデータベース方言で共通の実行APIを提供し、Node.js、Bun、Deno、ブラウザ(SQLite WASM)、Cloudflare D1で実行できます。

const users = await db.all(sql.rows<User>`
  SELECT id, name
  FROM users
  /*@braid where*/
    /*@braid if ${name != null}*/   AND name = ${name}       /*@braid end*/
    /*@braid if ${teamId != null}*/ AND team_id = ${teamId}  /*@braid end*/
  /*@braid end*/
`);
// name = null, teamId = 7 の場合のレンダリングされたSQL -> (pg) SELECT id, name FROM users WHERE team_id = $1   [7]

ドキュメント · 5分クイックスタート · インタラクティブプレビュー · サポートマトリクス

モチベーション

私は職場でJavaフレームワークライブラリを保守しており、長年iBatis/MyBatis XMLマッパーを使用してきました。SQLを直接記述し、動的な条件をクエリ内に保持し、手作業での行のマッピングなしに型付きの結果を得られる点が気に入っています。

TypeScriptで同様のワークフローを探したとき、見つかったトレードオフに満足できませんでした。

  • 生のドライバ: パラメータバインディングは安全ですが、動的な WHERE フィルタを追加すると、すぐに煩雑な文字列連結とパラメータインデックスの管理が必要になります。
  • クエリビルダー: 型推論は素晴らしいですが、頭の中にあるSQLをライブラリ固有のメソッドチェーンDSLに変換する必要があります。
  • ORM: 生成されたクエリやオブジェクトモデルに依存するよりも、自分でSQLを書くことを好みます。

コーディングエージェントが利用可能になったので、自分で構築してみようと思いました。

最大のハードルは、JavaScriptにはJDBCに相当するものがないことでした。Javaでは、MyBatisは標準JDBCインターフェース(Connection、PreparedStatement、ResultSet)の上に位置しているため、ドライバごとにカスタム実行レイヤーを構築する必要がありません。JavaScriptには、pg、mysql2、oracledb、tedious、node:sqlite の間に標準インターフェースがなく、接続管理、プレースホルダー構文($1、?、:1、@p1)、トランザクション、ストリーミング、ストアドプロシージャはすべて異なる方法で機能します。

そのため、SQLBraidはMyBatisスタイルの動的SQLプロセッサだけでなく、データベース固有の機能を隠すことなく、さまざまなJavaScriptデータベースドライバを共通APIに接続する実行レイヤーも必要としました。

クイックスタート: SQLite

Node.js 22.18以降では、組み込みの node:sqlite モジュールを使用してSQLBraidをすぐに実行できます。データベースサーバーは不要です。

pnpm add sqlbraid @sqlbraid/sqlite

以下のコードを quickstart.mts として保存し、node quickstart.mts を実行してください。

import { DatabaseSync } from "node:sqlite";
import { createNodeSqliteDatabase } from "@sqlbraid/sqlite/node-sqlite";
import { sql } from "@sqlbraid/sqlite";

interface UserRow {
  id: string;
  name: string;
}

const native = new DatabaseSync(":memory:");
try {
  native.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)");
  native.prepare("INSERT INTO users (name) VALUES (?)").run("Ada");

  const db = createNodeSqliteDatabase(native);
  const userId = 1;
  const users = await db.all(sql.rows<UserRow>`
    SELECT id, name FROM users WHERE id = ${userId}
  `);

  console.log(users); // [{ id: "1", name: "Ada" }]
} finally {
  native.close();
}

id が文字列として返されるのはなぜですか? JavaScriptの number 型は、大きな64ビット整数(BIGINT)や正確な10進数(DECIMAL)で精度を失います。サイレントな丸めを防ぐため、SQLBraidはデフォルトで正確なデータベース数値型を文字列として返します。アプリケーションコードで変換するか、Standard Schemaをアタッチして自動的に変換できます(データ表現を参照)。

クイックスタート: PostgreSQL コネクションプール

コネクションプールはドライバ(例: pg または mysql2)で作成し、SQLBraidに渡します。SQLBraidは、操作ごとに1つの接続をリースするか、トランザクション全体で1つの接続をピン留めします。

pnpm add sqlbraid @sqlbraid/postgres pg
import { Pool } from "pg";
import { createPgPoolDatabase } from "@sqlbraid/postgres/pg";
import { sql } from "@sqlbraid/postgres";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = createPgPoolDatabase(pool);

const account = await db.one(sql.rows<{ id: string; email: string }>`
  SELECT id, email FROM accounts WHERE id = ${accountId}
`);

await db.tx(async (tx) => {
  await tx.execute(sql.command`UPDATE accounts SET active = true WHERE id = ${accountId}`);
});

他のデータベースについては、以下のGetting Startedガイドを参照してください:MySQL · MariaDB · Oracle · SQL Server · ブラウザSQLite & Cloudflare D1

仕組み

SQLBraidは3つのステップでテンプレートから実行可能なステートメントを構築します。

  1. ディレクティブ評価 (/*@braid ... */): ディレクティブは標準的なSQLブロックコメントを使用するため、エディタやSQLフォーマッタはクエリを有効なSQLとして扱います。SQLBraidは if、choose / when / otherwise、where、set、trim ブロックを順に評価し、非アクティブなブランチを削除し、複数行およびネストされたブロックをサポートします。 where と set は、先頭の AND/OR トークンや末尾のカンマを自動的に削除します。
  2. パラメータ抽出 (${value}): アクティブなSQLブランチで使用される値は、バインドパラメータとして送信されます。SQL文字列に連結されることはありません。オプションのコンパイラがない場合、JavaScriptはディレクティブが処理される前にすべてのテンプレート式を評価します。
  3. ドライバプレースホルダーレンダリング: どこでも同じ ${value} 構文を記述すると、ターゲットドライバアダプタはネイティブプレースホルダー($1、?、:1、@p1)を出力します。
sql.rows<User>`
  SELECT u.id, u.name
  FROM users u
  /*@braid if ${teamName != null}*/
    JOIN teams t
      ON t.id = u.team_id
     AND t.name = ${teamName}
  /*@braid end*/
  /*@braid where*/
    /*@braid if ${onlyActive}*/
      AND u.active = 1
      AND u.name IS NOT NULL
    /*@braid end*/
  /*@braid end*/
  /*@braid choose*/
    /*@braid when ${sort === "name"}*/    ORDER BY u.name
    /*@braid when ${sort === "created"}*/ ORDER BY u.created_at DESC
    /*@braid otherwise*/                  ORDER BY u.id
  /*@braid end*/
`;
// teamName = "core", onlyActive = true  ->  ... JOIN teams t ON t.id = u.team_id AND t.name = ? WHERE u.active = 1 AND u.name IS NOT NULL ORDER BY ...
// teamName = null,   onlyActive = false ->  SELECT u.id, u.name FROM users u ORDER BY ...
  • ゼロビルド実行 & オプションコンパイラ: すべてのディレクティブはビルドステップなしで実行時に機能します。非アクティブな /*@braid if ...*/ ブランチ内の ${...} 式の評価をスキップしたい場合(遅延評価)、オプションのコンパイラプラグイン(@sqlbraid/vite、@sqlbraid/webpack、または@sqlbraid/cli)を追加します。コンパイラは式をサーンク(thunk)でラップするだけです。最終的なSQLの組み立ては実行時に行われます(コンパイラが行うことを参照)。
  • 動的識別子 & SQLフラグメント: テーブル名、カラムリスト、再利用可能なサブ句など、SQL構造を動的に構成する必要がある場合は、明示的なヘルパー sql.ident、sql.list、sql.join、sql.fragment を使用します(構造的SQLフラグメントを参照)。

コンパイラを使用する場合、@sqlbraid/template を直接のランタイム依存関係としてインストールします。生成されたコードはそのヘルパーをインポートします。

結果宣言 & スキーマ検証

各クエリの期待される結果の種類を宣言します:行セットの場合は sql.rows<T>、INSERT/UPDATE/DELETE の場合は sql.command、ストアドプロシージャの場合は sql.call です。データベースが宣言された結果の種類と異なるものを返した場合、SQLBraidは例外をスローします(SQLタグと結果の種類を参照)。sql.rows<T> はTypeScriptの型宣言です。実行時に行フィールドと値を検証するには、Standard Schemaを渡します。

Standard Schema統合(例: Zod、Valibot、ArkType):

実行時に行を検証し、値を変換するために、型引数の代わりに任意のStandard Schemaオブジェクトを渡します(例: 数値文字列を number または bigint にパースする。 Standard Schema結果マッピングを参照)。

const user = await db.one(sql.rows(UserSchema)`SELECT id, name FROM users WHERE id = ${id}`);

プレーンJavaScriptからのSQLBraidの使用:

<T> 型引数なしで sql.rows を呼び出します。返される各行は、データベースのカラム名(または AS エイリアス)と完全に一致するキーを持つプレーンオブジェクトです。SQLBraidは user_id を userId に暗黙的に変換しません。キャメルケースのキーが必要な場合は、SQLで SELECT user_id AS "userId" と記述してください。

ストアドプロシージャ (db.call)

ストアドプロシージャを型なしの生のクエリとして扱う代わりに、sql.call は型付きの OUT パラメータ、複数の結果セット、および戻り値のための構造化されたAPIを提供します。

これは、refcursor OUT パラメータから行を読み取るPostgreSQLの例です(PostgreSQLではカーソルを読み取るために開いたトランザクションが必要なため、db.tx 内で実行されます)。

CREATE PROCEDURE list_active_users(OUT users refcursor) LANGUAGE plpgsql AS $$
BEGIN
  OPEN users FOR SELECT id, name FROM users WHERE active;
END $$;
import type { RoutineCallResult } from "sqlbraid";
import { postgresParameter, sql } from "@sqlbraid/postgres";

interface UserRow {
  id: string;
  name: string;
}

const result = await db.tx(async (tx) =>
  tx.call(
    sql.call<RoutineCallResult<{}, [UserRow]>>`
      CALL list_active_users(${sql.out("users", postgresParameter.refcursor())})
    `,
  ),
);

console.log(result.resultSets[0].rows); // UserRow[]: [{ id: "1", name: "Ada" }]

RoutineCallResult<Output, Sets, ReturnValue> は、スカラー OUT オブジェクト型、各結果セットの行型のタプル、および戻り値の型を宣言します。sql.rows と同様に、Standard Schemaを渡して各チャネルを検証および変換できます。

import { postgresParameter, sql } from "@sqlbraid/postgres";

const result = await db.tx(async (tx) =>
  tx.call(
    sql.call({ resultSets: [UserSchema] as const })`
      CALL list_active_users(${sql.out("users", postgresParameter.refcursor())})
    `,
  ),
);

ドライバ固有のサポートの詳細については、Routine calls および Support matrix を参照してください。

他のライブラリとの比較

記述するSQL動的SQL処理結果型サポートするデータベース
PrismaPrismaスキーマ & クライアントAPI; 生SQLには$queryRawクライアントAPI(undefinedフィルタをスキップ)、または生SQLでのPrisma.sql / Prisma.joinスキーマから生成; 生クエリの場合は手動型付けPostgreSQL, MySQL, SQLite, SQL Server, CockroachDB, MongoDB
DrizzleTypeScriptスキーマ & クエリAPI; 生SQL部分にはsqlテンプレートクエリAPI(and(...)がundefinedをスキップ)DBインターフェースから推論PostgreSQL, MySQL, SQLite, SingleStore, Gel
Kysely型安全なクエリビルダー; 生SQL部分にはsqlテンプレートビルダーメソッドチェーン(例: 条件付き.where())DBインターフェースから推論PostgreSQL, MySQL, SQLite, SQL Server
postgres.js / slonikタグ付きテンプレート内の生SQLJSでのフラグメント合成(ネストされたsql、sql.fragment、sql.join)あなたが宣言PostgreSQL
pgtyped:nameパラメータ付きの生SQLファイル内蔵なし(SQLに:name IS NULL OR ...と記述)ライブDBに対するビルド時に生成PostgreSQL
sqlc (TypeScriptプラグイン)生SQLファイル内蔵なし(SQLにIS NULL OR ...と記述)SQLとスキーマファイルから生成PostgreSQL (pg, postgres), MySQL (mysql2), SQLite (sqlite3)
SQLBraidタグ付きテンプレート内の生SQLSQL内コメントディレクティブ(/*@braid if ...*/)あなたが宣言 & Standard Schemaで検証; オプションでテーブルコード生成PostgreSQL, MySQL, MariaDB, SQLite, Oracle, SQL Server

SQLBraidが適している場合: SQLファーストでクエリを設計するチーム、多くのオプション検索フィルタや条件付き結合を持つクエリ、または複数のデータベースエンジン間で共通の実行およびストアドプロシージャAPIを必要とするプロジェクト。

SQLBraidが行わないこと:

  • ORMではない: 自動的なリレーションロードやアイデンティティマップはありません。
  • 任意のSELECT型推論なし: 返り値の行型を宣言するか、Standard Schemaを渡します。オプションで@sqlbraid/codegenを使用してテーブルモデルを生成できます。
  • SQLの書き換えや機能エミュレーションなし: 基盤となるドライバがサポートしない機能(クエリキャンセルなど)を呼び出すと、偽装するのではなく明示的なエラーがスローされます。
  • 組み込みコネクションプール実装なし: ドライバのプールを渡し、SQLBraidは操作ごとまたはトランザクションごとに接続をリースします。

コア実行機能

  • トランザクション (db.tx): セーブポイント、分離レベル、読み取り専用モード
  • ピン留めセッション (db.session): トランザクションを開始せずに複数のクエリ間で単一の接続を保持
  • ストリーミング (db.stream): 自動カーソルクリーンアップにより、大きな結果セットを段階的に消費
  • プリペアドクエリ & バルクDML: db.prepare(...) および db.bulk(...)
  • ストアドプロシージャ (db.call): 型付き OUT パラメータ、複数の結果セット、戻り値
  • 制御 & 可観測性: AbortSignal によるクエリキャンセル、ロギングのための実行オブザーバー、およびオプションのOpenTelemetryスパンとメトリクス

データベース、ドライバ、およびランタイム間の正確なサポートについては、Support matrix を確認してください。

サポートされているドライバ

sqlbraid、対応する方言またはアダプタパッケージ、およびドライバパッケージをインストールします。組み込みドライバとCloudflare D1バインディングはドライバパッケージを必要としません。

データベースまたはアダプタ必須パッケージ
PostgreSQL@sqlbraid/postgres
MySQL@sqlbraid/mysql
MariaDB@sqlbraid/mariadb
SQLite@sqlbraid/sqlite
Oracle@sqlbraid/oracle
SQL Server@sqlbraid/mssql
Bun.SQL@sqlbraid/bun-sql

sqlbraid は @sqlbraid/core、@sqlbraid/runtime、および @sqlbraid/template のみに依存し、方言やアダプタのピアはありません。使用する各パッケージを明示的にインストールしてください。パッケージルートから sql と方言ヒントをインポートし、アダプタパスからファクトリをインポートします。

sqlbraid バージョン2.0.0では、ドライバおよび方言ファサードサブパスが削除されました。各ファーストパーティパッケージは独自のバージョンラインを維持します。

Official は、正確なドライババージョンとランタイムを実際のデータベースに対してCIテストしていることを意味します。Compatible は、アダプタがピン留めされた認定ターゲットなしで同じAPI要件に従っていることを意味します。

データベースドライバインポートパスサポート
PostgreSQLpg@sqlbraid/postgres/pgOfficial
PostgreSQLpostgres (postgres.js)@sqlbraid/postgres/postgres-jsCompatible
PostgreSQL@neondatabase/serverless (WebSocket)@sqlbraid/postgres/neonCompatible
PostgreSQL@neondatabase/serverless (HTTP)@sqlbraid/postgres/neon-httpCompatible
PostgreSQL@electric-sql/pglite@sqlbraid/postgres/pgliteCompatible
MySQLmysql2@sqlbraid/mysql/mysql2Official
MariaDBmariadb@sqlbraid/mariadb/mariadbOfficial
SQLitenode:sqlite@sqlbraid/sqlite/node-sqliteOfficial
SQLitebetter-sqlite3@sqlbraid/sqlite/better-sqlite3Compatible
SQLite@libsql/client (libSQL)@sqlbraid/sqlite/libsqlCompatible
SQLite@sqlite.org/sqlite-wasm (ブラウザ)@sqlbraid/sqlite/wasmOfficial
SQLiteCloudflare D1 バインディング@sqlbraid/sqlite/d1Compatible
Oracleoracledb (Thin)@sqlbraid/oracle/oracledbOfficial
SQL Servertedious@sqlbraid/mssql/tediousOfficial
PostgreSQL, MySQL, MariaDB, SQLiteBun.SQL (Bunに組み込み)@sqlbraid/bun-sqlOfficial

オプションツール

以下のすべてのパッケージはオプションであり、ベースランタイムの依存関係ではありません(外部ランタイム依存関係をゼロ追加します)。完全なリストについては、Package map を参照してください。

  • コード生成 (@sqlbraid/codegen, @sqlbraid/cli): データベーススキーマを検査して Row、Insert、Update 型を生成します。CI検証用の codegen --check もあります(Codegen configurationを参照)。
  • エディタサポート (@sqlbraid/language-server、VS Code拡張機能): タグ付きテンプレート内のSQLに対する診断、オートコンプリート、ホバー情報。
  • Viteプラグイン (@sqlbraid/vite): ビルド時にディレクティブ式コンパイラを実行します(Vite統合を参照)。
  • Webpack、Rspack & Next.js (@sqlbraid/webpack): WebpackとNext.js 16のTurbopackの両方をサポートするディレクティブコンパイラローダー(Next.js例を参照)。
  • マイグレーション (@sqlbraid/migrate, @sqlbraid/cli): バージョン管理されたプレーンSQLマイグレーションファイルと、起動時の明示的なスキーマドリフトチェックを実行します。
  • エージェントスキル (skills/sqlbraid): AIコーディングエージェントにidiomaticなSQLBraidクエリの書き方を教えるポータブルな指示。

例

すべてのサンプルプロジェクトは、パックされたリリースtarballに対してCIで統合テストされています。

例説明
nextjs-webpackNext.js App Router、Webpack/Turbopackローダー設定、動的フィルタ、トランザクション
bookstore-apiPostgreSQL上のHono REST API: マイグレーション、Standard Schemaマッピング、注文トランザクション、ストリーミング
sqlite-quickstart組み込み node:sqlite とディレクティブコンパイラを使用した最小限の設定
postgresPostgreSQL直接クライアントとコネクションプール使用法
mysqlMySQLコネクションプール使用法
codegenメタデータスナップショットからの Row、Insert、Update 型の生成
opentelemetry-slow-queryOpenTelemetryオブザーバーを使用したクエリ स्पैनと実行時間メトリクスの収集

要件とステータス

  • ランタイム要件: ランタイムパッケージ(sqlbraid およびドライバアダプタ)はNode.js 16.20.2以降をサポートします(各ドライバ固有の最小Nodeバージョンに依存します。node:sqlite およびCLI/ビルドツールはNode.js 22.18+が必要です)。アクティブなLTSリリースを推奨します。
  • 現在のステータス: sqlbraid および @sqlbraid/* 2.x。公開APIはセマンティックバージョニング(SemVer)に従います。 リリースノート、現在の制限事項、および ロードマップ を参照してください。

開発とAI利用

SQLBraidのアーキテクチャとAPIは、JavaとMyBatisでの私の経験に基づいて設計しました。すべてのコードコミットは、主にCodexを使用したコーディングエージェントによって生成され、Claudeは実装の詳細、エッジケース、セキュリティ問題の独立したレビューに使用されました。私は結果をレビューし、最終的な設計上の決定を下しました。

SQLBraidにはAIランタイム依存関係はありません。

貢献とライセンス

Issueとプルリクエストを歓迎します(CONTRIBUTING.md および SECURITY.md を参照)。Apache-2.0 ライセンス。