prisma-driver-adapter-implementation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePrisma 7 Driver Adapter Implementation Guide
Prisma 7 驱动适配器实现指南
This skill provides everything needed to implement a Prisma ORM v7 driver adapter for any database.
本指南提供了为任意数据库实现Prisma ORM v7驱动适配器所需的全部内容。
Architecture Overview
架构概述
┌─────────────────────────────────────────────────────────────────┐
│ PrismaClient │
│ (requires adapter factory) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SqlMigrationAwareDriverAdapterFactory │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ connect() │ │ connectToShadowDb() │ │
│ │ → SqlDriverAdapter │ │ → SqlDriverAdapter │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SqlDriverAdapter │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ queryRaw() │ │ executeRaw() │ │ startTransaction() │ │
│ │ → ResultSet │ │ → number │ │ → Transaction │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │executeScript │ │ dispose() │ │ getConnectionInfo() │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Transaction │
│ Extends SqlQueryable + commit() + rollback() + options │
│ (lifecycle hooks only — Prisma sends SQL via executeRaw) │
└─────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────┐
│ PrismaClient │
│ (requires adapter factory) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SqlMigrationAwareDriverAdapterFactory │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ connect() │ │ connectToShadowDb() │ │
│ │ → SqlDriverAdapter │ │ → SqlDriverAdapter │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SqlDriverAdapter │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ queryRaw() │ │ executeRaw() │ │ startTransaction() │ │
│ │ → ResultSet │ │ → number │ │ → Transaction │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │executeScript │ │ dispose() │ │ getConnectionInfo() │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Transaction │
│ Extends SqlQueryable + commit() + rollback() + options │
│ (lifecycle hooks only — Prisma sends SQL via executeRaw) │
└─────────────────────────────────────────────────────────────────┘Required Interfaces
必备接口
Import from :
@prisma/driver-adapter-utilstypescript
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";从导入:
@prisma/driver-adapter-utilstypescript
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";Interface Definitions
接口定义
SqlQuery (input to queryRaw/executeRaw)
SqlQuery(queryRaw/executeRaw的输入参数)
typescript
type SqlQuery = {
sql: string; // Parameterized SQL with placeholders
args: Array<unknown>; // Bound parameter values
argTypes: Array<ArgType>; // Type hints for each argument
};
type ArgType = {
scalarType: ArgScalarType; // 'string' | 'int' | 'bigint' | 'float' | 'decimal' | 'boolean' | 'enum' | 'uuid' | 'json' | 'datetime' | 'bytes' | 'unknown'
dbType?: string;
arity: "scalar" | "list";
};typescript
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 (output from queryRaw)
SqlResultSet(queryRaw的输出结果)
typescript
interface SqlResultSet {
columnNames: Array<string>; // Column names in order
columnTypes: Array<ColumnType>; // Column types matching columnNames
rows: Array<Array<unknown>>; // Row data as arrays
lastInsertId?: string; // For INSERT without RETURNING
}typescript
interface SqlResultSet {
columnNames: Array<string>; // 按顺序排列的列名
columnTypes: Array<ColumnType>; // 与columnNames对应的列类型
rows: Array<Array<unknown>>; // 以数组形式存储的行数据
lastInsertId?: string; // 用于无RETURNING子句的INSERT操作
}ColumnTypeEnum values
ColumnTypeEnum 取值
typescript
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;typescript
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
SqlDriverAdapter
typescript
interface SqlDriverAdapter extends SqlQueryable {
executeScript(script: string): Promise<void>;
startTransaction(isolationLevel?: IsolationLevel): Promise<Transaction>;
getConnectionInfo?(): ConnectionInfo;
dispose(): Promise<void>;
}typescript
interface SqlDriverAdapter extends SqlQueryable {
executeScript(script: string): Promise<void>;
startTransaction(isolationLevel?: IsolationLevel): Promise<Transaction>;
getConnectionInfo?(): ConnectionInfo;
dispose(): Promise<void>;
}Transaction
Transaction
typescript
interface Transaction extends SqlQueryable {
readonly options: TransactionOptions;
commit(): Promise<void>;
rollback(): Promise<void>;
}
type TransactionOptions = { usePhantomQuery: boolean };typescript
interface Transaction extends SqlQueryable {
readonly options: TransactionOptions;
commit(): Promise<void>;
rollback(): Promise<void>;
}
type TransactionOptions = { usePhantomQuery: boolean };SqlMigrationAwareDriverAdapterFactory
SqlMigrationAwareDriverAdapterFactory
typescript
interface SqlMigrationAwareDriverAdapterFactory {
readonly provider: "mysql" | "postgres" | "sqlite" | "sqlserver";
readonly adapterName: string;
connect(): Promise<SqlDriverAdapter>;
connectToShadowDb(): Promise<SqlDriverAdapter>;
}typescript
interface SqlMigrationAwareDriverAdapterFactory {
readonly provider: "mysql" | "postgres" | "sqlite" | "sqlserver";
readonly adapterName: string;
connect(): Promise<SqlDriverAdapter>;
connectToShadowDb(): Promise<SqlDriverAdapter>;
}Implementation Steps
实现步骤
Step 1: Create the Queryable base class
步骤1:创建Queryable基类
typescript
class MyQueryable<TClient> implements SqlQueryable {
readonly provider = "postgres" as const; // or '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" })
);
// Execute query with your driver
const result = await this.client.query(query.sql, args);
// Extract column metadata
const columnNames = /* get from result */;
const columnTypes = /* map to ColumnTypeEnum */;
// Map rows to ResultValue arrays
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));
}
}typescript
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));
}
}Step 2: Create the Transaction class
步骤2:创建Transaction类
Critical: and are lifecycle hooks only. They must NOT issue SQL. Prisma sends / via on the transaction object.
commit()rollback()COMMITROLLBACKexecuteRawtypescript
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> {
// DO NOT issue COMMIT SQL here — Prisma does it via executeRaw
this.#release(); // Release connection/resources
return Promise.resolve();
}
rollback(): Promise<void> {
// DO NOT issue ROLLBACK SQL here — Prisma does it via executeRaw
this.#release();
return Promise.resolve();
}
}关键注意事项:和仅作为生命周期钩子使用。它们绝对不能执行SQL语句。Prisma会通过事务对象的方法发送/命令。
commit()rollback()executeRawCOMMITROLLBACKtypescript
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();
}
}Step 3: Create the Adapter class
步骤3:创建Adapter类
typescript
class MyAdapter extends MyQueryable<TClient> implements SqlDriverAdapter {
#transactionDepth = 0;
constructor(client: TClient) {
super(client);
}
async executeScript(script: string): Promise<void> {
// For SQLite: split on ';' and run each statement
// For Postgres: use multi-statement execution
try {
// Implementation depends on driver capabilities
} catch (e) {
this.onError(e);
}
}
async startTransaction(
isolationLevel?: IsolationLevel,
): Promise<Transaction> {
// Validate isolation level for your database
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) {
// Issue BEGIN (with isolation level if specified)
const beginSql = isolationLevel
? `BEGIN ISOLATION LEVEL ${isolationLevel}`
: "BEGIN";
await this.client.query(beginSql);
} else {
// Nested: use savepoints
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();
}
}typescript
class MyAdapter extends MyQueryable<TClient> implements SqlDriverAdapter {
#transactionDepth = 0;
constructor(client: TClient) {
super(client);
}
async executeScript(script: string): Promise<void> {
// 对于SQLite:按';'拆分并逐个执行语句
// 对于PostgreSQL:使用多语句执行
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();
}
}Step 4: Create the Factory class
步骤4:创建Factory类
typescript
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)));
}
}typescript
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)));
}
}Conversion Helpers
转换工具函数
Argument Mapping (input)
参数映射(输入)
Convert Prisma argument values to driver-native types:
typescript
function mapArg(arg: unknown, argType: ArgType): unknown {
if (arg === null || arg === undefined) return null;
// String → number for int columns
if (typeof arg === "string" && argType.scalarType === "int")
return Number.parseInt(arg, 10);
// String → number for float columns
if (typeof arg === "string" && argType.scalarType === "float")
return Number.parseFloat(arg);
// String → BigInt for bigint columns
if (typeof arg === "string" && argType.scalarType === "bigint")
return BigInt(arg);
// Base64 string → Buffer for bytes columns
if (typeof arg === "string" && argType.scalarType === "bytes")
return Buffer.from(arg, "base64");
// Boolean → 0/1 for SQLite
if (typeof arg === "boolean" && /* SQLite */)
return arg ? 1 : 0;
return arg;
}将Prisma参数值转换为驱动原生类型:
typescript
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;
}Row Mapping (output)
行数据映射(输出)
Convert driver result values to Prisma-expected types:
typescript
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 → string for Int64 (JSON-safe)
if (typeof value === "bigint") {
result.push(value.toString());
continue;
}
// Date → ISO 8601 string for DateTime
if (value instanceof Date) {
result.push(value.toISOString());
continue;
}
// JSON objects → stringified
if (colType === ColumnTypeEnum.Json && typeof value === "object") {
result.push(JSON.stringify(value));
continue;
}
result.push(value as ResultValue);
}
return result;
}将驱动返回的结果值转换为Prisma期望的类型:
typescript
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→字符串(保证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;
}Column Type Inference
列类型推断
When the driver doesn't provide type metadata, infer from JS values:
typescript
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; // fallback
if (typeof value === "object") return ColumnTypeEnum.Json;
if (typeof value === "number") return ColumnTypeEnum.UnknownNumber;
return ColumnTypeEnum.Text;
}当驱动不提供类型元数据时,从JS值推断列类型:
typescript
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;
}Error Handling
错误处理
Map driver errors to for Prisma to handle correctly:
MappedErrortypescript
function convertDriverError(error: unknown): MappedError {
if (error instanceof Error) {
// Database-specific error mapping
const dbError = error as Error & { code?: string; errno?: number };
// PostgreSQL example
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 example
if (error.name === "SQLiteError") {
return {
kind: "sqlite",
extendedCode: dbError.errno ?? 1,
message: error.message,
};
}
// PostgreSQL raw error
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 };
}将驱动错误映射为,以便Prisma正确处理:
MappedErrortypescript
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 };
}Database-Specific Notes
数据库特定注意事项
SQLite
SQLite
- Set when opening the database to get
safeIntegers: truefor large integersbigint - Only isolation level is valid
SERIALIZABLE - : split on
executeScriptand run each statement individually; - Boolean values: store as 0/1, return as boolean
- 打开数据库时设置,以便大整数以
safeIntegers: true类型返回bigint - 仅支持隔离级别
SERIALIZABLE - :按
executeScript拆分脚本并逐个执行语句; - 布尔值:以0/1存储,以布尔值返回
PostgreSQL
PostgreSQL
- All standard isolation levels are valid
- For connection pooling (PgBouncer), use
prepare: false - Transactions require a dedicated connection (pattern)
reserve() - : use multi-statement execution (
executeScriptin some drivers).simple() - columns may return as string (already stringified by driver)
int8 - columns return as string to preserve precision
numeric
- 支持所有标准隔离级别
- 对于连接池(如PgBouncer),使用
prepare: false - 事务需要专用连接(使用模式)
reserve() - :使用多语句执行(部分驱动中使用
executeScript).simple() - 列可能以字符串形式返回(驱动已自动字符串化)
int8 - 列以字符串形式返回以保留精度
numeric
MySQL/MariaDB
MySQL/MariaDB
- Supports ,
READ UNCOMMITTED,READ COMMITTED,REPEATABLE READSERIALIZABLE - Use placeholders for parameters
? - Handle as string for large values
BIGINT
- 支持、
READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ隔离级别SERIALIZABLE - 使用作为参数占位符
? - 大数值以字符串形式处理
BIGINT
Testing Strategy
测试策略
Unit Tests (no PrismaClient)
单元测试(无PrismaClient)
Test the adapter directly with the raw database driver:
typescript
describe("queryRaw", () => {
test("returns column names and types", 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 persists changes", async () => {
const adapter = new MyAdapter(createTestConnection());
const tx = await adapter.startTransaction();
await tx.executeRaw({
sql: "INSERT INTO users (name) VALUES (?)",
args: ["Alice"],
argTypes: [],
});
// Prisma sends COMMIT via executeRaw
await tx.executeRaw({ sql: "COMMIT", args: [], argTypes: [] });
await tx.commit(); // lifecycle hook only
// Verify data persisted
});
});直接使用原生数据库驱动测试适配器:
typescript
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("提交操作持久化变更", 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(); // 仅作为生命周期钩子
// 验证数据已持久化
});
});E2E Tests (with PrismaClient)
端到端测试(使用PrismaClient)
Test the full integration:
typescript
describe("E2E", () => {
let prisma: PrismaClient;
beforeEach(async () => {
const factory = new MyAdapterFactory({ url: TEST_DB_URL });
prisma = new PrismaClient({ adapter: factory });
});
test("CRUD operations", 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("transactions roll back on error", 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);
});
});测试完整集成流程:
typescript
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);
});
});Usage Example
使用示例
typescript
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 });
// Use prisma normally
const users = await prisma.user.findMany();typescript
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();Checklist
检查清单
Before considering the adapter complete:
- implemented with
SqlMigrationAwareDriverAdapterFactoryandconnect()connectToShadowDb() - implements
SqlDriverAdapter,queryRaw,executeRaw,executeScript,startTransactiondispose - implements
Transaction,queryRaw,executeRaw,commitwithrollbackoptions: { usePhantomQuery: false } - and
commit()are lifecycle hooks only (no SQL issued)rollback() - issues
startTransaction(depth 1) orBEGIN(nested)SAVEPOINT sp_N - Argument mapping handles: string→int, string→bigint, string→float, base64→bytes
- Row mapping handles: bigint→string, Date→ISO string, JSON→string
- Column types correctly mapped to
ColumnTypeEnum - Errors wrapped in with proper
DriverAdapterErrorkindMappedError - Isolation level validation for the target database
- Unit tests pass for queryRaw, executeRaw, executeScript, transactions
- E2E tests pass with real PrismaClient
在认为适配器开发完成前,请确认以下项已完成:
- 已实现,包含
SqlMigrationAwareDriverAdapterFactory和connect()方法connectToShadowDb() - 已实现
SqlDriverAdapter、queryRaw、executeRaw、executeScript、startTransaction方法dispose - 已实现
Transaction、queryRaw、executeRaw、commit方法,且rollbackoptions: { usePhantomQuery: false } - 和
commit()仅作为生命周期钩子使用(未执行任何SQL语句)rollback() - 在深度为1时执行
startTransaction,嵌套时执行BEGINSAVEPOINT sp_N - 参数映射已处理:字符串→整数、字符串→大整数、字符串→浮点数、Base64→字节
- 行数据映射已处理:大整数→字符串、日期→ISO字符串、JSON→字符串
- 列类型已正确映射到
ColumnTypeEnum - 错误已包装在中,并带有正确的
DriverAdapterError类型MappedError - 已针对目标数据库进行隔离级别验证
- queryRaw、executeRaw、executeScript、事务相关的单元测试已通过
- 与真实PrismaClient集成的端到端测试已通过