prisma-driver-adapter-implementation について
インストール済みの正確な `@prisma/driver-adapter-utils` バージョンに合わせて、Prisma SQL ドライバーアダプターを実装する開発者向けガイドです。トランザクションのライフサイクル、値とエラーのマッピング、リソース所有権、シャドウデータベースの分離を扱います。
主な機能
- 01アダプターファクトリーと接続の契約
- 02クエリー値とメタデータのマッピング
- 03トランザクションとセーブポイントの管理
- 04データベースエラーのマッピング
- 05プール所有権とシャドウデータベースの規則
- 06実装の検証チェックリスト
活用シーン
- 01Prisma SQL ドライバーアダプターの実装
- 02ネストしたトランザクションの追加
- 03アダプターエラーや接続リークの調査
- 04シャドウデータベース動作のテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ prisma/ skills --skill prisma-driver-adapter-implementationインストール後、Claude Code で/prisma-driver-adapter-implementationを入力して読み込まれたか確認します。
詳細
Prisma SQL ドライバーアダプターの実装
このガイドは、対象となる Prisma リリースにインストールされているものと完全に同じバージョンの @prisma/driver-adapter-utils と併せて使用してください。ドライバーアダプターはプロトコルの境界です。型互換性のあるコードでも、値を破損させたり、接続をリークさせたり、トランザクションを壊したりする可能性があります。
適用するケース
SqlDriverAdapterFactory、SqlMigrationAwareDriverAdapterFactory、SqlDriverAdapter、またはTransactionを実装する場合- ネストしたトランザクションやセーブポイントのサポートを追加する場合
- ドライバーの値、カラムメタデータ、バインド引数、またはデータベースエラーをマッピングする場合
P2039、トランザクションリーク、シャドウデータベースの失敗、またはアダプター固有のクエリ動作をデバッグする場合
コントラクトの概要
interface SqlDriverAdapterFactory extends AdapterInfo {
connect(): Promise<SqlDriverAdapter>
}
interface SqlMigrationAwareDriverAdapterFactory extends SqlDriverAdapterFactory {
connectToShadowDb(): Promise<SqlDriverAdapter>
}
interface SqlDriverAdapter extends AdapterInfo {
queryRaw(query: SqlQuery): Promise<SqlResultSet>
executeRaw(query: SqlQuery): Promise<number>
executeScript(script: string): Promise<void>
startTransaction(isolationLevel?: IsolationLevel): Promise<Transaction>
getConnectionInfo?(): ConnectionInfo
dispose(): Promise<void>
}
interface Transaction extends AdapterInfo {
readonly options: { usePhantomQuery: boolean }
queryRaw(query: SqlQuery): Promise<SqlResultSet>
executeRaw(query: SqlQuery): Promise<number>
commit(): Promise<void>
rollback(): Promise<void>
createSavepoint?(name: string): Promise<void>
rollbackToSavepoint?(name: string): Promise<void>
releaseSavepoint?(name: string): Promise<void>
}
現在の IsolationLevel には READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SNAPSHOT、SERIALIZABLE が含まれます。具体的なデータベースがサポートしている値を検証してください。
優先ルール
| 優先度 | ルール | 影響 |
|---|---|---|
| CRITICAL | トランザクションごとに専用の接続を1つ使用する | インターリーブとリークを防ぎます |
| CRITICAL | commit/rollback はライフサイクルのクリーンアップフックです | COMMIT/ROLLBACK の重複を防ぎます |
| CRITICAL | セーブポイントはアダプター全体の深度ではなく、Transaction に保持する | ネストしたスコープを接続単位にできます |
| CRITICAL | 元のデータベースエラーコード/メッセージを保持する | 有用な P2039 フォールバックを可能にします |
| HIGH | 引数と結果メタデータを正確にマッピングする | 値が気付かれずに破損するのを防ぎます |
| HIGH | シャドウデータベースを分離し、必ずクリーンアップする | Migrate を安全に実行できます |
| HIGH | アダプターが所有するリソースのみを破棄する | 呼び出し元が所有するプールの停止を防ぎます |
クエリの実装
SqlQuery には sql、args、および並列配列の argTypes が含まれます。各引数は値と ArgType の両方を使ってマッピングしてください。型情報や引数の個数に関する情報を破棄しないでください。カラムの順序が安定するよう、ドライバーの配列/タプル行モードで実行してください。
class ExampleQueryable {
readonly provider = 'postgres' as const
readonly adapterName = '@acme/adapter-example'
constructor(protected readonly connection: DriverConnection) {}
async queryRaw(query: SqlQuery): Promise<SqlResultSet> {
try {
const result = await this.connection.query({
text: query.sql,
values: query.args.map((value, index) =>
mapArg(value, query.argTypes[index]),
),
rowMode: 'array',
})
return {
columnNames: result.fields.map((field) => field.name),
columnTypes: result.fields.map(mapColumnType),
rows: result.rows,
}
} catch (error) {
throwAdapterError(error)
}
}
async executeRaw(query: SqlQuery): Promise<number> {
try {
const result = await this.connection.execute(
query.sql,
query.args.map((value, index) => mapArg(value, query.argTypes[index])),
)
return result.rowsAffected ?? 0
} catch (error) {
throwAdapterError(error)
}
}
}
結果のマッピング
columnNames、columnTypes、rows は、長さと順序を一致させて返してください。ドライバーのメタデータを ColumnTypeEnum に慎重にマッピングします。
- 符号付き整数の幅を
Int32/Int64に対応付けます。JavaScript の数値で切り捨てられないよう、64 ビット値を保持してください - 小数/数値は、Prisma が想定する表現を用いて
Numericに対応付けます - バイナリは
Uint8Array/Bytesに対応付けます - 日付のみ、時刻のみ、タイムスタンプはそれぞれ
Date、Time、DateTimeに対応付けます - UUID、JSON、列挙型、配列、プロバイダー固有の不明な値は、それぞれの明示的な型に対応付けます
- サポートされていないネイティブ型は
DriverAdapterError({ kind: 'UnsupportedNativeDataType', type })に対応付けます
null、空配列、配列要素の型、大きな整数、小数、バイト配列、JSON、日付、ユーザー定義型/不明なネイティブ型をテストしてください。
スクリプトの実行
executeScript は、プロバイダーが想定する方法でマイグレーションスクリプトを実行する必要があります。ドライバーのネイティブな複数ステートメント/スクリプト実行機能、または適切な SQL パーサーを使用してください。単純に ; で分割すると、関数、トリガー、引用符で囲まれた文字列、方言固有のブロックが壊れます。
トランザクションプロトコル
startTransaction は専用の接続を1つ取得し、データベーストランザクションを開始して、要求された分離レベルを適用したうえで、同じ接続にバインドされた Transaction を返す必要があります。セットアップに失敗した場合は、直ちに接続を解放してください。
async startTransaction(level?: IsolationLevel): Promise<Transaction> {
const connection = await this.pool.acquire()
try {
const tx = new ExampleTransaction(connection, () => connection.release())
await tx.executeRaw({ sql: 'BEGIN', args: [], argTypes: [] })
if (level) {
await tx.executeRaw({
sql: `SET TRANSACTION ISOLATION LEVEL ${validateLevel(level)}`,
args: [],
argTypes: [],
})
}
return tx
} catch (error) {
connection.release(error)
throwAdapterError(error)
}
}
コミットとロールバック
Prisma は executeRaw を通じて SQL の COMMIT/ROLLBACK を調整します。トランザクションオブジェクトの commit() メソッドと rollback() メソッドはライフサイクルフックです。リスナーを切り離し、専用の接続を一度だけ解放してください。2回目の SQL コミット/ロールバックを実行してはいけません。
class ExampleTransaction extends ExampleQueryable implements Transaction {
readonly options = { usePhantomQuery: false }
#closed = false
constructor(connection: DriverConnection, private readonly release: () => void) {
super(connection)
}
async commit() { this.finish() }
async rollback() { this.finish() }
private finish() {
if (this.#closed) return
this.#closed = true
this.release()
}
async createSavepoint(name: string) {
await this.control(`SAVEPOINT ${safeSavepoint(name)}`)
}
async rollbackToSavepoint(name: string) {
await this.control(`ROLLBACK TO SAVEPOINT ${safeSavepoint(name)}`)
}
async releaseSavepoint(name: string) {
await this.control(`RELEASE SAVEPOINT ${safeSavepoint(name)}`)
}
private async control(sql: string) {
await this.executeRaw({ sql, args: [], argTypes: [] })
}
}
オプションのセーブポイントメソッドは、プロバイダーがサポートしている場合にのみ実装してください。セーブポイント識別子を検証し、適切に引用してください。セーブポイントが意図的に何もしないプロバイダーについては、その制限を文書化してテストしてください。
共有アダプターにトランザクションの深度を保持してはいけません。並列トランザクションではアダプター全体の深度が正しく機能しません。ネスト状態は、返されたトランザクションの接続と Prisma のセーブポイント呼び出しに保持してください。
エラーのマッピング
認識されたドライバーの失敗は DriverAdapterError でラップします。既知の条件は、制約違反、認証/到達可能性、テーブル/列/データベースの欠落、タイムアウト、トランザクションのクローズ、無効な入力、値の範囲、書き込みの競合などの MappedError 種別にマッピングします。
データベースエラーでは、プロバイダー固有の未加工のバリアントにフォールバックする場合でも、originalCode と originalMessage を保持します。
import {
DriverAdapterError,
type Error as DriverAdapterErrorObject,
type MappedError,
} from '@prisma/driver-adapter-utils'
function convertDriverError(error: DatabaseError): DriverAdapterErrorObject {
return {
originalCode: String(error.code),
originalMessage: error.message,
...mapKnownOrRaw(error),
}
}
function mapKnownOrRaw(error: DatabaseError): MappedError {
if (error.code === '23505') {
return { kind: 'UniqueConstraintViolation', constraint: parsedConstraint(error) }
}
return {
kind: 'postgres',
code: String(error.code ?? 'N/A'),
severity: error.severity ?? 'N/A',
message: error.message,
detail: error.detail,
column: error.column,
hint: error.hint,
}
}
function throwAdapterError(error: unknown): never {
if (!isDatabaseError(error)) throw error
throw new DriverAdapterError(convertDriverError(error))
}
Prisma は、マッピングされていないドライバーエラーが P2039 になるとき、保持された元の詳細を使用します。不明な例外をすべて捏造した GenericJs ID に置き換えないでください。プログラミング上のバグが見える状態を保つため、ドライバー由来ではない真に予期しないエラーは再スローしてください。
ファクトリー、所有権、シャドウデータベース
connect()は、新しく使用可能なアダプター接続/プールのラッパーを返します。- ファクトリーがプールを作成したかどうかを追跡します。
dispose()は、所有するプールをクローズし、所有権を引き継ぐ明示的なオプションが指定されていない限り、呼び出し元が所有するプールからはリスナーのみをデタッチします。 connectToShadowDb()で分離されたシャドウデータベースを作成して接続でき、破棄時/失敗時のクリーンアップで削除できる場合にのみ、SqlMigrationAwareDriverAdapterFactoryを実装します。- シャドウアダプターをプライマリデータベースに接続してはいけません。生成する識別子は引用符で囲み、暗号学的に一意な名前を使用します。
getConnectionInfo()は、該当する場合にschemaName、maxBindValues、supportsRelationJoinsを正確に報告する必要があります。
検証チェックリスト
- 対象となる正確な
@prisma/driver-adapter-utilsバージョンに対して型チェックする -
queryRawが列の順序、型、NULL、精度を保持する -
executeRawが影響を受けた行数を正しく報告する -
executeScriptがプロバイダー固有の複数ステートメント構文を処理する - 並行するインタラクティブトランザクションが、それぞれ異なる専用接続を使用する
- 成功時はコミットして一度だけ解放し、失敗時はロールバックして一度だけ解放する
- ネストされたトランザクションのテストで、セーブポイントの作成/ロールバック/解放のフックを検証する
- サポートされていない分離レベルが
InvalidIsolationLevelとして失敗する - 既知の制約が構造化エラーにマッピングされる
- マッピングされていないデータベースエラーが元のコード/メッセージを保持し、有用な
P2039として表面化する - 内部プールと外部プールの両方について、破棄時の所有権をテストする
- シャドウデータベースの作成、使用、失敗時のクリーンアップ、破棄が分離されている
- アダプターの単体テストだけでなく、Prisma Client の統合テスト/E2E テストも実行する