プロジェクト構造
リポジトリ: drizzle-team/drizzle-orm 分析日: 2026-03-04
概要
pnpm ワークスペースと Turborepo を組み合わせたモノレポで、ORM コアパッケージ(drizzle-orm)を中心に CLI・バリデーション統合・シード・ESLint プラグインの 9 パッケージを管理する。注目すべきは、drizzle-orm が単一パッケージ内にコア層・4 つの方言コア・20 以上のドライバアダプターを格納する「ファットコアパッケージ」戦略を採用している点で、パッケージ分割の粒度と exports map の設計に独自のプラクティスが見られる。
背景にある原則
ファットコアに方言とドライバを内包し、サブパスエクスポートで境界を作る: 方言(pg-core, mysql-core, sqlite-core, singlestore-core)とドライバアダプター(node-postgres, better-sqlite3, d1 等 20+)を個別 npm パッケージに分割せず、単一パッケージの内部ディレクトリとして保持する。ユーザーは
drizzle-orm/node-postgresのようなサブパスインポートで使い分ける。パッケージ数の爆発を防ぎつつ、ツリーシェイキング可能な構造を維持するため。(drizzle-orm/package.jsonの exports、drizzle-orm/scripts/build.ts:10-44で全src/**/*.tsからエクスポートを自動生成)依存の方向を厳格に一方向にし、コアを無依存に保つ: コア(table.ts, column.ts, sql/)は外部依存を持たず、方言コアはコアのみに依存し、ドライバアダプターは方言コアとサードパーティドライバに依存する。この三層構造で依存の逆流を防止する。(
drizzle-orm/src/node-postgres/driver.tsが~/pg-core/db.tsを参照するが、pg-core は node-postgres を参照しない)ビルド成果物への直接リンクでパッケージ間依存を実現する:
workspace:./drizzle-orm/distやlink:../drizzle-orm/distのように dist ディレクトリを直接参照する。通常のworkspace:*がソースを参照するのに対し、ビルド済み成果物を参照することで、パッケージ消費者と同じ条件(exports map 解決、CJS/ESM 互換)での開発・テストを保証する。(package.json各所、drizzle-kit/package.json:86,drizzle-zod/package.json:74)Turborepo の依存グラフで全パッケージをコアのビルドに同期させる: 全パッケージの build タスクが
drizzle-orm#buildにdependsOnで接続され、コアのビルドなしに下流パッケージのビルドが走らない。(turbo.jsonでdrizzle-kit#build,drizzle-zod#build等が全てdrizzle-orm#buildを先行タスクとして指定)
実例と分析
三層アーキテクチャ: コア / 方言コア / ドライバアダプター
drizzle-orm パッケージ内部は明確な三層に分かれる:
- コア層(
src/直下: table.ts, column.ts, column-builder.ts, sql/, relations.ts, entity.ts 等): SQL 方言に依存しない抽象概念を定義。Table,Column,SQLなどの基底クラスがここにある。 - 方言コア層(
src/pg-core/,src/mysql-core/,src/sqlite-core/,src/singlestore-core/): 方言固有のテーブル・カラム・クエリビルダー・ダイアレクトを定義。コア層の Table/Column を継承して方言固有の機能を追加。 - ドライバアダプター層(
src/node-postgres/,src/better-sqlite3/,src/d1/等 20+ ディレクトリ): 各ドライバが方言コアのDialectとSessionを使い、具象接続を提供する。
各ドライバアダプターは統一された構造を持つ: driver.ts(接続構築と drizzle() ファクトリ関数)、session.ts(PreparedQuery と Session の具象実装)、migrator.ts(マイグレーション実行)、index.ts(公開 API の re-export)。
ファットパッケージの exports 自動生成
drizzle-orm/scripts/build.ts は src/**/*.ts を glob で列挙し、package.json の exports フィールドを動的に構築する。各エントリに対して ESM(.js / .d.ts)と CJS(.cjs / .d.cts)の両方のパスを生成する。この自動化により、20 以上のドライバサブパスを手動管理する負担を排除している。
// drizzle-orm/scripts/build.ts:10-44
const entries = await glob('src/**/*.ts');
pkg.exports = entries.reduce<Record<string, {...}>>(
(acc, rawEntry) => {
const entry = rawEntry.match(/src\/(.*)\.ts/)![1]!;
const exportsEntry = entry === 'index' ? '.' : './' + entry.replace(/\/index$/, '');
acc[exportsEntry] = {
import: { types: `./${entry}.d.ts`, default: `./${entry}.js` },
require: { types: `./${entry}.d.cts`, default: `./${entry}.cjs` },
types: `./${entry}.d.ts`,
default: `./${entry}.js`,
};
return acc;
},
{},
);CJS/ESM のインポートパス修正
ビルド後に scripts/fix-imports.ts が recast で AST を走査し、CJS ファイルの .js → .cjs、ESM ファイルのパスエイリアス ~/ → 相対パスの変換を行う。これにより、ソースコードではパスエイリアスで簡潔に書きつつ、出力は正確なモジュール解決が保証される。
// drizzle-orm/scripts/fix-imports.ts:8-15
function resolvePathAlias(importPath: string, file: string) {
if (importPath.startsWith("~/")) {
const relativePath = path.relative(
path.dirname(file),
path.resolve("dist.new", importPath.slice(2)),
);
importPath = relativePath.startsWith(".") ? relativePath : "./" + relativePath;
}
return importPath;
}workspace:dist パターンによるパッケージ間参照
通常のモノレポでは workspace:* でパッケージのソースを参照するが、drizzle-orm では workspace:./drizzle-orm/dist のように dist ディレクトリを直指定する。スキーマ統合パッケージ(drizzle-zod 等)は link:../drizzle-orm/dist を使う。
// package.json(ルート)
"drizzle-orm": "workspace:./drizzle-orm/dist"
// drizzle-kit/package.json
"drizzle-orm": "workspace:./drizzle-orm/dist"
// drizzle-zod/package.json
"drizzle-orm": "link:../drizzle-orm/dist"これにより開発時にもパッケージ消費者と同一の exports map 解決パスが再現され、「開発環境では動くがパブリッシュ後に壊れる」問題を防止する。
バリデーション統合パッケージの同構造パターン
drizzle-zod, drizzle-typebox, drizzle-valibot, drizzle-arktype の 4 パッケージは完全に同一のファイル構造を持つ:
src/
├── column.ts
├── column.types.ts
├── constants.ts
├── index.ts
├── schema.ts
├── schema.types.internal.ts
├── schema.types.ts
└── utils.tspackage.json のスクリプト構成(build, test, test:types, pack, publish)と exports map も同一パターン。異なるのは peerDependencies(zod, @sinclair/typebox, valibot, arktype)のみ。バリデーションライブラリごとにパッケージを分離することで、ユーザーは必要なバリデーションライブラリだけを依存に追加できる。
統合テストの共通化パターン
integration-tests/tests/pg/ では pg-common.ts に DB 方言の共通テスト関数群を定義し、各ドライバのテストファイル(node-postgres.test.ts, pglite.test.ts 等)がそれをインポートして実行する。Docker でデータベースを起動し、ドライバ固有のセットアップのみをテストファイルに記述する。
// integration-tests/tests/pg/node-postgres.test.ts:11
import { createDockerDB, tests, usersMigratorTable, usersTable } from "./pg-common";entityKind による instanceof 代替とカスタム ESLint ルール
entity.ts で定義された entityKind Symbol を使い、is() 関数が instanceof の代わりにプロトタイプチェーンを辿る型判定を行う。eslint/eslint-plugin-drizzle-internal が require-entity-kind ルールで全クラスに static readonly [entityKind] の宣言を強制する。
// drizzle-orm/src/entity.ts:1
export const entityKind = Symbol.for("drizzle:entityKind");
// drizzle-orm/src/entity.ts:12-42
export function is<T extends DrizzleEntityClass<any>>(value: any, type: T): value is InstanceType<T> {
// ...
let cls = Object.getPrototypeOf(value).constructor;
if (cls) {
while (cls) {
if (entityKind in cls && cls[entityKind] === type[entityKind]) {
return true;
}
cls = Object.getPrototypeOf(cls);
}
}
return false;
}ESLint ルールがクラス宣言を検査し、entityKind プロパティがなければエラーを報告する:
// eslint/eslint-plugin-drizzle-internal/index.js:7
'require-entity-kind': ESLintUtils.RuleCreator(...)({
// ...
messages: {
missingEntityKind:
"Class '{{name}}' doesn't have a static readonly [entityKind] property...",
},
})パターンカタログ
Abstract Factory (生成)
- 解決する問題: 同一インターフェースで異なるデータベース接続を構築する
- 適用条件: 複数のドライバが同じ方言コアの Dialect/Session を共有する場合
- コード例: 各ドライバの
drizzle()ファクトリ関数 (node-postgres/driver.ts:91,neon-http/driver.ts:171,better-sqlite3/driver.ts:68) - 注意点: 各ファクトリ関数は内部で
construct()ヘルパーを呼ぶ二段構成で、オーバーロード解決を construct に委譲している
Template Method (振る舞い)
- 解決する問題: 方言間で共通のクエリ構築フローを維持しつつ、SQL 生成の詳細を方言ごとに変える
- 適用条件: 複数の SQL 方言を単一フレームワークでサポートする場合
- コード例:
PgDialect/MySqlDialect/SQLiteSyncDialect/SingleStoreDialectが各自の dialect.ts で SQL 構築メソッドを実装 (pg-core/dialect.ts,mysql-core/dialect.ts,sqlite-core/dialect.ts) - 注意点: 方言コア間でのコード共有はなく、各 dialect.ts は独立して実装されている(1445/1169/926/834 行)
Adapter (構造)
- 解決する問題: サードパーティドライバの API 差異を方言コアの Session インターフェースに合わせる
- 適用条件: 同一 SQL 方言に対して複数のドライバライブラリが存在する場合
- コード例:
NodePgSession extends PgSession(node-postgres/session.ts),NeonHttpSession extends PgSession(neon-http/session.ts) - 注意点: 各ドライバの session.ts は PgPreparedQuery / PgSession を継承し、execute/all/values の具象実装のみを提供する
Good Patterns
サブパスエクスポートで仮想的なパッケージ境界を作る: 20 以上のドライバを個別 npm パッケージに分割せず、
drizzle-orm/node-postgres,drizzle-orm/pg-coreのようなサブパスで公開する。npm パッケージの管理負荷(バージョニング、リリース、依存解決)を単一パッケージに集約しつつ、消費者にはモジュールレベルの粒度を提供する。typescript// ユーザーコード import { drizzle } from "drizzle-orm/node-postgres"; import { pgTable, serial, text } from "drizzle-orm/pg-core";ビルド成果物リンクで「消費者と同じ条件」のモノレポ開発:
workspace:./drizzle-orm/distやlink:../drizzle-orm/distでビルド成果物を参照することで、exports map の解決パスが開発時とパブリッシュ後で一致する。json// drizzle-kit/package.json { "drizzle-orm": "workspace:./drizzle-orm/dist" }テンプレート化されたバリデーション統合パッケージ: 同一のファイル構成(column.ts, schema.ts, utils.ts 等 8 ファイル)を 4 つのバリデーション統合パッケージで再利用。新しいバリデーションライブラリの統合を追加する際、既存パッケージをコピーして対応箇所を書き換えるだけで済む。
カスタム ESLint ルールで設計規約を機械的に強制する:
require-entity-kindルールにより、全クラスがentityKindプロパティを持つことをビルド時に保証。人間の注意力に頼らず、規約違反をコンパイル前に検出する。
Anti-Patterns / 注意点
方言コア間のコード重複: pg-core (11,402行), mysql-core (8,612行), sqlite-core (7,296行), singlestore-core (8,042行) は各自が独立した dialect.ts, session.ts, query-builders/ を持ち、共通基底クラスからの差分実装ではなく、大部分がコピーベースで実装されている。方言間で共通のロジック(リレーショナルクエリ構築、エイリアス処理等)が各 dialect.ts に重複している。
Bad: 各方言の dialect.ts が独立して
buildRelationalQuery等の複雑なメソッドを個別実装pg-core/dialect.ts : 1445 行 mysql-core/dialect.ts : 1169 行 sqlite-core/dialect.ts : 926 行 singlestore-core/dialect.ts: 834 行Better: 共通ロジックを基底 Dialect クラスに抽出し、方言固有の差分のみをオーバーライドする(ただし SQL 方言の差異が大きい場合、過度な抽象化は可読性を損なう場合もあるため、トレードオフの判断が必要)
ファットパッケージによるインストールサイズの肥大化: 全ドライバ・全方言が単一パッケージに含まれるため、PostgreSQL しか使わないユーザーも MySQL/SQLite 関連のコードを含むパッケージをインストールすることになる。バンドルサイズにはツリーシェイキングで対応できるが、npm install 時のサイズには影響する。
導出ルール
[MUST]モノレポでパッケージ間の依存方向を単一方向に保ち、コアパッケージが下流パッケージを参照しない構造にする- 根拠: drizzle-orm のコア層(table.ts, column.ts, sql/)は方言コアやドライバを一切 import しない。pg-core は node-postgres を参照しない。この厳格な方向性により、コアの変更が局所的な影響に留まる (
drizzle-orm/src/全体の import 方向)
- 根拠: drizzle-orm のコア層(table.ts, column.ts, sql/)は方言コアやドライバを一切 import しない。pg-core は node-postgres を参照しない。この厳格な方向性により、コアの変更が局所的な影響に留まる (
[MUST]Turborepo 等のタスクランナーで、コアパッケージのビルドを全下流パッケージの先行タスクとして設定する- 根拠:
turbo.jsonで全パッケージの build がdrizzle-orm#buildに依存し、コアのビルドなしに下流パッケージがビルドされない保証を作っている (turbo.json:4-253)
- 根拠:
[SHOULD]サブパスエクスポートによりパッケージ分割を代替する(同一パッケージ内で論理的な境界を提供する)- 根拠: 20 以上のドライバアダプターを個別パッケージにせず、
drizzle-orm/<driver>形式のサブパスで公開。バージョン同期の負荷を排除しつつ、消費者には明確な API 境界を提供する (drizzle-orm/scripts/build.tsの exports 自動生成)
- 根拠: 20 以上のドライバアダプターを個別パッケージにせず、
[SHOULD]モノレポ内のパッケージ間参照はビルド成果物(dist ディレクトリ)を対象にし、ソースコード直参照を避ける- 根拠:
workspace:./drizzle-orm/distパターンにより、開発時もパブリッシュ後と同じ exports map 解決を再現し、「開発時だけ動く」問題を防止する (drizzle-kit/package.json:86,drizzle-zod/package.json:74)
- 根拠:
[SHOULD]設計規約をカスタム ESLint ルールで機械的に強制する(ドキュメントやコードレビューに頼らない)- 根拠:
eslint-plugin-drizzle-internalのrequire-entity-kindルールが全クラスのentityKind宣言を強制し、is()関数の正常動作を保証する (eslint/eslint-plugin-drizzle-internal/index.js:7)
- 根拠:
[SHOULD]統合テストの共通ロジックを共有モジュールに抽出し、ドライバ固有のセットアップのみを各テストファイルに残す- 根拠:
pg-common.tsが PostgreSQL 方言の全テストケースを定義し、node-postgres/pglite/neon 等の各テストファイルは DB 接続のセットアップだけを行う (integration-tests/tests/pg/pg-common.ts,integration-tests/tests/pg/node-postgres.test.ts:11)
- 根拠:
[AVOID]モノレポ内の同種パッケージ群(バリデーション統合等)で構造を個別に設計する — テンプレート化して統一する- 根拠: drizzle-zod/typebox/valibot/arktype が完全に同一のファイル構成(8 ファイル)を持ち、新規追加時のコストを最小化している
適用チェックリスト
- [ ] 単一パッケージ内に多数のサブモジュールがある場合、サブパスエクスポート(package.json
exportsフィールド)で論理的な境界を作っているか - [ ] exports map の生成を自動化しているか(手動管理はエントリ追加時のミスを招く)
- [ ] パッケージ間の依存方向が一方向になっているか(循環依存やコアから下流への参照がないか)
- [ ] Turborepo 等のタスクランナーで、依存パッケージのビルド順序を正しく設定しているか
- [ ] モノレポ内パッケージの相互参照で、ソースではなくビルド成果物を参照しているか(exports map 解決の整合性)
- [ ] CJS/ESM デュアルパブリッシュのインポートパス(拡張子)が正しいか — ビルド後の自動修正を導入しているか
- [ ] 同種のパッケージ群が統一されたファイル構成・スクリプト構成を持っているか
- [ ] 設計上の規約(型タグ、命名規則等)が ESLint カスタムルールやビルド時チェックで機械的に検証されているか
- [ ] 統合テストの共通ロジックが共有モジュールに抽出され、ドライバ固有の部分だけが各テストファイルに残っているか