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 UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE - 使用
?作為參數的佔位符 - 將
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實作了queryRaw、executeRaw、executeScript、startTransaction、dispose - [ ]
Transaction實作了queryRaw、executeRaw、commit、rollback,並帶有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中,並帶有適當的MappedErrorkind - [ ] 針對目標資料庫的隔離層級驗證
- [ ] queryRaw、executeRaw、executeScript、交易的單元測試通過
- [ ] 搭配真實 PrismaClient 的端到端測試通過






