kysely
Kyselyを使用したType-safeなTypeScript SQLクエリビルダーの開発ガイドラインです。オートコンプリートのサポートを活用しながら、型安全なSQLクエリを構築する際に参照します。
description の原文を見る
Guidelines for developing with Kysely, a type-safe TypeScript SQL query builder with autocompletion support
SKILL.md 本文
Kysely 開発ガイドライン
あなたは Kysely、TypeScript、SQL データベース設計の専門家であり、型安全性とクエリ最適化に重点を置いています。
コア原則
- Kysely は SQL の上に構築された薄い抽象化レイヤーで、SQL 愛好家によって SQL 愛好家のために設計されています
- テーブル、列、クエリ結果に対する完全な型安全性と自動補完
- 予測可能な 1:1 の SQL コンパイル - 書いたものがそのまま生成されます
- 魔法や隠れた動作はなし - 明示的で透明なクエリビルディング
- Node.js、Deno、Bun、Cloudflare Workers、ブラウザで動作
データベースインターフェース定義
データベーススキーマの定義
import { Generated, ColumnType, Selectable, Insertable, Updateable } from "kysely";
// テーブルインターフェースを定義
interface UserTable {
id: Generated<number>;
email: string;
name: string | null;
is_active: boolean;
created_at: Generated<Date>;
updated_at: ColumnType<Date, Date | undefined, Date>;
}
interface PostTable {
id: Generated<number>;
title: string;
content: string | null;
author_id: number;
published_at: Date | null;
created_at: Generated<Date>;
}
// データベースインターフェースを定義
interface Database {
users: UserTable;
posts: PostTable;
}
// 各テーブルのヘルパー型をエクスポート
export type User = Selectable<UserTable>;
export type NewUser = Insertable<UserTable>;
export type UserUpdate = Updateable<UserTable>;
export type Post = Selectable<PostTable>;
export type NewPost = Insertable<PostTable>;
export type PostUpdate = Updateable<PostTable>;
Generated vs ColumnType
Generated<T>- データベースで自動生成される列(オートインクリメント、デフォルト値)ColumnType<SelectType, InsertType, UpdateType>- 操作によって異なる型
データベース接続
PostgreSQL セットアップ
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
const db = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: process.env.DATABASE_URL,
}),
}),
});
export { db };
MySQL セットアップ
import { Kysely, MysqlDialect } from "kysely";
import { createPool } from "mysql2";
const db = new Kysely<Database>({
dialect: new MysqlDialect({
pool: createPool({
uri: process.env.DATABASE_URL,
}),
}),
});
SQLite セットアップ
import { Kysely, SqliteDialect } from "kysely";
import Database from "better-sqlite3";
const db = new Kysely<Database>({
dialect: new SqliteDialect({
database: new Database("database.db"),
}),
});
クエリパターン
SELECT クエリ
// テーブルのすべての列を選択
const users = await db.selectFrom("users").selectAll().execute();
// 特定の列を選択
const userEmails = await db
.selectFrom("users")
.select(["id", "email", "name"])
.execute();
// WHERE 条件付き
const activeUsers = await db
.selectFrom("users")
.selectAll()
.where("is_active", "=", true)
.execute();
// 複数の条件
const filteredUsers = await db
.selectFrom("users")
.selectAll()
.where("is_active", "=", true)
.where("email", "like", "%@example.com")
.execute();
// OR 条件
const users = await db
.selectFrom("users")
.selectAll()
.where((eb) =>
eb.or([
eb("name", "=", "John"),
eb("name", "=", "Jane"),
])
)
.execute();
列エイリアス
// Kysely は自動的にエイリアス型を推論
const result = await db
.selectFrom("users")
.select([
"id",
"email",
"name as userName", // エイリアスは正しく解析・型付けされます
])
.executeTakeFirst();
// result.userName は正しく型付けされます
JOIN
// 内部結合
const postsWithAuthors = await db
.selectFrom("posts")
.innerJoin("users", "users.id", "posts.author_id")
.select([
"posts.id",
"posts.title",
"users.name as authorName",
])
.execute();
// 左結合
const usersWithPosts = await db
.selectFrom("users")
.leftJoin("posts", "posts.author_id", "users.id")
.select([
"users.id",
"users.name",
"posts.title as postTitle",
])
.execute();
サブクエリ
// SELECT 内のサブクエリ
const usersWithPostCount = await db
.selectFrom("users")
.select([
"users.id",
"users.name",
(eb) =>
eb
.selectFrom("posts")
.select(eb.fn.count<number>("posts.id").as("count"))
.whereRef("posts.author_id", "=", "users.id")
.as("postCount"),
])
.execute();
// WHERE 内のサブクエリ
const usersWithPosts = await db
.selectFrom("users")
.selectAll()
.where("id", "in", (eb) =>
eb.selectFrom("posts").select("author_id").distinct()
)
.execute();
INSERT 操作
// 単一の INSERT
const result = await db
.insertInto("users")
.values({
email: "user@example.com",
name: "John Doe",
is_active: true,
})
.returning(["id", "email", "created_at"])
.executeTakeFirstOrThrow();
// 一括 INSERT
await db
.insertInto("users")
.values([
{ email: "user1@example.com", name: "User 1", is_active: true },
{ email: "user2@example.com", name: "User 2", is_active: true },
])
.execute();
// ON CONFLICT 付き INSERT (upsert)
await db
.insertInto("users")
.values({
email: "user@example.com",
name: "John",
is_active: true,
})
.onConflict((oc) =>
oc.column("email").doUpdateSet({
name: "John Updated",
updated_at: new Date(),
})
)
.execute();
UPDATE 操作
const result = await db
.updateTable("users")
.set({
name: "Jane Doe",
updated_at: new Date(),
})
.where("id", "=", 1)
.returning(["id", "name", "updated_at"])
.executeTakeFirst();
DELETE 操作
const result = await db
.deleteFrom("users")
.where("id", "=", 1)
.returning(["id", "email"])
.executeTakeFirst();
トランザクション
await db.transaction().execute(async (trx) => {
const user = await trx
.insertInto("users")
.values({
email: "user@example.com",
name: "User",
is_active: true,
})
.returning(["id"])
.executeTakeFirstOrThrow();
await trx
.insertInto("posts")
.values({
title: "First Post",
author_id: user.id,
})
.execute();
});
kysely-codegen による型生成
既存のデータベースから型を生成するには kysely-codegen を使用します:
# インストール
npm install -D kysely-codegen
# 型を生成 (DATABASE_URL 環境変数から読み込み)
npx kysely-codegen
# 出力ファイルを指定
npx kysely-codegen --out-file src/db/types.ts
データベーススキーマが変更されるたびに型を再生成してください。
プラグイン
CamelCase プラグイン
snake_case 列名を camelCase に変換:
import { Kysely, PostgresDialect, CamelCasePlugin } from "kysely";
const db = new Kysely<Database>({
dialect: new PostgresDialect({ pool }),
plugins: [new CamelCasePlugin()],
});
カスタムプラグイン
import { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs } from "kysely";
class LoggingPlugin implements KyselyPlugin {
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
console.log("Query:", args.node);
return args.node;
}
async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<unknown>> {
console.log("Result:", args.result);
return args.result;
}
}
高度なパターン
動的クエリビルディング
function findUsers(filters: {
email?: string;
isActive?: boolean;
name?: string;
}) {
let query = db.selectFrom("users").selectAll();
if (filters.email) {
query = query.where("email", "=", filters.email);
}
if (filters.isActive !== undefined) {
query = query.where("is_active", "=", filters.isActive);
}
if (filters.name) {
query = query.where("name", "like", `%${filters.name}%`);
}
return query.execute();
}
生 SQL
import { sql } from "kysely";
// SELECT 内の生 SQL 式
const result = await db
.selectFrom("users")
.select([
"id",
sql<string>`CONCAT(first_name, ' ', last_name)`.as("fullName"),
])
.execute();
// WHERE 内の生 SQL 式
const users = await db
.selectFrom("users")
.selectAll()
.where(sql`LOWER(email)`, "=", "user@example.com")
.execute();
共通テーブル式 (CTE)
const result = await db
.with("active_users", (db) =>
db.selectFrom("users").selectAll().where("is_active", "=", true)
)
.selectFrom("active_users")
.selectAll()
.execute();
ベストプラクティス
TypeScript 設定
tsconfig.json で strict モードを有効化:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"target": "ES2020"
}
}
最適な型推論のために TypeScript 5.4 以降を使用してください。
パフォーマンスのヒント
- 必要な列だけを選択 - 特定フィールドが必要な場合は
selectAll()を避ける - 適切なインデックスを使用 - WHERE と JOIN 列にはデータベースインデックスが必要
- バッチ操作を使用 - 複数レコードには一括 INSERT を使用
- コネクションプーリング - 本番環境では常にコネクションプールを使用
- プリペアドステートメント - Kysely は自動的にプリペアドステートメントを使用
エラーハンドリング
import { NoResultError } from "kysely";
try {
const user = await db
.selectFrom("users")
.selectAll()
.where("id", "=", 999)
.executeTakeFirstOrThrow();
} catch (error) {
if (error instanceof NoResultError) {
// 見つからないケースを処理
}
throw error;
}
クエリの再利用性
// 再利用可能なクエリパーツを作成
function withActiveUsers(db: Kysely<Database>) {
return db.selectFrom("users").where("is_active", "=", true);
}
// クエリで使用
const activeUsers = await withActiveUsers(db).selectAll().execute();
マイグレーション管理
Kysely はシンプルなマイグレーションシステムを提供します:
import { Migrator, FileMigrationProvider } from "kysely";
import path from "path";
import { promises as fs } from "fs";
const migrator = new Migrator({
db,
provider: new FileMigrationProvider({
fs,
path,
migrationFolder: path.join(__dirname, "migrations"),
}),
});
// マイグレーションを実行
await migrator.migrateToLatest();
マイグレーションファイルの例:
// migrations/001_create_users.ts
import { Kysely, sql } from "kysely";
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable("users")
.addColumn("id", "serial", (col) => col.primaryKey())
.addColumn("email", "varchar(255)", (col) => col.notNull().unique())
.addColumn("name", "varchar(255)")
.addColumn("is_active", "boolean", (col) => col.defaultTo(true))
.addColumn("created_at", "timestamp", (col) =>
col.defaultTo(sql`now()`).notNull()
)
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable("users").execute();
}
ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- mindrally
- リポジトリ
- mindrally/skills
- ライセンス
- Apache-2.0
- 最終更新
- 不明
Source: https://github.com/mindrally/skills / ライセンス: Apache-2.0
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。