prisma-pglite-bridge 0.6.1 → 0.7.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 +29 -733
- package/dist/index.cjs +735 -382
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +192 -44
- package/dist/index.d.mts +192 -44
- package/dist/index.mjs +730 -383
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -9
- package/dist/ppb.mjs +0 -1456
- package/dist/ppb.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -21,753 +21,49 @@ TypeScript users also need `@types/pg`.
|
|
|
21
21
|
import { PGlite } from '@electric-sql/pglite';
|
|
22
22
|
import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
|
|
23
23
|
import { PrismaClient } from '@prisma/client';
|
|
24
|
+
import seed from './seed.ts'; // user-provided: (prisma: PrismaClient) => Promise<void>
|
|
24
25
|
|
|
25
26
|
const pglite = new PGlite();
|
|
26
|
-
|
|
27
|
-
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
27
|
+
await pushMigrations(pglite, { migrationsPath: './prisma/migrations' });
|
|
28
28
|
|
|
29
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
## Documentation
|
|
53
|
+
|
|
54
|
+
- **[API reference](./docs/api.md)** — exports, options, return
|
|
55
|
+
values, fs-sync policy.
|
|
56
|
+
- **[Cookbook](./docs/cookbook.md)** — Vitest / Jest setup,
|
|
57
|
+
per-test isolation, seed sharing, extensions, persistent dev
|
|
58
|
+
database, clean shutdown.
|
|
59
|
+
- **[`PGliteServer`](./docs/server.md)** — TCP / Unix-socket
|
|
60
|
+
front for PGlite. Use for the Prisma CLI shadow database, `psql`,
|
|
61
|
+
and SQL GUIs.
|
|
62
|
+
- **[Stats and diagnostics](./docs/stats.md)** — `stats()`
|
|
63
|
+
snapshots and `node:diagnostics_channel` event streams.
|
|
64
|
+
- **[Troubleshooting & limitations](./docs/troubleshooting.md)** —
|
|
65
|
+
known issues (PGlite version mismatch, WASM `ExperimentalWarning`)
|
|
66
|
+
and runtime constraints.
|
|
771
67
|
|
|
772
68
|
## License
|
|
773
69
|
|