Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pglite-adapter #23877

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/adapter-pg-lite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# @prisma/adapter-pg-lite

This package contains the driver adapter for Prisma ORM that enables usage of the [`PGlite`](https://github.com/electric-sql/pglite) database driver for PostgreSQL running in WebAssembly. You can learn more in the [documentation](https://pris.ly/d/adapter-pg-lite).

`PGlite` is a lightweight, WASM-based PostgreSQL build that allows running PostgreSQL directly in the browser, Node.js, and Bun environments without any external dependencies. It is only 2.6mb gzipped and supports both ephemeral in-memory databases and persistent storage.

> **Note:**: Support for the `PGlite` driver is available from Prisma versions [5.4.2](https://github.com/prisma/prisma/releases/tag/5.4.2) and later.

## Usage

This section explains how you can use it with Prisma ORM and the `@prisma/adapter-pg-lite` driver adapter. Be sure that the `DATABASE_URL` environment variable is set to your PostgreSQL connection string (e.g., in a `.env` file) or use a direct in-memory connection.

### 1. Enable the `driverAdapters` Preview feature flag

Since driver adapters are currently in [Preview](/orm/more/releases#preview), you need to enable its feature flag on the `datasource` block in your Prisma schema:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Since driver adapters are currently in [Preview](/orm/more/releases#preview), you need to enable its feature flag on the `datasource` block in your Prisma schema:
Since driver adapters are currently in [Preview](/orm/more/releases#preview), you need to enable its feature flag in the `generator client` block in your Prisma schema:


```prisma
// schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}

datasource db {
provider = "postgresql"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be pglite instead?

Suggested change
provider = "postgresql"
provider = "pglite"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tobbe I wondered that myself but if you look at the Neon adapter https://github.com/prisma/prisma/tree/main/packages/adapter-neon it specifies ‘postgres’ and in the PlanetScale adapter ‘mysql’ so it might be the underlying provider?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I went off of the new provider added here

https://github.com/BLamy/prisma/blob/bd007e4b5f2cc2063d828c2325642de4d3388f70/packages/driver-adapter-utils/src/types.ts#L86
readonly provider: 'mysql' | 'postgres' | 'sqlite' | 'pglite'

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d have to check with the Prisma team by I thought of it as “flavor” and “client” — meaning this flavor of SQL and the features it supports is “postgres” (for example arrays and enums are supported — how you define your schema etc ), but the client used to make queries etc is the adapter “pglite”. But still learning.

Copy link
Author

@BLamy BLamy Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was a little confused by this. I think what I want is that I want postgres to be used on both prod and dev. But if NODE_ENV=test I want it to be pglite.

Not sure if that goal aligns with what everyone else wants (or even if it's a good goal for that matter)

url = env("DATABASE_URL")
}
```

Once you have added the feature flag to your schema, re-generate Prisma Client:

```
npx prisma generate
```

### 2. Install the dependencies

Next, install the `@electric-sql/pglite` package and Prisma ORM's driver adapter:
4 changes: 4 additions & 0 deletions packages/adapter-pg-lite/helpers/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { build } from '../../../helpers/compile/build'
import { adapterConfig } from '../../../helpers/compile/configs'

void build(adapterConfig)
3 changes: 3 additions & 0 deletions packages/adapter-pg-lite/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
preset: '../../helpers/test/presets/default.js',
}
54 changes: 54 additions & 0 deletions packages/adapter-pg-lite/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@prisma/adapter-pg",
"version": "0.0.0",
"description": "Prisma's driver adapter for \"pg\"",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
}
},
"repository": {
"type": "git",
"url": "https://github.com/prisma/prisma.git",
"directory": "packages/adapter-pg"
},
"scripts": {
"dev": "DEV=true tsx helpers/build.ts",
"build": "tsx helpers/build.ts",
"test": "jest"
},
"files": [
"dist",
"README.md"
],
"keywords": [],
"author": "Tom Houlé <houle@prisma.io>",
"license": "Apache-2.0",
"sideEffects": false,
"dependencies": {
"@electric-sql/pglite": "0.1.3",
"@prisma/driver-adapter-utils": "workspace:*",
"postgres-array": "3.0.2"
},
"devDependencies": {
"@swc/core": "1.4.11",
"@swc/jest": "0.2.36",
"@types/pg": "8.11.5",
"jest": "29.7.0",
"jest-junit": "16.0.0",
"pg": "8.11.5"
},
"peerDependencies": {
"pg": "^8.11.3"
}
}
1 change: 1 addition & 0 deletions packages/adapter-pg-lite/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PrismaPgLite } from './pg-lite'
28 changes: 28 additions & 0 deletions packages/adapter-pg-lite/src/pg-lite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Client, Pool } from 'pg'

import { PrismaPgLite } from './pg-lite'

describe('validation', () => {
test('throws if passed Client instance', () => {
const client = new Client()

expect(() => {
// @ts-ignore
new PrismaPg(client)
}).toThrowErrorMatchingInlineSnapshot(`
"PrismaPg must be initialized with an instance of Pool:
import { Pool } from 'pg'
const pool = new Pool({ connectionString: url })
const adapter = new PrismaPg(pool)
"
`)
})

test('accepts Pool instance', () => {
const pool = new Pool()

expect(() => {
new PrismaPgLite(pool)

Check failure on line 25 in packages/adapter-pg-lite/src/pg-lite.test.ts

View workflow job for this annotation

GitHub Actions / Tests / Workspace types (16)

Argument of type 'Pool' is not assignable to parameter of type 'PGlite'.
}).not.toThrow()
})
})
125 changes: 125 additions & 0 deletions packages/adapter-pg-lite/src/pg-lite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/* eslint-disable @typescript-eslint/require-await */
import { PGlite } from '@electric-sql/pglite'
import type {
ColumnType,
ConnectionInfo,
DriverAdapter,
Query,
Queryable,
Result,
ResultSet,
Transaction,
TransactionOptions,
} from '@prisma/driver-adapter-utils'
import { Debug, err, ok } from '@prisma/driver-adapter-utils'

import { name as packageName } from '../package.json'

const debug = Debug('prisma:driver-adapter:pglite')

class PgLiteQueryable<ClientT extends PGlite> implements Queryable {
readonly provider = 'pglite'
readonly adapterName = packageName

constructor(protected readonly client: ClientT) {}

async queryRaw(query: Query): Promise<Result<ResultSet>> {
const tag = '[js::query_raw]'
debug(`${tag} %O`, query)

const res = await this.performIO(query)

if (!res.ok) {
return err(res.error)
}

const { fields, rows } = res.value
const columnNames = fields.map((field) => field.name)
const columnTypes: ColumnType[] = fields.map((field) => field.dataTypeID as ColumnType)

return ok({
columnNames,
columnTypes,
rows,
})
}

async executeRaw(query: Query): Promise<Result<number>> {
const tag = '[js::execute_raw]'
debug(`${tag} %O`, query)

return (await this.performIO(query)).map(({ affectedRows }) => affectedRows ?? 0)
}

private async performIO(query: Query): Promise<Result<any>> {
const { sql, args: values } = query

try {
const result = await this.client.query(sql, values, {
rowMode: 'array',
})
return ok(result)
} catch (e) {
const error = e as Error
debug('Error in performIO: %O', error)
if (e && typeof e.code === 'string' && typeof e.severity === 'string' && typeof e.message === 'string') {
return err({
kind: 'Postgres',
code: e.code,
severity: e.severity,
message: e.message,
detail: e.detail,
column: e.column,
hint: e.hint,
})
}
throw error
}
}
}

class PgLiteTransaction extends PgLiteQueryable<PGlite> implements Transaction {
constructor(client: PGlite, readonly options: TransactionOptions) {
super(client)
}

async commit(): Promise<Result<void>> {
debug(`[js::commit]`)
return ok(undefined)
}

async rollback(): Promise<Result<void>> {
debug(`[js::rollback]`)
return ok(undefined)
}
}

export type PrismaPgLiteOptions = {
schema?: string
}

export class PrismaPgLite extends PgLiteQueryable<PGlite> implements DriverAdapter {
constructor(client: PGlite, private options?: PrismaPgLiteOptions) {
if (!(client instanceof PGlite)) {
throw new TypeError(`PrismaPgLite must be initialized with an instance of PGlite`)
}
super(client)
}

getConnectionInfo(): Result<ConnectionInfo> {
return ok({
schemaName: this.options?.schema,
})
}

async startTransaction(): Promise<Result<Transaction>> {
const options: TransactionOptions = {
usePhantomQuery: false,
}

const tag = '[js::startTransaction]'
debug(`${tag} options: %O`, options)

return ok(new PgLiteTransaction(this.client, options))
}
}
7 changes: 7 additions & 0 deletions packages/adapter-pg-lite/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.build.regular.json",
"compilerOptions": {
"outDir": "declaration"
},
"include": ["src"]
}
3 changes: 3 additions & 0 deletions packages/adapter-pg-lite/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}
2 changes: 1 addition & 1 deletion packages/driver-adapter-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const officialPrismaAdapters = [
] as const

export interface Queryable {
readonly provider: 'mysql' | 'postgres' | 'sqlite'
readonly provider: 'mysql' | 'postgres' | 'sqlite' | 'pglite'
readonly adapterName: (typeof officialPrismaAdapters)[number] | (string & {})

/**
Expand Down