👽
drizzle-llm ~自然言語で指示できる、ビルド時SQL生成~
飲み会で「もうSQLはLLMに書いてもらう方が良さそうですね〜ガハハ」という会話があったので、drizzle-orm向けの仕組みを作ってみました。
できること
自然言語でクエリロジックを書くと、LLMがSQLを生成してくれます。
ビルド時に生成する仕組みなので、一度生成してしまえば実行ごとにお金がかかることはありません。
また、クエリ自体はSQLファイルで保存されるため必要に応じて修正できますし、型安全性をある程度(drizzleが)保ってくれます。
(Golangのsqlcをイメージしています)
使い方
$ npm install drizzle-llm
LLMの設定ファイルを追加
// drizzle-llm.config.ts
export default {
provider: {
type: 'openai',
apiKey: process.env.OPENAI_API_KEY,
},
paths: {
schema: './src/schema',
queries: ['./src/**/*.ts'],
}
};
以下のように、自然言語でクエリを書き
// src/user.ts
import { llm } from "drizzle-llm";
import type { Sqlite } from "@/types";
import type { User } from "./types";
export async function findName(db: Sqlite, id: string): Promise<string | undefined> {
return await db.get<string>(llm`Find the user name by id ${id}`);
}
export async function list(db: Sqlite): Promise<User[]> {
return await db.all<User>(llm`ユーザーを全件取得する`);
}
export async function getAverageOfAge(db: Sqlite): Promise<number> {
return await db.get<number>(llm`ユーザーの平均年齢を取得する`).then((value: number) => value ?? -1);
}
ビルドを実行
$ npx drizzle-llm build
SQLファイル(.sql)と中間処理ファイル(.query.ts)が生成されます。
-- 8c381e8b6af5a90ebcf8c72a27bd3a49
-- Find the user name by id ${0}
SELECT name FROM user WHERE id = $1
-- 5eed765f7bd9b64c48e08823ad321f38
-- ユーザーを全件取得する
SELECT id, name, age FROM user
-- 1cc99f0e4ef8e0aa88d7ed06d7f43bd1
-- ユーザーの平均年齢を取得する
SELECT AVG(age) FROM user
実行時には生成されたSQLが使用されます。
ちょっと複雑な指示でも大丈夫です
複雑なクエリ1
export async function getInventoryAlerts(db: Sqlite): Promise<Array<{
productId: string;
productName: string;
currentStock: number;
salesLastMonth: number;
estimatedDaysLeft: number;
}>> {
return await db.all<{
productId: string;
productName: string;
currentStock: number;
salesLastMonth: number;
estimatedDaysLeft: number;
}>(llm`Generate inventory alerts for products that might run out soon based on current stock and sales velocity from last 30 days`);
}
SELECT
p.id AS "productId",
p.name AS "productName",
p.stock AS "currentStock",
COALESCE(SUM(s.quantity), 0) AS "salesLastMonth",
CASE
WHEN COALESCE(SUM(s.quantity), 0) > 0
THEN ROUND((p.stock * 30.0) / SUM(s.quantity))
ELSE NULL
END AS "estimatedDaysLeft"
FROM
product p
LEFT JOIN
sales s ON p.id = s.productId AND s.saleDate >= datetime('now', '-30 days') AND s.status != 'cancelled'
WHERE
p.is_active = 'true'
GROUP BY
p.id, p.name, p.stock
HAVING
p.stock < 20 OR (COALESCE(SUM(s.quantity), 0) > 0 AND (p.stock * 30.0) / SUM(s.quantity) < 15)
ORDER BY
"estimatedDaysLeft" ASC NULLS LAST
複雑なクエリ2
export async function getMonthlySalesGrowth(db: Sqlite, year: number): Promise<Array<{
month: string;
revenue: number;
previousMonthRevenue: number;
growthRate: number;
cumulativeRevenue: number;
}>> {
return await db.all<{
month: string;
revenue: number;
previousMonthRevenue: number;
growthRate: number;
cumulativeRevenue: number;
}>(llm`${year}年の月別売上を取得し、前月比の成長率と年累計売上も一緒に表示`);
}
WITH monthly_revenue AS (
SELECT
strftime('%Y-%m', sale_date) as month,
SUM(total_amount) as revenue
FROM sales
WHERE strftime('%Y', sale_date) = $1
GROUP BY strftime('%Y-%m', sale_date)
),
with_previous AS (
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) as previous_month_revenue
FROM monthly_revenue
),
with_cumulative AS (
SELECT
month,
revenue,
previous_month_revenue,
CASE
WHEN previous_month_revenue IS NULL OR previous_month_revenue = 0 THEN NULL
ELSE (revenue - previous_month_revenue) / previous_month_revenue * 100
END as growth_rate,
SUM(revenue) OVER (ORDER BY month) as cumulative_revenue
FROM with_previous
)
SELECT
month,
revenue,
previous_month_revenue,
growth_rate,
cumulative_revenue
FROM with_cumulative
ORDER BY month
やっていること
ビルド時
- ビルド時にAST解析でタグ付きテンプレートリテラル(
llm
)を走査 - LLMによるクエリ生成
- スキーマ情報(テーブル名、カラム名)を必要に応じて提供
- プレースホルダや返り値の型[1]、メソッド(all/get)、ユーザーの自然言語クエリをコンテキストとして追加
- SQLファイル(.sql)と紐づけ用の中間処理ファイル(.query.ts)を生成
実行時
- テンプレートリテラル(
llm
)が実行される際、生成済みのSQLファイルからハッシュ値をもとにクエリを検索[2] - パラメータをバインドし、Drizzle ORMを通じてデータベースに実行
- 型安全な結果を返却
仕組みそのものだけであれば、実はdrizzle-ormに乗っかる必要はなかったりします。
TODO
- 入力値によるハッシュ値を検証し、変更が必要ないクエリはスルーする
- 現在もハッシュは生成しているものの、キャッシュがうまく機能していない
- SQLファイルとしての整合性担保
- スキーマが存在するなら、そちらとの突き合わせ
- 自然言語→SQLへのコードジャンプ
さいごに
Claude Code (Maxプラン)が約1日でゴリゴリと作り上げてくれました。自分では面倒くさくてやらないことも頑張ってくれるのは助かりますね。作業過程は
- 思いつく
- Claude(Web)と会話し、アイデアを検証
- Deep Researchで同種のソリューションがないか調査[3]
- 実装をClaude Codeに引き継ぐためドキュメント (CLAUDE.md)を作成
- 以降はVibe Coding
- 様子を見つつ指示出し
といった形で完全な自律とまではいきませんが、自分自身は別のことに時間を使えるのは良いです。
また、Text-To-SQLはGoogle も提唱していますが、自分の雑なプロンプトでもいい感じに動くのは面白かったです。
雑な思いつきはどんどん形にしていきましょう。
Discussion