prisma-pglite-bridge 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +288 -124
- package/dist/index.cjs +324 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +138 -71
- package/dist/index.d.mts +138 -71
- package/dist/index.mjs +320 -153
- package/dist/index.mjs.map +1 -1
- package/dist/ppb.mjs +1456 -0
- package/dist/ppb.mjs.map +1 -0
- package/package.json +26 -17
package/README.md
CHANGED
|
@@ -19,17 +19,16 @@ TypeScript users also need `@types/pg`.
|
|
|
19
19
|
|
|
20
20
|
```typescript
|
|
21
21
|
import { PGlite } from '@electric-sql/pglite';
|
|
22
|
-
import {
|
|
22
|
+
import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
|
|
23
23
|
import { PrismaClient } from '@prisma/client';
|
|
24
24
|
|
|
25
25
|
const pglite = new PGlite();
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
migrationsPath: './prisma/migrations',
|
|
29
|
-
});
|
|
30
|
-
const prisma = new PrismaClient({ adapter });
|
|
26
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
27
|
+
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
const prisma = new PrismaClient({ adapter: bridge.adapter });
|
|
30
|
+
|
|
31
|
+
beforeEach(() => bridge.resetDb());
|
|
33
32
|
```
|
|
34
33
|
|
|
35
34
|
Call `resetDb()` in `beforeEach` to wipe all data between tests.
|
|
@@ -40,34 +39,31 @@ That's it. Run `prisma migrate dev` first to generate migration
|
|
|
40
39
|
files. No Docker, no database server — works in GitHub Actions,
|
|
41
40
|
GitLab CI, and any environment where Node.js runs.
|
|
42
41
|
|
|
43
|
-
##
|
|
42
|
+
## Populating the database
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
`createPGliteBridge` returns an empty database. The bridge offers
|
|
45
|
+
two helpers to populate it — pick the one that matches your
|
|
46
|
+
project layout:
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
migration files (same resolution as `prisma migrate dev`),
|
|
53
|
-
triggered by passing `configRoot`. Requires `prisma` to be
|
|
54
|
-
installed (which provides `@prisma/config` as a transitive
|
|
55
|
-
dependency).
|
|
48
|
+
| Helper | When to use | Cost |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| [`pushMigrations`](#pushmigrationstarget-options) | You already run `prisma migrate dev` and have a `prisma/migrations` directory | No WASM schema engine; no Node `ExperimentalWarning` |
|
|
51
|
+
| [`pushSchema`](#pushschematarget-options) | You only have `schema.prisma` (test fixtures, prototypes) and want it applied via `prisma db push` semantics | Loads `@prisma/schema-engine-wasm` once per process |
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
Both accept the same `PGliteBridge` returned by
|
|
54
|
+
`createPGliteBridge`, so you can swap helpers without touching the
|
|
55
|
+
bridge wiring. If you reopen a persistent `dataDir` that already
|
|
56
|
+
holds the schema, call neither.
|
|
60
57
|
|
|
61
|
-
Schema SQL
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
writable only by trusted processes.
|
|
58
|
+
Schema SQL is executed verbatim with no checksum or signature
|
|
59
|
+
verification. Compose it from trusted, version-controlled source
|
|
60
|
+
only — never from environment variables, network input, or any
|
|
61
|
+
value that crosses a trust boundary, and keep the migrations
|
|
62
|
+
directory writable only by trusted processes.
|
|
67
63
|
|
|
68
64
|
## Bridge fs-sync policy
|
|
69
65
|
|
|
70
|
-
The
|
|
66
|
+
The bridge defaults `syncToFs` to `'auto'`:
|
|
71
67
|
|
|
72
68
|
- in-memory PGlite (`new PGlite()` or `memory://...`) resolves to `false`
|
|
73
69
|
- persistent `dataDir` usage resolves to `true`
|
|
@@ -75,31 +71,36 @@ The adapter defaults `syncToFs` to `'auto'`:
|
|
|
75
71
|
That keeps bridge-heavy test workloads on the lower-memory fast path
|
|
76
72
|
without changing durability defaults for persistent databases.
|
|
77
73
|
If you use a custom `fs`, set `syncToFs` explicitly because the
|
|
78
|
-
|
|
74
|
+
bridge cannot infer whether that storage is durable.
|
|
79
75
|
|
|
80
76
|
## API
|
|
81
77
|
|
|
82
|
-
### `
|
|
78
|
+
### `createPGliteBridge(options)`
|
|
83
79
|
|
|
84
|
-
Creates a
|
|
85
|
-
instance
|
|
80
|
+
Creates a `PGliteBridge` — a bundle holding a Prisma driver adapter,
|
|
81
|
+
the underlying PGlite instance, and lifecycle helpers — backed by a
|
|
82
|
+
caller-supplied PGlite instance.
|
|
86
83
|
|
|
87
84
|
```typescript
|
|
88
85
|
const pglite = new PGlite(/* dataDir, extensions, ... */);
|
|
89
86
|
|
|
90
|
-
const
|
|
91
|
-
pglite,
|
|
92
|
-
|
|
93
|
-
sql: 'CREATE TABLE ...', // (first match wins, see Schema Resolution)
|
|
94
|
-
configRoot: '../..', // monorepo: where to find prisma.config.ts
|
|
95
|
-
max: 1, // pool connections (default: 1, see "Pool sizing" below)
|
|
87
|
+
const bridge = await createPGliteBridge({
|
|
88
|
+
pglite, // required — caller owns lifecycle
|
|
89
|
+
max: 1, // pool connections (default: 1)
|
|
96
90
|
statsLevel: 'off', // 'off' | 'basic' | 'full' (default: 'off')
|
|
91
|
+
syncToFs: 'auto', // 'auto' | true | false (default: 'auto')
|
|
97
92
|
});
|
|
98
93
|
```
|
|
99
94
|
|
|
100
|
-
|
|
95
|
+
To apply schema SQL, call [`pushMigrations`](#pushmigrationstarget-options)
|
|
96
|
+
or [`pushSchema`](#pushschematarget-options) on the returned
|
|
97
|
+
bridge — see [Populating the database](#populating-the-database).
|
|
98
|
+
|
|
99
|
+
Returns a `PGliteBridge`:
|
|
101
100
|
|
|
102
101
|
- `adapter` — pass to `new PrismaClient({ adapter })`
|
|
102
|
+
- `pglite` — the caller-supplied PGlite instance, re-exposed for
|
|
103
|
+
symmetry with `pushSchema` / `pushMigrations`
|
|
103
104
|
- `resetDb()` — truncates all user tables and discards
|
|
104
105
|
session-local state via `DISCARD ALL` (for example `SET`
|
|
105
106
|
variables, prepared statements, temp tables, and `LISTEN`
|
|
@@ -112,16 +113,101 @@ Returns:
|
|
|
112
113
|
the pool is released promptly and leak warnings do not fire.
|
|
113
114
|
- `stats()` — returns telemetry when `statsLevel` is `'basic'` or
|
|
114
115
|
`'full'`, else `undefined`. See [Stats collection](#stats-collection).
|
|
115
|
-
- `
|
|
116
|
+
- `bridgeId` — a unique `symbol` identifying this bridge. Use it
|
|
116
117
|
to filter events from the public
|
|
117
118
|
[diagnostics channels](#diagnostics-channels) when multiple
|
|
118
|
-
|
|
119
|
+
bridges share a process.
|
|
119
120
|
- `snapshotDb()` — captures the current DB contents into an internal
|
|
120
121
|
snapshot so later `resetDb()` calls restore to that state instead of
|
|
121
122
|
truncating to empty.
|
|
122
123
|
- `resetSnapshot()` — discards the current snapshot so later
|
|
123
124
|
`resetDb()` calls truncate back to empty again.
|
|
124
125
|
|
|
126
|
+
### `pushMigrations(target, options)`
|
|
127
|
+
|
|
128
|
+
Applies SQL migrations to a `PGliteBridge`. Use this for projects
|
|
129
|
+
that already have a `prisma/migrations` directory generated by
|
|
130
|
+
`prisma migrate dev`. Does not load `@prisma/schema-engine-wasm`.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
|
|
134
|
+
|
|
135
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
136
|
+
|
|
137
|
+
// inline SQL
|
|
138
|
+
await pushMigrations(bridge, { sql: 'CREATE TABLE ...' });
|
|
139
|
+
|
|
140
|
+
// from a migrations directory
|
|
141
|
+
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
142
|
+
|
|
143
|
+
// auto-discovered via prisma.config.ts (monorepo: pass configRoot)
|
|
144
|
+
await pushMigrations(bridge, { configRoot: process.cwd() });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Resolution order — first match wins:
|
|
148
|
+
|
|
149
|
+
1. **`sql` option** — pre-generated SQL string, applied directly
|
|
150
|
+
2. **`migrationsPath` option** — concatenates every
|
|
151
|
+
`migration.sql` found one directory level below, in
|
|
152
|
+
directory-name order
|
|
153
|
+
3. **Auto-discovered migrations** — uses `@prisma/config` to find
|
|
154
|
+
migration files (same resolution as `prisma migrate dev`),
|
|
155
|
+
triggered by passing `configRoot`. Requires `prisma` to be
|
|
156
|
+
installed (which provides `@prisma/config` as a transitive
|
|
157
|
+
dependency).
|
|
158
|
+
|
|
159
|
+
Returns `{ durationMs }` — the wall-clock time PGlite spent applying
|
|
160
|
+
the SQL, useful when you want to log schema-setup cost.
|
|
161
|
+
|
|
162
|
+
### `pushSchema(target, options)`
|
|
163
|
+
|
|
164
|
+
Applies a Prisma schema to the database via
|
|
165
|
+
`@prisma/schema-engine-wasm`, in-process. No native schema-engine
|
|
166
|
+
binary, no TCP, no Docker. `prisma generate` still goes through the
|
|
167
|
+
regular CLI; only schema apply / reset is bridged.
|
|
168
|
+
|
|
169
|
+
Use this for projects without a migrations directory — typical of
|
|
170
|
+
test fixtures or quick prototypes — or when you want `prisma db
|
|
171
|
+
push` semantics (diff `schema.prisma` against the live DB).
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { readFile } from 'node:fs/promises';
|
|
175
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
176
|
+
import { createPGliteBridge, pushSchema } from 'prisma-pglite-bridge';
|
|
177
|
+
|
|
178
|
+
const pglite = new PGlite();
|
|
179
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
180
|
+
|
|
181
|
+
await pushSchema(bridge, {
|
|
182
|
+
schema: await readFile('prisma/schema.prisma', 'utf8'),
|
|
183
|
+
// acceptDataLoss: true, // apply destructive changes flagged as warnings
|
|
184
|
+
// forceReset: true, // drop every non-system schema before applying
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Returns `{ executedSteps, warnings, unexecutable }`.
|
|
189
|
+
`acceptDataLoss: true` lets the engine apply destructive changes
|
|
190
|
+
that would otherwise be reported as warnings; `unexecutable` steps
|
|
191
|
+
are independent — the engine refuses them either way and the
|
|
192
|
+
caller must reshape the schema. `forceReset: true` drops every
|
|
193
|
+
non-system schema before applying.
|
|
194
|
+
|
|
195
|
+
The first call in a Node process emits an
|
|
196
|
+
[`ExperimentalWarning`](#experimentalwarning-importing-webassembly-module-instances-is-an-experimental-feature)
|
|
197
|
+
about WebAssembly imports.
|
|
198
|
+
|
|
199
|
+
### `resetSchema(target)`
|
|
200
|
+
|
|
201
|
+
Drops every non-system schema and recreates `public`. Useful as a
|
|
202
|
+
between-suite reset when you want a clean slate without re-running
|
|
203
|
+
all migrations.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { resetSchema } from 'prisma-pglite-bridge';
|
|
207
|
+
|
|
208
|
+
await resetSchema(bridge);
|
|
209
|
+
```
|
|
210
|
+
|
|
125
211
|
### `createPool(options)`
|
|
126
212
|
|
|
127
213
|
Lower-level escape hatch. Creates a `pg.Pool` backed by PGlite
|
|
@@ -138,22 +224,22 @@ const { pool, close } = await createPool({ pglite });
|
|
|
138
224
|
const adapter = new PrismaPg(pool);
|
|
139
225
|
```
|
|
140
226
|
|
|
141
|
-
Returns `pool` (pg.Pool), `
|
|
227
|
+
Returns `pool` (pg.Pool), `bridgeId` (a unique `symbol` for
|
|
142
228
|
[diagnostics channel](#diagnostics-channels) filtering), and
|
|
143
229
|
`close()` (which shuts down the pool only — the caller-supplied
|
|
144
230
|
PGlite instance is not closed). Accepts `pglite` (required),
|
|
145
|
-
`max`, `
|
|
231
|
+
`max`, `bridgeId`, and `syncToFs`.
|
|
146
232
|
|
|
147
|
-
### `
|
|
233
|
+
### `PGliteDuplex`
|
|
148
234
|
|
|
149
235
|
The Duplex stream that replaces `pg.Client`'s network socket.
|
|
150
236
|
Exported for advanced use cases (custom `pg.Client` setup, direct
|
|
151
|
-
wire protocol access). When using multiple
|
|
152
|
-
same PGlite instance, pass a shared `SessionLock` to prevent
|
|
237
|
+
wire protocol access). When using multiple duplex streams against
|
|
238
|
+
the same PGlite instance, pass a shared `SessionLock` to prevent
|
|
153
239
|
transaction interleaving.
|
|
154
240
|
|
|
155
241
|
```typescript
|
|
156
|
-
import {
|
|
242
|
+
import { PGliteDuplex, SessionLock } from 'prisma-pglite-bridge';
|
|
157
243
|
import { PGlite } from '@electric-sql/pglite';
|
|
158
244
|
import pg from 'pg';
|
|
159
245
|
|
|
@@ -162,10 +248,51 @@ await pglite.waitReady;
|
|
|
162
248
|
|
|
163
249
|
const lock = new SessionLock();
|
|
164
250
|
const client = new pg.Client({
|
|
165
|
-
stream: () => new
|
|
251
|
+
stream: () => new PGliteDuplex(pglite, lock),
|
|
166
252
|
});
|
|
167
253
|
```
|
|
168
254
|
|
|
255
|
+
### `SessionLock`
|
|
256
|
+
|
|
257
|
+
An async mutex that serializes PGlite access across multiple
|
|
258
|
+
duplex streams sharing one PGlite instance. `createPGliteBridge`
|
|
259
|
+
and `createPool` install one automatically; export it for custom
|
|
260
|
+
multi-duplex setups built on top of `PGliteDuplex`.
|
|
261
|
+
|
|
262
|
+
### Diagnostics channel exports
|
|
263
|
+
|
|
264
|
+
`QUERY_CHANNEL`, `LOCK_WAIT_CHANNEL`, and the matching
|
|
265
|
+
`QueryEvent` / `LockWaitEvent` types are exported for subscribing
|
|
266
|
+
to live per-query and per-lock-wait events. See
|
|
267
|
+
[Diagnostics channels](#diagnostics-channels) for the wiring.
|
|
268
|
+
|
|
269
|
+
## CLI (`ppb`)
|
|
270
|
+
|
|
271
|
+
The `ppb` CLI exposes [`pushSchema`](#pushschematarget-options) and
|
|
272
|
+
[`resetSchema`](#resetschematarget) as standalone commands so you
|
|
273
|
+
can apply a Prisma schema to a PGlite database without writing
|
|
274
|
+
glue code.
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
pnpm exec ppb db-push [--schema <path>] # default: prisma/schema.prisma
|
|
278
|
+
[--force-reset]
|
|
279
|
+
[--accept-data-loss]
|
|
280
|
+
[--data-dir <path>] # overrides DATABASE_URL
|
|
281
|
+
pnpm exec ppb db-reset [--data-dir <path>]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`DATABASE_URL` is read from env / `.env` and parsed as a `pglite://`
|
|
285
|
+
URL — `pglite://memory` for in-memory, `pglite:///abs/path` or
|
|
286
|
+
`pglite://./rel/path` for filesystem-backed PGlite. `--data-dir`
|
|
287
|
+
overrides it.
|
|
288
|
+
|
|
289
|
+
Exit codes:
|
|
290
|
+
|
|
291
|
+
- **0** — success.
|
|
292
|
+
- **1** — engine reported `unexecutable` steps, or `warnings` were
|
|
293
|
+
reported and `--accept-data-loss` was not supplied, or the
|
|
294
|
+
schema failed to parse / push.
|
|
295
|
+
|
|
169
296
|
## Examples
|
|
170
297
|
|
|
171
298
|
### Replacing your production database in tests
|
|
@@ -189,20 +316,18 @@ the in-memory PGlite version:
|
|
|
189
316
|
```typescript
|
|
190
317
|
// vitest.setup.ts
|
|
191
318
|
import { PGlite } from '@electric-sql/pglite';
|
|
192
|
-
import {
|
|
319
|
+
import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
|
|
193
320
|
import { PrismaClient } from '@prisma/client';
|
|
194
321
|
import { beforeEach, vi } from 'vitest';
|
|
195
322
|
|
|
196
323
|
const pglite = new PGlite();
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
});
|
|
201
|
-
export const testPrisma = new PrismaClient({ adapter });
|
|
324
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
325
|
+
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
326
|
+
export const testPrisma = new PrismaClient({ adapter: bridge.adapter });
|
|
202
327
|
|
|
203
328
|
vi.mock('./lib/prisma', () => ({ prisma: testPrisma }));
|
|
204
329
|
|
|
205
|
-
beforeEach(() => resetDb());
|
|
330
|
+
beforeEach(() => bridge.resetDb());
|
|
206
331
|
```
|
|
207
332
|
|
|
208
333
|
```typescript
|
|
@@ -225,7 +350,7 @@ the top level, not inside `beforeAll`:
|
|
|
225
350
|
```typescript
|
|
226
351
|
// jest.setup.ts
|
|
227
352
|
const { PGlite } = require('@electric-sql/pglite');
|
|
228
|
-
const {
|
|
353
|
+
const { createPGliteBridge, pushMigrations } = require('prisma-pglite-bridge');
|
|
229
354
|
const { PrismaClient } = require('@prisma/client');
|
|
230
355
|
|
|
231
356
|
let testPrisma;
|
|
@@ -237,12 +362,10 @@ jest.mock('./lib/prisma', () => ({
|
|
|
237
362
|
|
|
238
363
|
beforeAll(async () => {
|
|
239
364
|
const pglite = new PGlite();
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
testPrisma = new PrismaClient({ adapter: result.adapter });
|
|
245
|
-
resetDb = result.resetDb;
|
|
365
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
366
|
+
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
367
|
+
testPrisma = new PrismaClient({ adapter: bridge.adapter });
|
|
368
|
+
resetDb = bridge.resetDb;
|
|
246
369
|
});
|
|
247
370
|
|
|
248
371
|
beforeEach(() => resetDb());
|
|
@@ -254,7 +377,7 @@ If your code accepts `PrismaClient` as a parameter:
|
|
|
254
377
|
|
|
255
378
|
```typescript
|
|
256
379
|
import { PGlite } from '@electric-sql/pglite';
|
|
257
|
-
import {
|
|
380
|
+
import { createPGliteBridge, pushMigrations, type ResetDbFn } from 'prisma-pglite-bridge';
|
|
258
381
|
import { PrismaClient } from '@prisma/client';
|
|
259
382
|
import { beforeAll, beforeEach, it, expect } from 'vitest';
|
|
260
383
|
|
|
@@ -263,12 +386,10 @@ let resetDb: ResetDbFn;
|
|
|
263
386
|
|
|
264
387
|
beforeAll(async () => {
|
|
265
388
|
const pglite = new PGlite();
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
prisma = new PrismaClient({ adapter: result.adapter });
|
|
271
|
-
resetDb = result.resetDb;
|
|
389
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
390
|
+
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
391
|
+
prisma = new PrismaClient({ adapter: bridge.adapter });
|
|
392
|
+
resetDb = bridge.resetDb;
|
|
272
393
|
});
|
|
273
394
|
|
|
274
395
|
beforeEach(() => resetDb());
|
|
@@ -306,7 +427,7 @@ Then reuse it in tests:
|
|
|
306
427
|
|
|
307
428
|
```typescript
|
|
308
429
|
import { PGlite } from '@electric-sql/pglite';
|
|
309
|
-
import {
|
|
430
|
+
import { createPGliteBridge, pushMigrations, type ResetDbFn } from 'prisma-pglite-bridge';
|
|
310
431
|
import { PrismaClient } from '@prisma/client';
|
|
311
432
|
import { seed } from '../prisma/seed';
|
|
312
433
|
|
|
@@ -315,12 +436,10 @@ let resetDb: ResetDbFn;
|
|
|
315
436
|
|
|
316
437
|
beforeAll(async () => {
|
|
317
438
|
const pglite = new PGlite();
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
prisma = new PrismaClient({ adapter: result.adapter });
|
|
323
|
-
resetDb = result.resetDb;
|
|
439
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
440
|
+
await pushMigrations(result, { migrationsPath: './prisma/migrations' });
|
|
441
|
+
prisma = new PrismaClient({ adapter: bridge.adapter });
|
|
442
|
+
resetDb = bridge.resetDb;
|
|
324
443
|
await seed(prisma);
|
|
325
444
|
});
|
|
326
445
|
|
|
@@ -331,6 +450,23 @@ beforeEach(async () => {
|
|
|
331
450
|
});
|
|
332
451
|
```
|
|
333
452
|
|
|
453
|
+
### Applying a schema directly (no migrations directory)
|
|
454
|
+
|
|
455
|
+
For test fixtures or prototypes without `prisma/migrations`, swap
|
|
456
|
+
`pushMigrations` for `pushSchema`:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { readFile } from 'node:fs/promises';
|
|
460
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
461
|
+
import { createPGliteBridge, pushSchema } from 'prisma-pglite-bridge';
|
|
462
|
+
|
|
463
|
+
const pglite = new PGlite();
|
|
464
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
465
|
+
await pushSchema(bridge, {
|
|
466
|
+
schema: await readFile('prisma/schema.prisma', 'utf8'),
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
334
470
|
### Using PostgreSQL extensions
|
|
335
471
|
|
|
336
472
|
If your schema uses `uuid-ossp`, `pgcrypto`, or other extensions,
|
|
@@ -338,15 +474,13 @@ pass them via the `extensions` option:
|
|
|
338
474
|
|
|
339
475
|
```typescript
|
|
340
476
|
import { PGlite } from '@electric-sql/pglite';
|
|
341
|
-
import {
|
|
477
|
+
import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
|
|
342
478
|
import { uuid_ossp } from '@electric-sql/pglite/contrib/uuid_ossp';
|
|
343
479
|
import { pgcrypto } from '@electric-sql/pglite/contrib/pgcrypto';
|
|
344
480
|
|
|
345
481
|
const pglite = new PGlite({ extensions: { uuid_ossp, pgcrypto } });
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
migrationsPath: './prisma/migrations',
|
|
349
|
-
});
|
|
482
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
483
|
+
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
350
484
|
```
|
|
351
485
|
|
|
352
486
|
Extensions are included in the `@electric-sql/pglite` package —
|
|
@@ -355,18 +489,18 @@ for the full list.
|
|
|
355
489
|
|
|
356
490
|
### Pre-generated SQL (fastest)
|
|
357
491
|
|
|
358
|
-
The `sql` option runs verbatim with no sandbox
|
|
359
|
-
it from trusted, version-controlled source
|
|
360
|
-
environment variables, network input, or values
|
|
361
|
-
|
|
362
|
-
full source-of-trust guidance.
|
|
492
|
+
The `sql` option on `pushMigrations` runs verbatim with no sandbox
|
|
493
|
+
or checksum. Compose it from trusted, version-controlled source
|
|
494
|
+
only — never from environment variables, network input, or values
|
|
495
|
+
that cross a trust boundary.
|
|
363
496
|
|
|
364
497
|
```typescript
|
|
365
498
|
import { PGlite } from '@electric-sql/pglite';
|
|
499
|
+
import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
|
|
366
500
|
|
|
367
501
|
const pglite = new PGlite();
|
|
368
|
-
const
|
|
369
|
-
|
|
502
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
503
|
+
await pushMigrations(bridge, {
|
|
370
504
|
sql: `
|
|
371
505
|
CREATE TABLE "User" (id text PRIMARY KEY, name text NOT NULL);
|
|
372
506
|
CREATE TABLE "Post" (
|
|
@@ -395,11 +529,9 @@ const dataDir = './data/pglite';
|
|
|
395
529
|
const firstRun = !existsSync(join(dataDir, 'PG_VERSION'));
|
|
396
530
|
|
|
397
531
|
const pglite = new PGlite(dataDir);
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
});
|
|
402
|
-
const prisma = new PrismaClient({ adapter });
|
|
532
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
533
|
+
if (firstRun) await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
534
|
+
const prisma = new PrismaClient({ adapter: bridge.adapter });
|
|
403
535
|
```
|
|
404
536
|
|
|
405
537
|
**Add `data/pglite/` to `.gitignore`.** Delete the data directory
|
|
@@ -411,19 +543,18 @@ or environments where installing PostgreSQL is impractical.
|
|
|
411
543
|
|
|
412
544
|
```typescript
|
|
413
545
|
import { PGlite } from '@electric-sql/pglite';
|
|
546
|
+
import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
|
|
414
547
|
|
|
415
548
|
const pglite = new PGlite();
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
});
|
|
420
|
-
const prisma = new PrismaClient({ adapter });
|
|
549
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
550
|
+
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
551
|
+
const prisma = new PrismaClient({ adapter: bridge.adapter });
|
|
421
552
|
|
|
422
553
|
try {
|
|
423
554
|
await seedDatabase(prisma);
|
|
424
555
|
} finally {
|
|
425
556
|
await prisma.$disconnect();
|
|
426
|
-
await close();
|
|
557
|
+
await bridge.close();
|
|
427
558
|
await pglite.close();
|
|
428
559
|
}
|
|
429
560
|
```
|
|
@@ -433,7 +564,7 @@ try {
|
|
|
433
564
|
For most developers, this is the easiest way to see how the bridge
|
|
434
565
|
performed in tests.
|
|
435
566
|
|
|
436
|
-
Enable `statsLevel` when creating the
|
|
567
|
+
Enable `statsLevel` when creating the bridge, run your tests, then
|
|
437
568
|
call `await stats()` at the end. You get one snapshot with the main
|
|
438
569
|
things you usually care about: query counts, timing percentiles,
|
|
439
570
|
database size, and, at `'full'`, process RSS and session-lock wait
|
|
@@ -448,19 +579,20 @@ external consumer subscribes to the public
|
|
|
448
579
|
|
|
449
580
|
```typescript
|
|
450
581
|
import { PGlite } from '@electric-sql/pglite';
|
|
582
|
+
import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
|
|
451
583
|
|
|
452
584
|
const pglite = new PGlite();
|
|
453
|
-
const
|
|
585
|
+
const bridge = await createPGliteBridge({
|
|
454
586
|
pglite,
|
|
455
|
-
migrationsPath: './prisma/migrations',
|
|
456
587
|
statsLevel: 'basic', // or 'full'
|
|
457
588
|
});
|
|
458
|
-
|
|
589
|
+
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
590
|
+
const prisma = new PrismaClient({ adapter: bridge.adapter });
|
|
459
591
|
|
|
460
592
|
afterAll(async () => {
|
|
461
593
|
await prisma.$disconnect();
|
|
462
|
-
await close();
|
|
463
|
-
const s = await stats();
|
|
594
|
+
await bridge.close();
|
|
595
|
+
const s = await bridge.stats();
|
|
464
596
|
if (s) console.log(s);
|
|
465
597
|
await pglite.close();
|
|
466
598
|
});
|
|
@@ -479,9 +611,8 @@ described below. That path is more flexible, but also more advanced.
|
|
|
479
611
|
|
|
480
612
|
**`'basic'`** — timing and counters:
|
|
481
613
|
|
|
482
|
-
- `durationMs` —
|
|
614
|
+
- `durationMs` — bridge lifetime (frozen at `close()`, drain
|
|
483
615
|
excluded)
|
|
484
|
-
- `schemaSetupMs` — one-time cost of applying migration SQL
|
|
485
616
|
- `queryCount`, `failedQueryCount` — WASM round-trips (a Prisma
|
|
486
617
|
extended-query pipeline is one round-trip, not five). Lifetime
|
|
487
618
|
counters.
|
|
@@ -489,7 +620,7 @@ described below. That path is more flexible, but also more advanced.
|
|
|
489
620
|
durations
|
|
490
621
|
- `recentP50QueryMs`, `recentP95QueryMs`, `recentMaxQueryMs` —
|
|
491
622
|
nearest-rank percentiles (no interpolation) over the most recent
|
|
492
|
-
~10,000 queries. On long-lived
|
|
623
|
+
~10,000 queries. On long-lived bridges these describe a different
|
|
493
624
|
population than `avgQueryMs`.
|
|
494
625
|
- `resetDbCalls` — counts `resetDb()` attempts
|
|
495
626
|
- `dbSizeBytes` — `pg_database_size(current_database())`, cached
|
|
@@ -517,7 +648,7 @@ rejects, `processRssPeakBytes` on runtimes without
|
|
|
517
648
|
|
|
518
649
|
The bridge publishes per-query and per-lock-wait events to
|
|
519
650
|
[`node:diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html)
|
|
520
|
-
channels. Built-in
|
|
651
|
+
channels. Built-in bridge stats are updated directly by the bridge
|
|
521
652
|
when `statsLevel` is `'basic'` or `'full'`; external consumers (OpenTelemetry, APM,
|
|
522
653
|
custom loggers) can subscribe directly without touching the bridge
|
|
523
654
|
API.
|
|
@@ -529,16 +660,16 @@ Subscribing opts you in to that work.
|
|
|
529
660
|
```typescript
|
|
530
661
|
import diagnostics_channel from 'node:diagnostics_channel';
|
|
531
662
|
import {
|
|
532
|
-
|
|
663
|
+
createPGliteBridge,
|
|
533
664
|
QUERY_CHANNEL,
|
|
534
665
|
type QueryEvent,
|
|
535
666
|
} from 'prisma-pglite-bridge';
|
|
536
667
|
|
|
537
|
-
const {
|
|
668
|
+
const { bridgeId } = await createPGliteBridge({ /* ... */ });
|
|
538
669
|
|
|
539
670
|
const listener = (msg: unknown) => {
|
|
540
671
|
const e = msg as QueryEvent;
|
|
541
|
-
if (e.
|
|
672
|
+
if (e.bridgeId !== bridgeId) return;
|
|
542
673
|
myMetrics.record('db.query', e.durationMs, { ok: e.succeeded });
|
|
543
674
|
};
|
|
544
675
|
diagnostics_channel.channel(QUERY_CHANNEL).subscribe(listener);
|
|
@@ -547,33 +678,37 @@ diagnostics_channel.channel(QUERY_CHANNEL).subscribe(listener);
|
|
|
547
678
|
Channels:
|
|
548
679
|
|
|
549
680
|
- `QUERY_CHANNEL` (`prisma-pglite-bridge:query`) — every
|
|
550
|
-
whole-query boundary. Payload: `{
|
|
681
|
+
whole-query boundary. Payload: `{ bridgeId: symbol; durationMs:
|
|
551
682
|
number; succeeded: boolean }`. `succeeded` is `false` for both
|
|
552
683
|
thrown errors and protocol-level `ErrorResponse` frames.
|
|
553
684
|
- `LOCK_WAIT_CHANNEL` (`prisma-pglite-bridge:lock-wait`) — every
|
|
554
|
-
session-lock acquisition. Payload: `{
|
|
685
|
+
session-lock acquisition. Payload: `{ bridgeId: symbol;
|
|
555
686
|
durationMs: number }`. `durationMs` is how long the acquirer
|
|
556
687
|
waited before the lock was granted.
|
|
557
688
|
|
|
558
|
-
Filter on `
|
|
559
|
-
share a process. Obtain it from the `
|
|
689
|
+
Filter on `bridgeId` to isolate events when multiple bridges
|
|
690
|
+
share a process. Obtain it from the `createPGliteBridge()` or
|
|
560
691
|
`createPool()` return value.
|
|
561
692
|
|
|
562
693
|
## Limitations
|
|
563
694
|
|
|
564
695
|
- **Node.js 20+ only** — requires `node:stream` and `node:fs`.
|
|
565
696
|
Does not work in browsers despite PGlite's browser support.
|
|
566
|
-
- **WASM cold start** — first `
|
|
697
|
+
- **WASM cold start** — first `createPGliteBridge()` call takes
|
|
567
698
|
~2s for PGlite WASM compilation. Subsequent calls in the same
|
|
568
699
|
process reuse the compiled module.
|
|
569
700
|
- **Single PostgreSQL session** — PGlite runs in single-user mode.
|
|
570
|
-
All pool connections share one session.
|
|
571
|
-
transactions (one at a time), but `SET`
|
|
572
|
-
connections within a single test. `resetDb()`
|
|
573
|
-
between tests via `DISCARD ALL`.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
`
|
|
701
|
+
All pool connections share one session. With `max > 1`, a
|
|
702
|
+
`SessionLock` serializes transactions (one at a time), but `SET`
|
|
703
|
+
variables leak between connections within a single test. `resetDb()`
|
|
704
|
+
clears more of this between tests via `DISCARD ALL`. The default
|
|
705
|
+
`max: 1` avoids extra bridge connections and session-lock overhead.
|
|
706
|
+
- **Schema source required** — pick one of
|
|
707
|
+
[`pushMigrations`](#pushmigrationstarget-options) (run
|
|
708
|
+
`prisma migrate dev` first or pass `sql` directly) or
|
|
709
|
+
[`pushSchema`](#pushschematarget-options) (apply
|
|
710
|
+
`schema.prisma` directly). `createPGliteBridge` alone returns
|
|
711
|
+
an empty database.
|
|
577
712
|
|
|
578
713
|
## Troubleshooting
|
|
579
714
|
|
|
@@ -605,6 +740,35 @@ If you see more than one version, force a single 0.4.x via
|
|
|
605
740
|
|
|
606
741
|
Then `pnpm install`.
|
|
607
742
|
|
|
743
|
+
### `ExperimentalWarning: Importing WebAssembly module instances is an experimental feature`
|
|
744
|
+
|
|
745
|
+
Emitted by Node when `pushSchema` / `resetSchema` (or the `ppb` CLI)
|
|
746
|
+
loads `@prisma/schema-engine-wasm`, which uses ESM static `.wasm`
|
|
747
|
+
imports. The warning is harmless and prints once per Node process.
|
|
748
|
+
|
|
749
|
+
If you only need to apply already-generated migration SQL, use
|
|
750
|
+
[`pushMigrations`](#pushmigrationstarget-options) instead — it does
|
|
751
|
+
not load the schema engine, so the warning never fires.
|
|
752
|
+
|
|
753
|
+
To silence it in tests or CI, pass Node's `--disable-warning` flag:
|
|
754
|
+
|
|
755
|
+
```sh
|
|
756
|
+
NODE_OPTIONS=--disable-warning=ExperimentalWarning pnpm test
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
Or scope it to Vitest workers via `vitest.config.ts`:
|
|
760
|
+
|
|
761
|
+
```ts
|
|
762
|
+
export default defineConfig({
|
|
763
|
+
test: {
|
|
764
|
+
execArgv: ['--disable-warning=ExperimentalWarning'],
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
Requires Node ≥ 22. The warning will go away once Node stabilizes
|
|
770
|
+
WebAssembly ESM imports.
|
|
771
|
+
|
|
608
772
|
## License
|
|
609
773
|
|
|
610
774
|
MIT
|