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 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 { createPgliteAdapter } from 'prisma-pglite-bridge';
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 { adapter, resetDb } = await createPgliteAdapter({
27
- pglite,
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
- beforeEach(() => resetDb());
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
- ## Schema Resolution
42
+ ## Populating the database
44
43
 
45
- When you pass any of `sql`, `migrationsPath`, or `configRoot`,
46
- `createPgliteAdapter` applies schema SQL. Resolution order:
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
- 1. **`sql` option** pre-generated SQL string, applied directly
49
- 2. **`migrationsPath` option** reads migration files from the
50
- given directory
51
- 3. **Auto-discovered migrations** uses `@prisma/config` to find
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
- When none of these options is provided, no SQL is applied — the
58
- PGlite instance is assumed to already hold the schema (useful
59
- for reopening a persistent `dataDir`).
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 whether inline via `sql` or loaded from `migrationsPath`
62
- is executed verbatim with no checksum or signature verification.
63
- Ensure the source is trusted and version-controlled. Do not compose
64
- it from environment variables, network input, or any value that
65
- crosses a trust boundary, and keep the migrations directory
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 adapter defaults `syncToFs` to `'auto'`:
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
- adapter cannot infer whether that storage is durable.
74
+ bridge cannot infer whether that storage is durable.
79
75
 
80
76
  ## API
81
77
 
82
- ### `createPgliteAdapter(options)`
78
+ ### `createPGliteBridge(options)`
83
79
 
84
- Creates a Prisma adapter backed by a caller-supplied PGlite
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 { adapter, resetDb, close, stats } = await createPgliteAdapter({
91
- pglite, // required — caller owns lifecycle
92
- migrationsPath: './prisma/migrations', // or:
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
- Returns:
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
- - `adapterId` — a unique `symbol` identifying this adapter. Use it
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
- adapters share a process.
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), `adapterId` (a unique `symbol` for
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`, `adapterId`, and `syncToFs`.
231
+ `max`, `bridgeId`, and `syncToFs`.
146
232
 
147
- ### `PGliteBridge`
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 bridges against the
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 { PGliteBridge, SessionLock } from 'prisma-pglite-bridge';
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 PGliteBridge(pglite, lock),
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 { createPgliteAdapter } from 'prisma-pglite-bridge';
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 { adapter, resetDb } = await createPgliteAdapter({
198
- pglite,
199
- migrationsPath: './prisma/migrations',
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 { createPgliteAdapter } = require('prisma-pglite-bridge');
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 result = await createPgliteAdapter({
241
- pglite,
242
- migrationsPath: './prisma/migrations',
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 { createPgliteAdapter, type ResetDbFn } from 'prisma-pglite-bridge';
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 result = await createPgliteAdapter({
267
- pglite,
268
- migrationsPath: './prisma/migrations',
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 { createPgliteAdapter, type ResetDbFn } from 'prisma-pglite-bridge';
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 result = await createPgliteAdapter({
319
- pglite,
320
- migrationsPath: './prisma/migrations',
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 { createPgliteAdapter } from 'prisma-pglite-bridge';
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 { adapter } = await createPgliteAdapter({
347
- pglite,
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 or checksum. Compose
359
- it from trusted, version-controlled source only — never from
360
- environment variables, network input, or values that cross a trust
361
- boundary. See [Schema Resolution](#schema-resolution) for the
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 { adapter } = await createPgliteAdapter({
369
- pglite,
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 { adapter, close } = await createPgliteAdapter({
399
- pglite,
400
- ...(firstRun ? { migrationsPath: './prisma/migrations' } : {}),
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 { adapter, close } = await createPgliteAdapter({
417
- pglite,
418
- migrationsPath: './prisma/migrations',
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 adapter, run your tests, then
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 { adapter, stats, close } = await createPgliteAdapter({
585
+ const bridge = await createPGliteBridge({
454
586
  pglite,
455
- migrationsPath: './prisma/migrations',
456
587
  statsLevel: 'basic', // or 'full'
457
588
  });
458
- const prisma = new PrismaClient({ adapter });
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` — adapter lifetime (frozen at `close()`, drain
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 adapters these describe a different
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 adapter stats are updated directly by the bridge
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
- createPgliteAdapter,
663
+ createPGliteBridge,
533
664
  QUERY_CHANNEL,
534
665
  type QueryEvent,
535
666
  } from 'prisma-pglite-bridge';
536
667
 
537
- const { adapterId } = await createPgliteAdapter({ /* ... */ });
668
+ const { bridgeId } = await createPGliteBridge({ /* ... */ });
538
669
 
539
670
  const listener = (msg: unknown) => {
540
671
  const e = msg as QueryEvent;
541
- if (e.adapterId !== adapterId) return;
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: `{ adapterId: symbol; durationMs:
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: `{ adapterId: symbol;
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 `adapterId` to isolate events when multiple adapters
559
- share a process. Obtain it from the `createPgliteAdapter()` or
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 `createPgliteAdapter()` call takes
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. A `SessionLock` serializes
571
- transactions (one at a time), but `SET` variables leak between
572
- connections within a single test. `resetDb()` clears more of this
573
- between tests via `DISCARD ALL`.
574
- - **Migration files required** run `prisma migrate dev` once to
575
- generate migration files, or pass schema SQL directly via the
576
- `sql` option.
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