.agents/skills/prisma-driver-adapter-implementation/SKILL.md
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.
npx skillsauth add HuynhSang2005/delivery-app prisma-driver-adapter-implementationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides everything needed to implement a Prisma ORM v7 driver adapter for any database.
┌─────────────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────────────┘
Import from @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";
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";
};
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
}
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;
interface SqlDriverAdapter extends SqlQueryable {
executeScript(script: string): Promise<void>;
startTransaction(isolationLevel?: IsolationLevel): Promise<Transaction>;
getConnectionInfo?(): ConnectionInfo;
dispose(): Promise<void>;
}
interface Transaction extends SqlQueryable {
readonly options: TransactionOptions;
commit(): Promise<void>;
rollback(): Promise<void>;
}
type TransactionOptions = { usePhantomQuery: boolean };
interface SqlMigrationAwareDriverAdapterFactory {
readonly provider: "mysql" | "postgres" | "sqlite" | "sqlserver";
readonly adapterName: string;
connect(): Promise<SqlDriverAdapter>;
connectToShadowDb(): Promise<SqlDriverAdapter>;
}
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));
}
}
Critical: commit() and rollback() are lifecycle hooks only. They must NOT issue SQL. Prisma sends COMMIT/ROLLBACK via executeRaw on the transaction object.
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();
}
}
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();
}
}
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)));
}
}
Convert Prisma argument values to driver-native types:
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;
}
Convert driver result values to Prisma-expected types:
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;
}
When the driver doesn't provide type metadata, infer from JS values:
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;
}
Map driver errors to MappedError for Prisma to handle correctly:
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 };
}
safeIntegers: true when opening the database to get bigint for large integersSERIALIZABLE isolation level is validexecuteScript: split on ; and run each statement individuallyprepare: falsereserve() pattern)executeScript: use multi-statement execution (.simple() in some drivers)int8 columns may return as string (already stringified by driver)numeric columns return as string to preserve precisionREAD UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE? placeholders for parametersBIGINT as string for large valuesTest the adapter directly with the raw database driver:
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
});
});
Test the full integration:
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);
});
});
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();
Before considering the adapter complete:
SqlMigrationAwareDriverAdapterFactory implemented with connect() and connectToShadowDb()SqlDriverAdapter implements queryRaw, executeRaw, executeScript, startTransaction, disposeTransaction implements queryRaw, executeRaw, commit, rollback with options: { usePhantomQuery: false }commit() and rollback() are lifecycle hooks only (no SQL issued)startTransaction issues BEGIN (depth 1) or SAVEPOINT sp_N (nested)ColumnTypeEnumDriverAdapterError with proper MappedError kindtools
React Hook Form performance optimization for client-side form validation using useForm, useWatch, useController, and useFieldArray. This skill should be used when building client-side controlled forms with React Hook Form library. This skill does NOT cover React 19 Server Actions, useActionState, or server-side form handling.
tools
Build type-safe validated forms using React Hook Form v7 and Zod v4. Single schema works on client and server with full TypeScript inference via z.infer. Use when building forms, multi-step wizards, or fixing uncontrolled warnings, resolver errors, useFieldArray issues, performance problems with large forms.
tools
Prisma Postgres setup and operations guidance across Console, create-db CLI, Management API, and Management API SDK. Use when creating Prisma Postgres databases, working in Prisma Console, provisioning with create-db/create-pg/create-postgres, or integrating programmatic provisioning with service tokens or OAuth.
development
Guides for configuring Prisma with different database providers (PostgreSQL, MySQL, SQLite, MongoDB, etc.). Use when setting up a new project, changing databases, or troubleshooting connection issues. Triggers on "configure postgres", "connect to mysql", "setup mongodb", "sqlite setup".