prisma-driver-adapter-implementation

安装量: 685
排名: #1718

安装

npx skills add https://github.com/prisma/skills --skill prisma-driver-adapter-implementation

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 && ! 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"
&&
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
;
}
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
&&
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
&
{
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
返回排行榜