prisma-pglite-bridge 0.6.1 → 1.0.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
@@ -18,756 +18,56 @@ TypeScript users also need `@types/pg`.
18
18
  ## Quickstart
19
19
 
20
20
  ```typescript
21
- import { PGlite } from '@electric-sql/pglite';
22
- import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
21
+ import { PGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
23
22
  import { PrismaClient } from '@prisma/client';
23
+ import seed from './seed.ts'; // user-provided: (prisma: PrismaClient) => Promise<void>
24
24
 
25
- const pglite = new PGlite();
26
- const bridge = await createPGliteBridge({ pglite });
27
- await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
25
+ const bridge = new PGliteBridge();
26
+ // Have prisma/migrations/? Use pushMigrations (shown).
27
+ // Only schema.prisma? Use pushSchema instead — see docs/api.md.
28
+ await pushMigrations(bridge.pglite, { migrationsPath: './prisma/migrations' });
28
29
 
29
30
  const prisma = new PrismaClient({ adapter: bridge.adapter });
31
+ await seed(prisma);
32
+ await bridge.snapshotDb();
30
33
 
31
34
  beforeEach(() => bridge.resetDb());
32
35
  ```
33
36
 
34
- Call `resetDb()` in `beforeEach` to wipe all data between tests.
35
- Skip it if your tests are read-only or you want state to carry
37
+ `snapshotDb()` captures the seeded state once. `resetDb()` in
38
+ `beforeEach` then restores each test to that snapshot fast,
39
+ deterministic, no re-seeding per test. Skip the snapshot/reset
40
+ pair if your tests are read-only or you want state to carry
36
41
  over.
37
42
 
38
43
  That's it. Run `prisma migrate dev` first to generate migration
39
44
  files. No Docker, no database server — works in GitHub Actions,
40
45
  GitLab CI, and any environment where Node.js runs.
41
46
 
42
- ## Populating the database
43
-
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
-
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 |
52
-
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.
57
-
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.
63
-
64
- ## Bridge fs-sync policy
65
-
66
- The bridge defaults `syncToFs` to `'auto'`:
67
-
68
- - in-memory PGlite (`new PGlite()` or `memory://...`) resolves to `false`
69
- - persistent `dataDir` usage resolves to `true`
70
-
71
- That keeps bridge-heavy test workloads on the lower-memory fast path
72
- without changing durability defaults for persistent databases.
73
- If you use a custom `fs`, set `syncToFs` explicitly because the
74
- bridge cannot infer whether that storage is durable.
75
-
76
- ## API
77
-
78
- ### `createPGliteBridge(options)`
79
-
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.
83
-
84
- ```typescript
85
- const pglite = new PGlite(/* dataDir, extensions, ... */);
86
-
87
- const bridge = await createPGliteBridge({
88
- pglite, // required — caller owns lifecycle
89
- max: 1, // pool connections (default: 1)
90
- statsLevel: 'off', // 'off' | 'basic' | 'full' (default: 'off')
91
- syncToFs: 'auto', // 'auto' | true | false (default: 'auto')
92
- });
93
- ```
94
-
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`:
100
-
101
- - `adapter` — pass to `new PrismaClient({ adapter })`
102
- - `pglite` — the caller-supplied PGlite instance, re-exposed for
103
- symmetry with `pushSchema` / `pushMigrations`
104
- - `resetDb()` — truncates all user tables and discards
105
- session-local state via `DISCARD ALL` (for example `SET`
106
- variables, prepared statements, temp tables, and `LISTEN`
107
- registrations). Call in `beforeEach` for per-test isolation.
108
- Note: this clears all data including seed data — re-seed after
109
- reset if needed.
110
- - `close()` — shuts down the pool. The caller-supplied PGlite
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.
114
- - `stats()` — returns telemetry when `statsLevel` is `'basic'` or
115
- `'full'`, else `undefined`. See [Stats collection](#stats-collection).
116
- - `bridgeId` — a unique `symbol` identifying this bridge. Use it
117
- to filter events from the public
118
- [diagnostics channels](#diagnostics-channels) when multiple
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
- ```
210
-
211
- ### `createPool(options)`
212
-
213
- Lower-level escape hatch. Creates a `pg.Pool` backed by PGlite
214
- without schema handling — useful for custom Prisma setups,
215
- other ORMs, or raw SQL.
216
-
217
- ```typescript
218
- import { PGlite } from '@electric-sql/pglite';
219
- import { createPool } from 'prisma-pglite-bridge';
220
- import { PrismaPg } from '@prisma/adapter-pg';
221
-
222
- const pglite = new PGlite();
223
- const { pool, close } = await createPool({ pglite });
224
- const adapter = new PrismaPg(pool);
225
- ```
226
-
227
- Returns `pool` (pg.Pool), `bridgeId` (a unique `symbol` for
228
- [diagnostics channel](#diagnostics-channels) filtering), and
229
- `close()` (which shuts down the pool only — the caller-supplied
230
- PGlite instance is not closed). Accepts `pglite` (required),
231
- `max`, `bridgeId`, and `syncToFs`.
232
-
233
- ### `PGliteDuplex`
234
-
235
- The Duplex stream that replaces `pg.Client`'s network socket.
236
- Exported for advanced use cases (custom `pg.Client` setup, direct
237
- wire protocol access). When using multiple duplex streams against
238
- the same PGlite instance, pass a shared `SessionLock` to prevent
239
- transaction interleaving.
240
-
241
- ```typescript
242
- import { PGliteDuplex, SessionLock } from 'prisma-pglite-bridge';
243
- import { PGlite } from '@electric-sql/pglite';
244
- import pg from 'pg';
245
-
246
- const pglite = new PGlite();
247
- await pglite.waitReady;
248
-
249
- const lock = new SessionLock();
250
- const client = new pg.Client({
251
- stream: () => new PGliteDuplex(pglite, lock),
252
- });
253
- ```
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
-
296
- ## Examples
297
-
298
- ### Replacing your production database in tests
299
-
300
- Most Prisma projects use a singleton module:
301
-
302
- ```typescript
303
- // lib/prisma.ts — your production singleton
304
- import { PrismaPg } from '@prisma/adapter-pg';
305
- import { PrismaClient } from '@prisma/client';
306
- import pg from 'pg';
307
-
308
- const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
309
- const adapter = new PrismaPg(pool);
310
- export const prisma = new PrismaClient({ adapter });
311
- ```
312
-
313
- In tests, swap the singleton via `vi.mock` so every import gets
314
- the in-memory PGlite version:
315
-
316
- ```typescript
317
- // vitest.setup.ts
318
- import { PGlite } from '@electric-sql/pglite';
319
- import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
320
- import { PrismaClient } from '@prisma/client';
321
- import { beforeEach, vi } from 'vitest';
322
-
323
- const pglite = new PGlite();
324
- const bridge = await createPGliteBridge({ pglite });
325
- await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
326
- export const testPrisma = new PrismaClient({ adapter: bridge.adapter });
327
-
328
- vi.mock('./lib/prisma', () => ({ prisma: testPrisma }));
329
-
330
- beforeEach(() => bridge.resetDb());
331
- ```
332
-
333
- ```typescript
334
- // vitest.config.ts
335
- export default defineConfig({
336
- test: {
337
- setupFiles: ['./vitest.setup.ts'],
338
- },
339
- });
340
- ```
341
-
342
- Now every test file that imports `prisma` from `lib/prisma`
343
- gets the PGlite-backed instance. No Docker, no test database,
344
- no cleanup scripts.
345
-
346
- For Jest, the same pattern works with `jest.mock`. Note that
347
- `jest.mock` is hoisted to the top of the file — place it at
348
- the top level, not inside `beforeAll`:
349
-
350
- ```typescript
351
- // jest.setup.ts
352
- const { PGlite } = require('@electric-sql/pglite');
353
- const { createPGliteBridge, pushMigrations } = require('prisma-pglite-bridge');
354
- const { PrismaClient } = require('@prisma/client');
355
-
356
- let testPrisma;
357
- let resetDb;
358
-
359
- jest.mock('./lib/prisma', () => ({
360
- get prisma() { return testPrisma; },
361
- }));
362
-
363
- beforeAll(async () => {
364
- const pglite = new PGlite();
365
- const bridge = await createPGliteBridge({ pglite });
366
- await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
367
- testPrisma = new PrismaClient({ adapter: bridge.adapter });
368
- resetDb = bridge.resetDb;
369
- });
370
-
371
- beforeEach(() => resetDb());
372
- ```
373
-
374
- ### Vitest with per-test isolation (no singleton)
375
-
376
- If your code accepts `PrismaClient` as a parameter:
377
-
378
- ```typescript
379
- import { PGlite } from '@electric-sql/pglite';
380
- import { createPGliteBridge, pushMigrations, type ResetDbFn } from 'prisma-pglite-bridge';
381
- import { PrismaClient } from '@prisma/client';
382
- import { beforeAll, beforeEach, it, expect } from 'vitest';
383
-
384
- let prisma: PrismaClient;
385
- let resetDb: ResetDbFn;
386
-
387
- beforeAll(async () => {
388
- const pglite = new PGlite();
389
- const bridge = await createPGliteBridge({ pglite });
390
- await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
391
- prisma = new PrismaClient({ adapter: bridge.adapter });
392
- resetDb = bridge.resetDb;
393
- });
394
-
395
- beforeEach(() => resetDb());
396
-
397
- it('creates a user', async () => {
398
- const user = await prisma.user.create({
399
- data: { name: 'Test' },
400
- });
401
- expect(user.id).toBeDefined();
402
- });
403
- ```
404
-
405
- ### Sharing seed logic between `prisma db seed` and tests
406
-
407
- Extract your seed logic into a function that accepts a
408
- PrismaClient:
409
-
410
- ```typescript
411
- // prisma/seed.ts
412
- import { PrismaClient } from '@prisma/client';
413
-
414
- export const seed = async (prisma: PrismaClient) => {
415
- await prisma.user.create({ data: { name: 'Alice', role: 'ADMIN' } });
416
- await prisma.user.create({ data: { name: 'Bob', role: 'MEMBER' } });
417
- };
418
-
419
- // Script entry point for `prisma db seed`
420
- if (import.meta.url === new URL(process.argv[1]!, 'file:').href) {
421
- const prisma = new PrismaClient();
422
- seed(prisma).then(() => prisma.$disconnect());
423
- }
424
- ```
425
-
426
- Then reuse it in tests:
427
-
428
- ```typescript
429
- import { PGlite } from '@electric-sql/pglite';
430
- import { createPGliteBridge, pushMigrations, type ResetDbFn } from 'prisma-pglite-bridge';
431
- import { PrismaClient } from '@prisma/client';
432
- import { seed } from '../prisma/seed';
433
-
434
- let prisma: PrismaClient;
435
- let resetDb: ResetDbFn;
436
-
437
- beforeAll(async () => {
438
- const pglite = new PGlite();
439
- const bridge = await createPGliteBridge({ pglite });
440
- await pushMigrations(result, { migrationsPath: './prisma/migrations' });
441
- prisma = new PrismaClient({ adapter: bridge.adapter });
442
- resetDb = bridge.resetDb;
443
- await seed(prisma);
444
- });
445
-
446
- // resetDb() clears all data — re-seed if needed
447
- beforeEach(async () => {
448
- await resetDb();
449
- await seed(prisma);
450
- });
451
- ```
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
-
470
- ### Using PostgreSQL extensions
471
-
472
- If your schema uses `uuid-ossp`, `pgcrypto`, or other extensions,
473
- pass them via the `extensions` option:
474
-
475
- ```typescript
476
- import { PGlite } from '@electric-sql/pglite';
477
- import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
478
- import { uuid_ossp } from '@electric-sql/pglite/contrib/uuid_ossp';
479
- import { pgcrypto } from '@electric-sql/pglite/contrib/pgcrypto';
480
-
481
- const pglite = new PGlite({ extensions: { uuid_ossp, pgcrypto } });
482
- const bridge = await createPGliteBridge({ pglite });
483
- await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
484
- ```
485
-
486
- Extensions are included in the `@electric-sql/pglite` package —
487
- no extra install needed. See [PGlite extensions](https://pglite.dev/extensions/)
488
- for the full list.
489
-
490
- ### Pre-generated SQL (fastest)
491
-
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.
496
-
497
- ```typescript
498
- import { PGlite } from '@electric-sql/pglite';
499
- import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
500
-
501
- const pglite = new PGlite();
502
- const bridge = await createPGliteBridge({ pglite });
503
- await pushMigrations(bridge, {
504
- sql: `
505
- CREATE TABLE "User" (id text PRIMARY KEY, name text NOT NULL);
506
- CREATE TABLE "Post" (
507
- id text PRIMARY KEY,
508
- title text NOT NULL,
509
- "userId" text REFERENCES "User"(id)
510
- );
511
- `,
512
- });
513
- ```
514
-
515
- ### Persistent dev database (optional)
516
-
517
- By default, PGlite runs entirely in memory — the database
518
- disappears when the process exits. This is ideal for tests. If you
519
- want data to survive restarts (local development, prototyping),
520
- pass a `dataDir` when constructing PGlite, and only apply
521
- migrations on first run:
522
-
523
- ```typescript
524
- import { existsSync } from 'node:fs';
525
- import { join } from 'node:path';
526
- import { PGlite } from '@electric-sql/pglite';
527
-
528
- const dataDir = './data/pglite';
529
- const firstRun = !existsSync(join(dataDir, 'PG_VERSION'));
530
-
531
- const pglite = new PGlite(dataDir);
532
- const bridge = await createPGliteBridge({ pglite });
533
- if (firstRun) await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
534
- const prisma = new PrismaClient({ adapter: bridge.adapter });
535
- ```
536
-
537
- **Add `data/pglite/` to `.gitignore`.** Delete the data directory
538
- after schema changes to pick up new migrations. This gives you a
539
- local PostgreSQL without Docker — useful for offline development
540
- or environments where installing PostgreSQL is impractical.
541
-
542
- ### Long-running script with clean shutdown
543
-
544
- ```typescript
545
- import { PGlite } from '@electric-sql/pglite';
546
- import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
547
-
548
- const pglite = new PGlite();
549
- const bridge = await createPGliteBridge({ pglite });
550
- await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
551
- const prisma = new PrismaClient({ adapter: bridge.adapter });
552
-
553
- try {
554
- await seedDatabase(prisma);
555
- } finally {
556
- await prisma.$disconnect();
557
- await bridge.close();
558
- await pglite.close();
559
- }
560
- ```
561
-
562
- ## Stats collection
563
-
564
- For most developers, this is the easiest way to see how the bridge
565
- performed in tests.
566
-
567
- Enable `statsLevel` when creating the bridge, run your tests, then
568
- call `await stats()` at the end. You get one snapshot with the main
569
- things you usually care about: query counts, timing percentiles,
570
- database size, and, at `'full'`, process RSS and session-lock wait
571
- times.
572
-
573
- This is the built-in, low-friction path for test diagnostics. It is
574
- useful for CI cost insight, perf tuning, and understanding test-suite
575
- behavior without wiring up a separate metrics pipeline. **Off by
576
- default**; the hot path stays effectively zero-cost as long as no
577
- external consumer subscribes to the public
578
- [diagnostics channels](#diagnostics-channels).
579
-
580
- ```typescript
581
- import { PGlite } from '@electric-sql/pglite';
582
- import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
583
-
584
- const pglite = new PGlite();
585
- const bridge = await createPGliteBridge({
586
- pglite,
587
- statsLevel: 'basic', // or 'full'
588
- });
589
- await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
590
- const prisma = new PrismaClient({ adapter: bridge.adapter });
591
-
592
- afterAll(async () => {
593
- await prisma.$disconnect();
594
- await bridge.close();
595
- const s = await bridge.stats();
596
- if (s) console.log(s);
597
- await pglite.close();
598
- });
599
- ```
600
-
601
- `stats()` returns `Promise<Stats | undefined>` — `undefined` when
602
- `statsLevel` is `'off'` (or omitted). Safe to call before or after
603
- `close()`; post-close reads return frozen values from the moment
604
- `close()` was invoked.
605
-
606
- If you need live per-query or per-lock-wait events instead of a final
607
- snapshot, use the public [diagnostics channels](#diagnostics-channels)
608
- described below. That path is more flexible, but also more advanced.
609
-
610
- ### Levels
611
-
612
- **`'basic'`** — timing and counters:
613
-
614
- - `durationMs` — bridge lifetime (frozen at `close()`, drain
615
- excluded)
616
- - `queryCount`, `failedQueryCount` — WASM round-trips (a Prisma
617
- extended-query pipeline is one round-trip, not five). Lifetime
618
- counters.
619
- - `totalQueryMs`, `avgQueryMs` — lifetime sum and mean of query
620
- durations
621
- - `recentP50QueryMs`, `recentP95QueryMs`, `recentMaxQueryMs` —
622
- nearest-rank percentiles (no interpolation) over the most recent
623
- ~10,000 queries. On long-lived bridges these describe a different
624
- population than `avgQueryMs`.
625
- - `resetDbCalls` — counts `resetDb()` attempts
626
- - `dbSizeBytes` — `pg_database_size(current_database())`, cached
627
- at close
628
-
629
- **`'full'`** — adds:
630
-
631
- - `processRssPeakBytes` — process-wide RSS peak, read from
632
- `process.resourceUsage().maxRSS` (kernel-tracked, lossless) at
633
- the moment `stats()` is called. Contaminated if unrelated work
634
- shares the process — use as an ordering signal, not an absolute
635
- measurement. `undefined` on runtimes without
636
- `process.resourceUsage` (Bun, Deno, edge workers).
637
- - `totalSessionLockWaitMs`, `sessionLockAcquisitionCount`,
638
- `avgSessionLockWaitMs`, `maxSessionLockWaitMs` — session-lock
639
- contention across pool connections
640
-
641
- `statsLevel` is echoed on the returned object. Any field typed
642
- `T | undefined` in the returned `Stats` is the exhaustive list of
643
- fields that can be missing — `dbSizeBytes` if `pg_database_size`
644
- rejects, `processRssPeakBytes` on runtimes without
645
- `process.resourceUsage`. Every other field is always defined.
646
-
647
- ## Diagnostics channels
648
-
649
- The bridge publishes per-query and per-lock-wait events to
650
- [`node:diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html)
651
- channels. Built-in bridge stats are updated directly by the bridge
652
- when `statsLevel` is `'basic'` or `'full'`; external consumers (OpenTelemetry, APM,
653
- custom loggers) can subscribe directly without touching the bridge
654
- API.
655
-
656
- Publication is gated by `channel.hasSubscribers`, so when nobody
657
- is listening the hot path pays no timing or payload cost.
658
- Subscribing opts you in to that work.
659
-
660
- ```typescript
661
- import diagnostics_channel from 'node:diagnostics_channel';
662
- import {
663
- createPGliteBridge,
664
- QUERY_CHANNEL,
665
- type QueryEvent,
666
- } from 'prisma-pglite-bridge';
667
-
668
- const { bridgeId } = await createPGliteBridge({ /* ... */ });
669
-
670
- const listener = (msg: unknown) => {
671
- const e = msg as QueryEvent;
672
- if (e.bridgeId !== bridgeId) return;
673
- myMetrics.record('db.query', e.durationMs, { ok: e.succeeded });
674
- };
675
- diagnostics_channel.channel(QUERY_CHANNEL).subscribe(listener);
676
- ```
677
-
678
- Channels:
679
-
680
- - `QUERY_CHANNEL` (`prisma-pglite-bridge:query`) — every
681
- whole-query boundary. Payload: `{ bridgeId: symbol; durationMs:
682
- number; succeeded: boolean }`. `succeeded` is `false` for both
683
- thrown errors and protocol-level `ErrorResponse` frames.
684
- - `LOCK_WAIT_CHANNEL` (`prisma-pglite-bridge:lock-wait`) — every
685
- session-lock acquisition. Payload: `{ bridgeId: symbol;
686
- durationMs: number }`. `durationMs` is how long the acquirer
687
- waited before the lock was granted.
688
-
689
- Filter on `bridgeId` to isolate events when multiple bridges
690
- share a process. Obtain it from the `createPGliteBridge()` or
691
- `createPool()` return value.
692
-
693
- ## Limitations
694
-
695
- - **Node.js 20+ only** — requires `node:stream` and `node:fs`.
696
- Does not work in browsers despite PGlite's browser support.
697
- - **WASM cold start** — first `createPGliteBridge()` call takes
698
- ~2s for PGlite WASM compilation. Subsequent calls in the same
699
- process reuse the compiled module.
700
- - **Single PostgreSQL session** — PGlite runs in single-user mode.
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.
712
-
713
- ## Troubleshooting
714
-
715
- ### `this.pglite.execProtocolRawStream is not a function`
716
-
717
- The bridge uses PGlite 0.4's streaming protocol API. Some packages
718
- in the Prisma ecosystem (e.g. `@prisma/dev`) still pin
719
- `@electric-sql/pglite` to 0.3.x, which pnpm will install alongside
720
- 0.4 — and the bridge can end up with the older copy.
721
-
722
- Check your tree:
723
-
724
- ```sh
725
- pnpm why @electric-sql/pglite
726
- ```
727
-
728
- If you see more than one version, force a single 0.4.x via
729
- `pnpm.overrides` in your project's `package.json`:
730
-
731
- ```json
732
- {
733
- "pnpm": {
734
- "overrides": {
735
- "@electric-sql/pglite": "^0.4.4"
736
- }
737
- }
738
- }
739
- ```
740
-
741
- Then `pnpm install`.
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.
47
+ For projects without a `prisma/migrations` directory (test
48
+ fixtures, prototypes), see [Populating the
49
+ database](./docs/api.md#populating-the-database) for the
50
+ `pushSchema` alternative.
51
+
52
+ Running the Prisma CLI against this bridge (shadow DB for
53
+ `migrate dev`, `psql`, SQL GUIs)? See
54
+ [`PGliteServer`](./docs/server.md) for the TCP/Unix-socket front.
55
+
56
+ ## Documentation
57
+
58
+ - **[API reference](./docs/api.md)** exports, options, return
59
+ values, fs-sync policy.
60
+ - **[Cookbook](./docs/cookbook.md)** Vitest / Jest setup,
61
+ per-test isolation, seed sharing, extensions, persistent dev
62
+ database, clean shutdown.
63
+ - **[`PGliteServer`](./docs/server.md)** TCP / Unix-socket
64
+ front for PGlite. Use for the Prisma CLI shadow database, `psql`,
65
+ and SQL GUIs.
66
+ - **[Stats and diagnostics](./docs/stats.md)** `stats()`
67
+ snapshots and `node:diagnostics_channel` event streams.
68
+ - **[Troubleshooting & limitations](./docs/troubleshooting.md)** —
69
+ known issues (PGlite version mismatch, WASM `ExperimentalWarning`)
70
+ and runtime constraints.
771
71
 
772
72
  ## License
773
73