prisma-driver-adapter-implementation

prisma-driver-adapter-implementation

Prisma v7 驅動介面卡實作的必要參考。在實作或修改介面卡、新增資料庫驅動程式,或接觸 SqlDriverAdapter/Transaction 介面時使用。包含無法從程式碼範例推斷的關鍵合約細節,包括交易生命週期協定、錯誤對映需求及驗證檢查清單。現有的實作無法取代此技能。

44星標
3分支
更新於 2026/7/14
SKILL.md
readonlyread-only
name
prisma-driver-adapter-implementation
description

Required reference for Prisma v7 driver adapter work. Use when implementing or modifying adapters, adding database drivers, or touching SqlDriverAdapter/Transaction interfaces. Contains critical contract details not inferable from code examples — including the transaction lifecycle protocol, error mapping requirements, and verification checklist. Existing implementations do not replace this skill.

Prisma 7 驅動介面卡實作指南

此技能提供實作任何資料庫的 Prisma ORM v7 驅動介面卡所需的一切。

架構概覽

┌─────────────────────────────────────────────────────────────────┐
│                         PrismaClient                            │
│                    (需要介面卡工廠)                              │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│            SqlMigrationAwareDriverAdapterFactory                │
│   ┌─────────────────────┐    ┌─────────────────────────────┐    │
│   │ connect()           │    │ connectToShadowDb()         │    │
│   │ → SqlDriverAdapter  │    │ → SqlDriverAdapter          │    │
│   └─────────────────────┘    └─────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                      SqlDriverAdapter                           │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│  │ queryRaw()   │ │ executeRaw() │ │ startTransaction()       │ │
│  │ → ResultSet  │ │ → number     │ │ → Transaction            │ │
│  └──────────────┘ └──────────────┘ └──────────────────────────┘ │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│  │executeScript │ │ dispose()    │ │ getConnectionInfo()      │ │
│  └──────────────┘ └──────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                        Transaction                              │
│  繼承 SqlQueryable + commit() + rollback() + options            │
│  (僅生命週期鉤子 — Prisma 透過 executeRaw 傳送 SQL)             │
└─────────────────────────────────────────────────────────────────┘

必要介面

@prisma/driver-adapter-utils 匯入:

import type {
  ColumnType,
  IsolationLevel,
  SqlDriverAdapter,
  SqlMigrationAwareDriverAdapterFactory,
  SqlQuery,
  SqlQueryable,
  SqlResultSet,
  Transaction,
  TransactionOptions,
  ArgType,
  ConnectionInfo,
  MappedError,
} from "@prisma/driver-adapter-utils";
import {
  ColumnTypeEnum,
  DriverAdapterError,
} from "@prisma/driver-adapter-utils";

介面定義

SqlQuery (queryRaw/executeRaw 的輸入)

type SqlQuery = {
  sql: string; // 帶有佔位符的參數化 SQL
  args: Array<unknown>; // 綁定的參數值
  argTypes: Array<ArgType>; // 每個引數的型別提示
};

type ArgType = {
  scalarType: ArgScalarType; // 'string' | 'int' | 'bigint' | 'float' | 'decimal' | 'boolean' | 'enum' | 'uuid' | 'json' | 'datetime' | 'bytes' | 'unknown'
  dbType?: string;
  arity: "scalar" | "list";
};

SqlResultSet (queryRaw 的輸出)

interface SqlResultSet {
  columnNames: Array<string>; // 依序的欄位名稱
  columnTypes: Array<ColumnType>; // 對應 columnNames 的欄位型別
  rows: Array<Array<unknown>>; // 以陣列表示的列資料
  lastInsertId?: string; // 用於沒有 RETURNING 的 INSERT
}

ColumnTypeEnum 值

const ColumnTypeEnum = {
  Int32: 0,
  Int64: 1,
  Float: 2,
  Double: 3,
  Numeric: 4,
  Boolean: 5,
  Character: 6,
  Text: 7,
  Date: 8,
  Time: 9,
  DateTime: 10,
  Json: 11,
  Enum: 12,
  Bytes: 13,
  Set: 14,
  Uuid: 15,
  Int32Array: 64,
  Int64Array: 65,
  FloatArray: 66,
  DoubleArray: 67,
  NumericArray: 68,
  BooleanArray: 69,
  CharacterArray: 70,
  TextArray: 71,
  DateArray: 72,
  TimeArray: 73,
  DateTimeArray: 74,
  JsonArray: 75,
  EnumArray: 76,
  BytesArray: 77,
  UuidArray: 78,
  UnknownNumber: 128,
} as const;

SqlDriverAdapter

interface SqlDriverAdapter extends SqlQueryable {
  executeScript(script: string): Promise<void>;
  startTransaction(isolationLevel?: IsolationLevel): Promise<Transaction>;
  getConnectionInfo?(): ConnectionInfo;
  dispose(): Promise<void>;
}

Transaction

interface Transaction extends SqlQueryable {
  readonly options: TransactionOptions;
  commit(): Promise<void>;
  rollback(): Promise<void>;
}

type TransactionOptions = { usePhantomQuery: boolean };

SqlMigrationAwareDriverAdapterFactory

interface SqlMigrationAwareDriverAdapterFactory {
  readonly provider: "mysql" | "postgres" | "sqlite" | "sqlserver";
  readonly adapterName: string;
  connect(): Promise<SqlDriverAdapter>;
  connectToShadowDb(): Promise<SqlDriverAdapter>;
}

實作步驟

步驟 1:建立 Queryable 基底類別

class MyQueryable<TClient> implements SqlQueryable {
  readonly provider = "postgres" as const; // 或 'sqlite' | 'mysql' | 'sqlserver'
  readonly adapterName = "@my-org/adapter-mydb" as const;

  constructor(protected readonly client: TClient) {}

  async queryRaw(query: SqlQuery): Promise<SqlResultSet> {
    try {
      const args = query.args.map((arg, i) =>
        mapArg(arg, query.argTypes[i] ?? { scalarType: "unknown", arity: "scalar" })
      );

      // 使用你的驅動程式執行查詢
      const result = await this.client.query(query.sql, args);

      // 提取欄位後設資料
      const columnNames = /* 從結果取得 */;
      const columnTypes = /* 對應到 ColumnTypeEnum */;

      // 將列對應到 ResultValue 陣列
      const rows = result.map(row => mapRow(row, columnTypes));

      return { columnNames, columnTypes, rows };
    } catch (e) {
      this.onError(e);
    }
  }

  async executeRaw(query: SqlQuery): Promise<number> {
    try {
      const args = query.args.map((arg, i) =>
        mapArg(arg, query.argTypes[i] ?? { scalarType: "unknown", arity: "scalar" })
      );
      const result = await this.client.query(query.sql, args);
      return result.affectedRows ?? 0;
    } catch (e) {
      this.onError(e);
    }
  }

  protected onError(error: unknown): never {
    throw new DriverAdapterError(convertDriverError(error));
  }
}

步驟 2:建立 Transaction 類別

關鍵commit()rollback() 僅是生命週期鉤子。它們不得發出 SQL。Prisma 會透過交易物件上的 executeRaw 傳送 COMMIT/ROLLBACK

class MyTransaction extends MyQueryable<TClient> implements Transaction {
  readonly options: TransactionOptions;
  readonly #release: () => void;

  constructor(
    client: TClient,
    options: TransactionOptions,
    release: () => void,
  ) {
    super(client);
    this.options = options;
    this.#release = release;
  }

  commit(): Promise<void> {
    // 不要在此發出 COMMIT SQL — Prisma 透過 executeRaw 處理
    this.#release(); // 釋放連線/資源
    return Promise.resolve();
  }

  rollback(): Promise<void> {
    // 不要在此發出 ROLLBACK SQL — Prisma 透過 executeRaw 處理
    this.#release();
    return Promise.resolve();
  }
}

步驟 3:建立 Adapter 類別

class MyAdapter extends MyQueryable<TClient> implements SqlDriverAdapter {
  #transactionDepth = 0;

  constructor(client: TClient) {
    super(client);
  }

  async executeScript(script: string): Promise<void> {
    // 對於 SQLite:以 ';' 分割並執行每個陳述式
    // 對於 Postgres:使用多陳述式執行
    try {
      // 實作取決於驅動程式能力
    } catch (e) {
      this.onError(e);
    }
  }

  async startTransaction(
    isolationLevel?: IsolationLevel,
  ): Promise<Transaction> {
    // 驗證資料庫的隔離層級
    const validLevels = new Set<IsolationLevel>([
      "READ UNCOMMITTED",
      "READ COMMITTED",
      "REPEATABLE READ",
      "SERIALIZABLE",
    ]);

    if (isolationLevel !== undefined && !validLevels.has(isolationLevel)) {
      throw new DriverAdapterError({
        kind: "InvalidIsolationLevel",
        level: isolationLevel,
      });
    }

    const options: TransactionOptions = { usePhantomQuery: false };

    this.#transactionDepth += 1;
    const depth = this.#transactionDepth;

    try {
      if (depth === 1) {
        // 發出 BEGIN(如有指定隔離層級)
        const beginSql = isolationLevel
          ? `BEGIN ISOLATION LEVEL ${isolationLevel}`
          : "BEGIN";
        await this.client.query(beginSql);
      } else {
        // 巢狀:使用儲存點
        await this.client.query(`SAVEPOINT sp_${depth}`);
      }
    } catch (e) {
      this.#transactionDepth -= 1;
      this.onError(e);
    }

    const release = () => {
      this.#transactionDepth -= 1;
    };
    return new MyTransaction(this.client, options, release);
  }

  getConnectionInfo(): ConnectionInfo {
    return { supportsRelationJoins: true };
  }

  async dispose(): Promise<void> {
    await this.client.close();
  }
}

步驟 4:建立 Factory 類別

export type MyAdapterConfig = {
  url: string;
};

export type MyAdapterOptions = {
  shadowDatabaseUrl?: string;
};

export class MyAdapterFactory implements SqlMigrationAwareDriverAdapterFactory {
  readonly provider = "postgres" as const;
  readonly adapterName = "@my-org/adapter-mydb" as const;

  constructor(
    private readonly config: MyAdapterConfig,
    private readonly options?: MyAdapterOptions,
  ) {}

  connect(): Promise<SqlDriverAdapter> {
    return Promise.resolve(new MyAdapter(openConnection(this.config.url)));
  }

  connectToShadowDb(): Promise<SqlDriverAdapter> {
    const url = this.options?.shadowDatabaseUrl ?? this.config.url;
    return Promise.resolve(new MyAdapter(openConnection(url)));
  }
}

轉換輔助函式

引數對映(輸入)

將 Prisma 引數值轉換為驅動程式原生型別:

function mapArg(arg: unknown, argType: ArgType): unknown {
  if (arg === null || arg === undefined) return null;

  // 字串 → 數字(int 欄位)
  if (typeof arg === "string" && argType.scalarType === "int")
    return Number.parseInt(arg, 10);

  // 字串 → 數字(float 欄位)
  if (typeof arg === "string" && argType.scalarType === "float")
    return Number.parseFloat(arg);

  // 字串 → BigInt(bigint 欄位)
  if (typeof arg === "string" && argType.scalarType === "bigint")
    return BigInt(arg);

  // Base64 字串 → Buffer(bytes 欄位)
  if (typeof arg === "string" && argType.scalarType === "bytes")
    return Buffer.from(arg, "base64");

  // 布林值 → 0/1(SQLite)
  if (typeof arg === "boolean" && /* SQLite */)
    return arg ? 1 : 0;

  return arg;
}

列對映(輸出)

將驅動程式結果值轉換為 Prisma 預期的型別:

function mapRow(row: unknown[], columnTypes: ColumnType[]): ResultValue[] {
  const result: ResultValue[] = [];

  for (let i = 0; i < row.length; i++) {
    const value = row[i] ?? null;
    const colType = columnTypes[i];

    if (value === null) {
      result.push(null);
      continue;
    }

    // bigint → 字串(Int64,JSON 安全)
    if (typeof value === "bigint") {
      result.push(value.toString());
      continue;
    }

    // Date → ISO 8601 字串(DateTime)
    if (value instanceof Date) {
      result.push(value.toISOString());
      continue;
    }

    // JSON 物件 → 字串化
    if (colType === ColumnTypeEnum.Json && typeof value === "object") {
      result.push(JSON.stringify(value));
      continue;
    }

    result.push(value as ResultValue);
  }

  return result;
}

欄位型別推斷

當驅動程式未提供型別後設資料時,從 JS 值推斷:

function inferColumnType(value: NonNullable<unknown>): ColumnType {
  if (typeof value === "boolean") return ColumnTypeEnum.Boolean;
  if (typeof value === "bigint") return ColumnTypeEnum.Int64;
  if (value instanceof Uint8Array) return ColumnTypeEnum.Bytes;
  if (value instanceof Date) return ColumnTypeEnum.DateTime;
  if (Array.isArray(value)) return ColumnTypeEnum.Text; // 備用
  if (typeof value === "object") return ColumnTypeEnum.Json;
  if (typeof value === "number") return ColumnTypeEnum.UnknownNumber;
  return ColumnTypeEnum.Text;
}

錯誤處理

將驅動程式錯誤對應到 MappedError,讓 Prisma 正確處理:

function convertDriverError(error: unknown): MappedError {
  if (error instanceof Error) {
    // 資料庫特定的錯誤對映
    const dbError = error as Error & { code?: string; errno?: number };

    // PostgreSQL 範例
    if (dbError.code === "23505") {
      return { kind: "UniqueConstraintViolation" };
    }
    if (dbError.code === "23502") {
      return { kind: "NullConstraintViolation" };
    }
    if (dbError.code === "23503") {
      return { kind: "ForeignKeyConstraintViolation" };
    }
    if (dbError.code === "42P01") {
      return { kind: "TableDoesNotExist" };
    }

    // SQLite 範例
    if (error.name === "SQLiteError") {
      return {
        kind: "sqlite",
        extendedCode: dbError.errno ?? 1,
        message: error.message,
      };
    }

    // PostgreSQL 原始錯誤
    if (dbError.code) {
      return {
        kind: "postgres",
        code: dbError.code,
        severity: "ERROR",
        message: error.message,
        detail: undefined,
        column: undefined,
        hint: undefined,
      };
    }
  }

  return { kind: "GenericJs", id: 0 };
}

資料庫特定注意事項

SQLite

  • 開啟資料庫時設定 safeIntegers: true,以取得大整數的 bigint
  • SERIALIZABLE 隔離層級有效
  • executeScript:以 ; 分割並逐一執行每個陳述式
  • 布林值:儲存為 0/1,回傳為布林值

PostgreSQL

  • 所有標準隔離層級均有效
  • 對於連線池(PgBouncer),使用 prepare: false
  • 交易需要專用連線(reserve() 模式)
  • executeScript:使用多陳述式執行(某些驅動程式中為 .simple()
  • int8 欄位可能回傳為字串(已由驅動程式字串化)
  • numeric 欄位回傳為字串以保留精確度

MySQL/MariaDB

  • 支援 READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
  • 使用 ? 作為參數的佔位符
  • BIGINT 處理為字串以處理大數值

測試策略

單元測試(無 PrismaClient)

直接使用原始資料庫驅動程式測試介面卡:

describe("queryRaw", () => {
  test("回傳欄位名稱與型別", async () => {
    const adapter = new MyAdapter(createTestConnection());
    const result = await adapter.queryRaw({
      sql: "SELECT id, name FROM users",
      args: [],
      argTypes: [],
    });
    expect(result.columnNames).toEqual(["id", "name"]);
    expect(result.columnTypes[0]).toBe(ColumnTypeEnum.Int32);
  });
});

describe("startTransaction", () => {
  test("commit 保留變更", async () => {
    const adapter = new MyAdapter(createTestConnection());
    const tx = await adapter.startTransaction();
    await tx.executeRaw({
      sql: "INSERT INTO users (name) VALUES (?)",
      args: ["Alice"],
      argTypes: [],
    });
    // Prisma 透過 executeRaw 傳送 COMMIT
    await tx.executeRaw({ sql: "COMMIT", args: [], argTypes: [] });
    await tx.commit(); // 僅生命週期鉤子
    // 驗證資料已保留
  });
});

端到端測試(搭配 PrismaClient)

測試完整整合:

describe("E2E", () => {
  let prisma: PrismaClient;

  beforeEach(async () => {
    const factory = new MyAdapterFactory({ url: TEST_DB_URL });
    prisma = new PrismaClient({ adapter: factory });
  });

  test("CRUD 操作", async () => {
    const user = await prisma.user.create({ data: { name: "Alice" } });
    expect(user.id).toBeGreaterThan(0);

    const found = await prisma.user.findUnique({ where: { id: user.id } });
    expect(found?.name).toBe("Alice");
  });

  test("交易在錯誤時回滾", async () => {
    await expect(
      prisma.$transaction(async (tx) => {
        await tx.user.create({ data: { name: "Bob" } });
        throw new Error("Rollback!");
      }),
    ).rejects.toThrow();

    expect(await prisma.user.count()).toBe(0);
  });
});

使用範例

import { PrismaClient } from "./generated/prisma/client";
import { MyAdapterFactory } from "@my-org/adapter-mydb";

const factory = new MyAdapterFactory({
  url: process.env.DATABASE_URL!,
});

const prisma = new PrismaClient({ adapter: factory });

// 正常使用 prisma
const users = await prisma.user.findMany();

檢查清單

在認為介面卡完成之前:

  • [ ] SqlMigrationAwareDriverAdapterFactory 已實作 connect()connectToShadowDb()
  • [ ] SqlDriverAdapter 實作了 queryRawexecuteRawexecuteScriptstartTransactiondispose
  • [ ] Transaction 實作了 queryRawexecuteRawcommitrollback,並帶有 options: { usePhantomQuery: false }
  • [ ] commit()rollback() 僅是生命週期鉤子(不發出 SQL)
  • [ ] startTransaction 發出 BEGIN(深度 1)或 SAVEPOINT sp_N(巢狀)
  • [ ] 引數對映處理:string→int、string→bigint、string→float、base64→bytes
  • [ ] 列對映處理:bigint→string、Date→ISO 字串、JSON→string
  • [ ] 欄位型別正確對應到 ColumnTypeEnum
  • [ ] 錯誤包裝在 DriverAdapterError 中,並帶有適當的 MappedError kind
  • [ ] 針對目標資料庫的隔離層級驗證
  • [ ] queryRaw、executeRaw、executeScript、交易的單元測試通過
  • [ ] 搭配真實 PrismaClient 的端到端測試通過