prisma-pglite-bridge 0.5.2 → 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`
@@ -107,15 +108,105 @@ Returns:
107
108
  Note: this clears all data including seed data — re-seed after
108
109
  reset if needed.
109
110
  - `close()` — shuts down the pool. The caller-supplied PGlite
110
- instance is not closed — you own its lifecycle. Not needed in
111
- tests (process exit handles it); use in long-running scripts
112
- or dev servers.
111
+ instance is not closed — you own its lifecycle. Recommended in
112
+ explicit test teardown, long-running scripts, and dev servers so
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.
120
+ - `snapshotDb()` — captures the current DB contents into an internal
121
+ snapshot so later `resetDb()` calls restore to that state instead of
122
+ truncating to empty.
123
+ - `resetSnapshot()` — discards the current snapshot so later
124
+ `resetDb()` calls truncate back to empty again.
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
+ ```
119
210
 
120
211
  ### `createPool(options)`
121
212
 
@@ -133,22 +224,22 @@ const { pool, close } = await createPool({ pglite });
133
224
  const adapter = new PrismaPg(pool);
134
225
  ```
135
226
 
136
- Returns `pool` (pg.Pool), `adapterId` (a unique `symbol` for
227
+ Returns `pool` (pg.Pool), `bridgeId` (a unique `symbol` for
137
228
  [diagnostics channel](#diagnostics-channels) filtering), and
138
229
  `close()` (which shuts down the pool only — the caller-supplied
139
230
  PGlite instance is not closed). Accepts `pglite` (required),
140
- `max`, and `adapterId`.
231
+ `max`, `bridgeId`, and `syncToFs`.
141
232
 
142
- ### `PGliteBridge`
233
+ ### `PGliteDuplex`
143
234
 
144
235
  The Duplex stream that replaces `pg.Client`'s network socket.
145
236
  Exported for advanced use cases (custom `pg.Client` setup, direct
146
- wire protocol access). When using multiple bridges against the
147
- 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
148
239
  transaction interleaving.
149
240
 
150
241
  ```typescript
151
- import { PGliteBridge, SessionLock } from 'prisma-pglite-bridge';
242
+ import { PGliteDuplex, SessionLock } from 'prisma-pglite-bridge';
152
243
  import { PGlite } from '@electric-sql/pglite';
153
244
  import pg from 'pg';
154
245
 
@@ -157,10 +248,51 @@ await pglite.waitReady;
157
248
 
158
249
  const lock = new SessionLock();
159
250
  const client = new pg.Client({
160
- stream: () => new PGliteBridge(pglite, lock),
251
+ stream: () => new PGliteDuplex(pglite, lock),
161
252
  });
162
253
  ```
163
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
+
164
296
  ## Examples
165
297
 
166
298
  ### Replacing your production database in tests
@@ -184,19 +316,18 @@ the in-memory PGlite version:
184
316
  ```typescript
185
317
  // vitest.setup.ts
186
318
  import { PGlite } from '@electric-sql/pglite';
187
- import { createPgliteAdapter } from 'prisma-pglite-bridge';
319
+ import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
188
320
  import { PrismaClient } from '@prisma/client';
321
+ import { beforeEach, vi } from 'vitest';
189
322
 
190
323
  const pglite = new PGlite();
191
- const { adapter, resetDb } = await createPgliteAdapter({
192
- pglite,
193
- migrationsPath: './prisma/migrations',
194
- });
195
- 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 });
196
327
 
197
328
  vi.mock('./lib/prisma', () => ({ prisma: testPrisma }));
198
329
 
199
- beforeEach(() => resetDb());
330
+ beforeEach(() => bridge.resetDb());
200
331
  ```
201
332
 
202
333
  ```typescript
@@ -219,7 +350,7 @@ the top level, not inside `beforeAll`:
219
350
  ```typescript
220
351
  // jest.setup.ts
221
352
  const { PGlite } = require('@electric-sql/pglite');
222
- const { createPgliteAdapter } = require('prisma-pglite-bridge');
353
+ const { createPGliteBridge, pushMigrations } = require('prisma-pglite-bridge');
223
354
  const { PrismaClient } = require('@prisma/client');
224
355
 
225
356
  let testPrisma;
@@ -231,12 +362,10 @@ jest.mock('./lib/prisma', () => ({
231
362
 
232
363
  beforeAll(async () => {
233
364
  const pglite = new PGlite();
234
- const result = await createPgliteAdapter({
235
- pglite,
236
- migrationsPath: './prisma/migrations',
237
- });
238
- testPrisma = new PrismaClient({ adapter: result.adapter });
239
- 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;
240
369
  });
241
370
 
242
371
  beforeEach(() => resetDb());
@@ -248,7 +377,7 @@ If your code accepts `PrismaClient` as a parameter:
248
377
 
249
378
  ```typescript
250
379
  import { PGlite } from '@electric-sql/pglite';
251
- import { createPgliteAdapter, type ResetDbFn } from 'prisma-pglite-bridge';
380
+ import { createPGliteBridge, pushMigrations, type ResetDbFn } from 'prisma-pglite-bridge';
252
381
  import { PrismaClient } from '@prisma/client';
253
382
  import { beforeAll, beforeEach, it, expect } from 'vitest';
254
383
 
@@ -257,12 +386,10 @@ let resetDb: ResetDbFn;
257
386
 
258
387
  beforeAll(async () => {
259
388
  const pglite = new PGlite();
260
- const result = await createPgliteAdapter({
261
- pglite,
262
- migrationsPath: './prisma/migrations',
263
- });
264
- prisma = new PrismaClient({ adapter: result.adapter });
265
- 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;
266
393
  });
267
394
 
268
395
  beforeEach(() => resetDb());
@@ -300,7 +427,7 @@ Then reuse it in tests:
300
427
 
301
428
  ```typescript
302
429
  import { PGlite } from '@electric-sql/pglite';
303
- import { createPgliteAdapter, type ResetDbFn } from 'prisma-pglite-bridge';
430
+ import { createPGliteBridge, pushMigrations, type ResetDbFn } from 'prisma-pglite-bridge';
304
431
  import { PrismaClient } from '@prisma/client';
305
432
  import { seed } from '../prisma/seed';
306
433
 
@@ -309,12 +436,10 @@ let resetDb: ResetDbFn;
309
436
 
310
437
  beforeAll(async () => {
311
438
  const pglite = new PGlite();
312
- const result = await createPgliteAdapter({
313
- pglite,
314
- migrationsPath: './prisma/migrations',
315
- });
316
- prisma = new PrismaClient({ adapter: result.adapter });
317
- 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;
318
443
  await seed(prisma);
319
444
  });
320
445
 
@@ -325,6 +450,23 @@ beforeEach(async () => {
325
450
  });
326
451
  ```
327
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
+
328
470
  ### Using PostgreSQL extensions
329
471
 
330
472
  If your schema uses `uuid-ossp`, `pgcrypto`, or other extensions,
@@ -332,15 +474,13 @@ pass them via the `extensions` option:
332
474
 
333
475
  ```typescript
334
476
  import { PGlite } from '@electric-sql/pglite';
335
- import { createPgliteAdapter } from 'prisma-pglite-bridge';
477
+ import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
336
478
  import { uuid_ossp } from '@electric-sql/pglite/contrib/uuid_ossp';
337
479
  import { pgcrypto } from '@electric-sql/pglite/contrib/pgcrypto';
338
480
 
339
481
  const pglite = new PGlite({ extensions: { uuid_ossp, pgcrypto } });
340
- const { adapter } = await createPgliteAdapter({
341
- pglite,
342
- migrationsPath: './prisma/migrations',
343
- });
482
+ const bridge = await createPGliteBridge({ pglite });
483
+ await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
344
484
  ```
345
485
 
346
486
  Extensions are included in the `@electric-sql/pglite` package —
@@ -349,18 +489,18 @@ for the full list.
349
489
 
350
490
  ### Pre-generated SQL (fastest)
351
491
 
352
- The `sql` option runs verbatim with no sandbox or checksum. Compose
353
- it from trusted, version-controlled source only — never from
354
- environment variables, network input, or values that cross a trust
355
- boundary. See [Schema Resolution](#schema-resolution) for the
356
- 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.
357
496
 
358
497
  ```typescript
359
498
  import { PGlite } from '@electric-sql/pglite';
499
+ import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
360
500
 
361
501
  const pglite = new PGlite();
362
- const { adapter } = await createPgliteAdapter({
363
- pglite,
502
+ const bridge = await createPGliteBridge({ pglite });
503
+ await pushMigrations(bridge, {
364
504
  sql: `
365
505
  CREATE TABLE "User" (id text PRIMARY KEY, name text NOT NULL);
366
506
  CREATE TABLE "Post" (
@@ -389,11 +529,9 @@ const dataDir = './data/pglite';
389
529
  const firstRun = !existsSync(join(dataDir, 'PG_VERSION'));
390
530
 
391
531
  const pglite = new PGlite(dataDir);
392
- const { adapter, close } = await createPgliteAdapter({
393
- pglite,
394
- ...(firstRun ? { migrationsPath: './prisma/migrations' } : {}),
395
- });
396
- 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 });
397
535
  ```
398
536
 
399
537
  **Add `data/pglite/` to `.gitignore`.** Delete the data directory
@@ -405,19 +543,18 @@ or environments where installing PostgreSQL is impractical.
405
543
 
406
544
  ```typescript
407
545
  import { PGlite } from '@electric-sql/pglite';
546
+ import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
408
547
 
409
548
  const pglite = new PGlite();
410
- const { adapter, close } = await createPgliteAdapter({
411
- pglite,
412
- migrationsPath: './prisma/migrations',
413
- });
414
- 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 });
415
552
 
416
553
  try {
417
554
  await seedDatabase(prisma);
418
555
  } finally {
419
556
  await prisma.$disconnect();
420
- await close();
557
+ await bridge.close();
421
558
  await pglite.close();
422
559
  }
423
560
  ```
@@ -427,7 +564,7 @@ try {
427
564
  For most developers, this is the easiest way to see how the bridge
428
565
  performed in tests.
429
566
 
430
- Enable `statsLevel` when creating the adapter, run your tests, then
567
+ Enable `statsLevel` when creating the bridge, run your tests, then
431
568
  call `await stats()` at the end. You get one snapshot with the main
432
569
  things you usually care about: query counts, timing percentiles,
433
570
  database size, and, at `'full'`, process RSS and session-lock wait
@@ -442,19 +579,20 @@ external consumer subscribes to the public
442
579
 
443
580
  ```typescript
444
581
  import { PGlite } from '@electric-sql/pglite';
582
+ import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
445
583
 
446
584
  const pglite = new PGlite();
447
- const { adapter, stats, close } = await createPgliteAdapter({
585
+ const bridge = await createPGliteBridge({
448
586
  pglite,
449
- migrationsPath: './prisma/migrations',
450
587
  statsLevel: 'basic', // or 'full'
451
588
  });
452
- const prisma = new PrismaClient({ adapter });
589
+ await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
590
+ const prisma = new PrismaClient({ adapter: bridge.adapter });
453
591
 
454
592
  afterAll(async () => {
455
593
  await prisma.$disconnect();
456
- await close();
457
- const s = await stats();
594
+ await bridge.close();
595
+ const s = await bridge.stats();
458
596
  if (s) console.log(s);
459
597
  await pglite.close();
460
598
  });
@@ -473,9 +611,8 @@ described below. That path is more flexible, but also more advanced.
473
611
 
474
612
  **`'basic'`** — timing and counters:
475
613
 
476
- - `durationMs` — adapter lifetime (frozen at `close()`, drain
614
+ - `durationMs` — bridge lifetime (frozen at `close()`, drain
477
615
  excluded)
478
- - `schemaSetupMs` — one-time cost of applying migration SQL
479
616
  - `queryCount`, `failedQueryCount` — WASM round-trips (a Prisma
480
617
  extended-query pipeline is one round-trip, not five). Lifetime
481
618
  counters.
@@ -483,7 +620,7 @@ described below. That path is more flexible, but also more advanced.
483
620
  durations
484
621
  - `recentP50QueryMs`, `recentP95QueryMs`, `recentMaxQueryMs` —
485
622
  nearest-rank percentiles (no interpolation) over the most recent
486
- ~10,000 queries. On long-lived adapters these describe a different
623
+ ~10,000 queries. On long-lived bridges these describe a different
487
624
  population than `avgQueryMs`.
488
625
  - `resetDbCalls` — counts `resetDb()` attempts
489
626
  - `dbSizeBytes` — `pg_database_size(current_database())`, cached
@@ -511,7 +648,7 @@ rejects, `processRssPeakBytes` on runtimes without
511
648
 
512
649
  The bridge publishes per-query and per-lock-wait events to
513
650
  [`node:diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html)
514
- channels. Built-in adapter stats are updated directly by the bridge
651
+ channels. Built-in bridge stats are updated directly by the bridge
515
652
  when `statsLevel` is `'basic'` or `'full'`; external consumers (OpenTelemetry, APM,
516
653
  custom loggers) can subscribe directly without touching the bridge
517
654
  API.
@@ -523,16 +660,16 @@ Subscribing opts you in to that work.
523
660
  ```typescript
524
661
  import diagnostics_channel from 'node:diagnostics_channel';
525
662
  import {
526
- createPgliteAdapter,
663
+ createPGliteBridge,
527
664
  QUERY_CHANNEL,
528
665
  type QueryEvent,
529
666
  } from 'prisma-pglite-bridge';
530
667
 
531
- const { adapterId } = await createPgliteAdapter({ /* ... */ });
668
+ const { bridgeId } = await createPGliteBridge({ /* ... */ });
532
669
 
533
670
  const listener = (msg: unknown) => {
534
671
  const e = msg as QueryEvent;
535
- if (e.adapterId !== adapterId) return;
672
+ if (e.bridgeId !== bridgeId) return;
536
673
  myMetrics.record('db.query', e.durationMs, { ok: e.succeeded });
537
674
  };
538
675
  diagnostics_channel.channel(QUERY_CHANNEL).subscribe(listener);
@@ -541,33 +678,37 @@ diagnostics_channel.channel(QUERY_CHANNEL).subscribe(listener);
541
678
  Channels:
542
679
 
543
680
  - `QUERY_CHANNEL` (`prisma-pglite-bridge:query`) — every
544
- whole-query boundary. Payload: `{ adapterId: symbol; durationMs:
681
+ whole-query boundary. Payload: `{ bridgeId: symbol; durationMs:
545
682
  number; succeeded: boolean }`. `succeeded` is `false` for both
546
683
  thrown errors and protocol-level `ErrorResponse` frames.
547
684
  - `LOCK_WAIT_CHANNEL` (`prisma-pglite-bridge:lock-wait`) — every
548
- session-lock acquisition. Payload: `{ adapterId: symbol;
685
+ session-lock acquisition. Payload: `{ bridgeId: symbol;
549
686
  durationMs: number }`. `durationMs` is how long the acquirer
550
687
  waited before the lock was granted.
551
688
 
552
- Filter on `adapterId` to isolate events when multiple adapters
553
- 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
554
691
  `createPool()` return value.
555
692
 
556
693
  ## Limitations
557
694
 
558
695
  - **Node.js 20+ only** — requires `node:stream` and `node:fs`.
559
696
  Does not work in browsers despite PGlite's browser support.
560
- - **WASM cold start** — first `createPgliteAdapter()` call takes
697
+ - **WASM cold start** — first `createPGliteBridge()` call takes
561
698
  ~2s for PGlite WASM compilation. Subsequent calls in the same
562
699
  process reuse the compiled module.
563
700
  - **Single PostgreSQL session** — PGlite runs in single-user mode.
564
- All pool connections share one session. A `SessionLock` serializes
565
- transactions (one at a time), but `SET` variables leak between
566
- connections within a single test. `resetDb()` clears more of this
567
- between tests via `DISCARD ALL`.
568
- - **Migration files required** run `prisma migrate dev` once to
569
- generate migration files, or pass schema SQL directly via the
570
- `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.
571
712
 
572
713
  ## Troubleshooting
573
714
 
@@ -599,6 +740,35 @@ If you see more than one version, force a single 0.4.x via
599
740
 
600
741
  Then `pnpm install`.
601
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
+
602
772
  ## License
603
773
 
604
774
  MIT