mcpField
P
prisma-driver-adapter-implementation

prisma-driver-adapter-implementation

github.com/prisma開発76383.2k 回インストール1 日前に更新
リポジトリ

prisma-driver-adapter-implementation について

インストール済みの正確な `@prisma/driver-adapter-utils` バージョンに合わせて、Prisma SQL ドライバーアダプターを実装する開発者向けガイドです。トランザクションのライフサイクル、値とエラーのマッピング、リソース所有権、シャドウデータベースの分離を扱います。

主な機能

  1. 01アダプターファクトリーと接続の契約
  2. 02クエリー値とメタデータのマッピング
  3. 03トランザクションとセーブポイントの管理
  4. 04データベースエラーのマッピング
  5. 05プール所有権とシャドウデータベースの規則
  6. 06実装の検証チェックリスト

活用シーン

  1. 01Prisma SQL ドライバーアダプターの実装
  2. 02ネストしたトランザクションの追加
  3. 03アダプターエラーや接続リークの調査
  4. 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つ使用するインターリーブとリークを防ぎます
CRITICALcommit/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 テストも実行する

ソースの参照先

コメント