prisma-driver-adapter-implementation

Complete reference for implementing Prisma v7 driver adapters with transaction lifecycle, error mapping, and type conversion details. Covers four required interfaces: SqlDriverAdapter , Transaction , SqlMigrationAwareDriverAdapterFactory , and SqlQueryable , with full method signatures and lifecycle requirements Transaction commit() and rollback() are lifecycle hooks only; Prisma sends SQL via executeRaw , not these methods Includes argument mapping (string→int/bigint/float, base64→bytes) and row mapping (bigint→string, Date→ISO, JSON→string) with column type inference fallbacks Provides database-specific guidance for SQLite, PostgreSQL, and MySQL, plus error conversion patterns for constraint violations and driver-native exceptions Contains implementation templates, testing strategies (unit and E2E), and a pre-launch verification checklist

INSTALLATION
npx skills add https://github.com/prisma/skills --skill prisma-driver-adapter-implementation
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Prisma 7 Driver Adapter Implementation Guide

This skill provides everything needed to implement a Prisma ORM v7 driver adapter for any database.

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)       │

└─────────────────────────────────────────────────────────────────┘

Required Interfaces

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";

Interface Definitions

SqlQuery (input to queryRaw/executeRaw)

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";

};

SqlResultSet (output from queryRaw)

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

}

ColumnTypeEnum values

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>;

}

Implementation Steps

Step 1: Create the Queryable base class

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));

  }

}

Step 2: Create the Transaction class

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();

  }

}

Step 3: Create the Adapter class

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 &#x26;&#x26; !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();

  }

}

Step 4: Create the Factory class

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:

function mapArg(arg: unknown, argType: ArgType): unknown {

  if (arg === null || arg === undefined) return null;

  // String → number for int columns

  if (typeof arg === "string" &#x26;&#x26; argType.scalarType === "int")

    return Number.parseInt(arg, 10);

  // String → number for float columns

  if (typeof arg === "string" &#x26;&#x26; argType.scalarType === "float")

    return Number.parseFloat(arg);

  // String → BigInt for bigint columns

  if (typeof arg === "string" &#x26;&#x26; argType.scalarType === "bigint")

    return BigInt(arg);

  // Base64 string → Buffer for bytes columns

  if (typeof arg === "string" &#x26;&#x26; argType.scalarType === "bytes")

    return Buffer.from(arg, "base64");

  // Boolean → 0/1 for SQLite

  if (typeof arg === "boolean" &#x26;&#x26; /* SQLite */)

    return arg ? 1 : 0;

  return arg;

}

Row Mapping (output)

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 &#x26;&#x26; 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:

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;

}

Error Handling

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 &#x26; { 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 };

}

Database-Specific Notes

SQLite

  • Set safeIntegers: true when opening the database to get bigint for large integers
  • Only SERIALIZABLE isolation level is valid
  • executeScript: split on ; and run each statement individually
  • Boolean values: store as 0/1, return as boolean

PostgreSQL

  • All standard isolation levels are valid
  • For connection pooling (PgBouncer), use prepare: false
  • Transactions require a dedicated connection (reserve() 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 precision

MySQL/MariaDB

  • Supports READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE
  • Use ? placeholders for parameters
  • Handle BIGINT as string for large values

Testing Strategy

Unit Tests (no PrismaClient)

Test 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

  });

});

E2E Tests (with PrismaClient)

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);

  });

});

Usage Example

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();

Checklist

Before considering the adapter complete:

  • SqlMigrationAwareDriverAdapterFactory implemented with connect() and connectToShadowDb()
  • SqlDriverAdapter implements queryRaw, executeRaw, executeScript, startTransaction, dispose
  • Transaction 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)
  • 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 DriverAdapterError with proper MappedError kind
  • Isolation level validation for the target database
  • Unit tests pass for queryRaw, executeRaw, executeScript, transactions
  • E2E tests pass with real PrismaClient
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card