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 +297 -127
- package/dist/index.cjs +346 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -76
- package/dist/index.d.mts +146 -76
- package/dist/index.mjs +342 -186
- package/dist/index.mjs.map +1 -1
- package/dist/ppb.mjs +1456 -0
- package/dist/ppb.mjs.map +1 -0
- package/package.json +26 -16
package/README.md
CHANGED
|
@@ -19,17 +19,16 @@ TypeScript users also need `@types/pg`.
|
|
|
19
19
|
|
|
20
20
|
```typescript
|
|
21
21
|
import { PGlite } from '@electric-sql/pglite';
|
|
22
|
-
import {
|
|
22
|
+
import { createPGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
|
|
23
23
|
import { PrismaClient } from '@prisma/client';
|
|
24
24
|
|
|
25
25
|
const pglite = new PGlite();
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
migrationsPath: './prisma/migrations',
|
|
29
|
-
});
|
|
30
|
-
const prisma = new PrismaClient({ adapter });
|
|
26
|
+
const bridge = await createPGliteBridge({ pglite });
|
|
27
|
+
await pushMigrations(bridge, { migrationsPath: './prisma/migrations' });
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
const prisma = new PrismaClient({ adapter: bridge.adapter });
|
|
30
|
+
|
|
31
|
+
beforeEach(() => bridge.resetDb());
|
|
33
32
|
```
|
|
34
33
|
|
|
35
34
|
Call `resetDb()` in `beforeEach` to wipe all data between tests.
|
|
@@ -40,34 +39,31 @@ That's it. Run `prisma migrate dev` first to generate migration
|
|
|
40
39
|
files. No Docker, no database server — works in GitHub Actions,
|
|
41
40
|
GitLab CI, and any environment where Node.js runs.
|
|
42
41
|
|
|
43
|
-
##
|
|
42
|
+
## Populating the database
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
`createPGliteBridge` returns an empty database. The bridge offers
|
|
45
|
+
two helpers to populate it — pick the one that matches your
|
|
46
|
+
project layout:
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
migration files (same resolution as `prisma migrate dev`),
|
|
53
|
-
triggered by passing `configRoot`. Requires `prisma` to be
|
|
54
|
-
installed (which provides `@prisma/config` as a transitive
|
|
55
|
-
dependency).
|
|
48
|
+
| Helper | When to use | Cost |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| [`pushMigrations`](#pushmigrationstarget-options) | You already run `prisma migrate dev` and have a `prisma/migrations` directory | No WASM schema engine; no Node `ExperimentalWarning` |
|
|
51
|
+
| [`pushSchema`](#pushschematarget-options) | You only have `schema.prisma` (test fixtures, prototypes) and want it applied via `prisma db push` semantics | Loads `@prisma/schema-engine-wasm` once per process |
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
Both accept the same `PGliteBridge` returned by
|
|
54
|
+
`createPGliteBridge`, so you can swap helpers without touching the
|
|
55
|
+
bridge wiring. If you reopen a persistent `dataDir` that already
|
|
56
|
+
holds the schema, call neither.
|
|
60
57
|
|
|
61
|
-
Schema SQL
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
writable only by trusted processes.
|
|
58
|
+
Schema SQL is executed verbatim with no checksum or signature
|
|
59
|
+
verification. Compose it from trusted, version-controlled source
|
|
60
|
+
only — never from environment variables, network input, or any
|
|
61
|
+
value that crosses a trust boundary, and keep the migrations
|
|
62
|
+
directory writable only by trusted processes.
|
|
67
63
|
|
|
68
64
|
## Bridge fs-sync policy
|
|
69
65
|
|
|
70
|
-
The
|
|
66
|
+
The bridge defaults `syncToFs` to `'auto'`:
|
|
71
67
|
|
|
72
68
|
- in-memory PGlite (`new PGlite()` or `memory://...`) resolves to `false`
|
|
73
69
|
- persistent `dataDir` usage resolves to `true`
|
|
@@ -75,31 +71,36 @@ The adapter defaults `syncToFs` to `'auto'`:
|
|
|
75
71
|
That keeps bridge-heavy test workloads on the lower-memory fast path
|
|
76
72
|
without changing durability defaults for persistent databases.
|
|
77
73
|
If you use a custom `fs`, set `syncToFs` explicitly because the
|
|
78
|
-
|
|
74
|
+
bridge cannot infer whether that storage is durable.
|
|
79
75
|
|
|
80
76
|
## API
|
|
81
77
|
|
|
82
|
-
### `
|
|
78
|
+
### `createPGliteBridge(options)`
|
|
83
79
|
|
|
84
|
-
Creates a
|
|
85
|
-
instance
|
|
80
|
+
Creates a `PGliteBridge` — a bundle holding a Prisma driver adapter,
|
|
81
|
+
the underlying PGlite instance, and lifecycle helpers — backed by a
|
|
82
|
+
caller-supplied PGlite instance.
|
|
86
83
|
|
|
87
84
|
```typescript
|
|
88
85
|
const pglite = new PGlite(/* dataDir, extensions, ... */);
|
|
89
86
|
|
|
90
|
-
const
|
|
91
|
-
pglite,
|
|
92
|
-
|
|
93
|
-
sql: 'CREATE TABLE ...', // (first match wins, see Schema Resolution)
|
|
94
|
-
configRoot: '../..', // monorepo: where to find prisma.config.ts
|
|
95
|
-
max: 1, // pool connections (default: 1, see "Pool sizing" below)
|
|
87
|
+
const bridge = await createPGliteBridge({
|
|
88
|
+
pglite, // required — caller owns lifecycle
|
|
89
|
+
max: 1, // pool connections (default: 1)
|
|
96
90
|
statsLevel: 'off', // 'off' | 'basic' | 'full' (default: 'off')
|
|
91
|
+
syncToFs: 'auto', // 'auto' | true | false (default: 'auto')
|
|
97
92
|
});
|
|
98
93
|
```
|
|
99
94
|
|
|
100
|
-
|
|
95
|
+
To apply schema SQL, call [`pushMigrations`](#pushmigrationstarget-options)
|
|
96
|
+
or [`pushSchema`](#pushschematarget-options) on the returned
|
|
97
|
+
bridge — see [Populating the database](#populating-the-database).
|
|
98
|
+
|
|
99
|
+
Returns a `PGliteBridge`:
|
|
101
100
|
|
|
102
101
|
- `adapter` — pass to `new PrismaClient({ adapter })`
|
|
102
|
+
- `pglite` — the caller-supplied PGlite instance, re-exposed for
|
|
103
|
+
symmetry with `pushSchema` / `pushMigrations`
|
|
103
104
|
- `resetDb()` — truncates all user tables and discards
|
|
104
105
|
session-local state via `DISCARD ALL` (for example `SET`
|
|
105
106
|
variables, prepared statements, temp tables, and `LISTEN`
|
|
@@ -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.
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
- `
|
|
116
|
+
- `bridgeId` — a unique `symbol` identifying this bridge. Use it
|
|
116
117
|
to filter events from the public
|
|
117
118
|
[diagnostics channels](#diagnostics-channels) when multiple
|
|
118
|
-
|
|
119
|
+
bridges share a process.
|
|
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), `
|
|
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 `
|
|
231
|
+
`max`, `bridgeId`, and `syncToFs`.
|
|
141
232
|
|
|
142
|
-
### `
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
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 {
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
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 {
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
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 {
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
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 {
|
|
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
|
|
341
|
-
|
|
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
|
|
353
|
-
it from trusted, version-controlled source
|
|
354
|
-
environment variables, network input, or values
|
|
355
|
-
|
|
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
|
|
363
|
-
|
|
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
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
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
|
|
585
|
+
const bridge = await createPGliteBridge({
|
|
448
586
|
pglite,
|
|
449
|
-
migrationsPath: './prisma/migrations',
|
|
450
587
|
statsLevel: 'basic', // or 'full'
|
|
451
588
|
});
|
|
452
|
-
|
|
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` —
|
|
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
|
|
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
|
|
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
|
-
|
|
663
|
+
createPGliteBridge,
|
|
527
664
|
QUERY_CHANNEL,
|
|
528
665
|
type QueryEvent,
|
|
529
666
|
} from 'prisma-pglite-bridge';
|
|
530
667
|
|
|
531
|
-
const {
|
|
668
|
+
const { bridgeId } = await createPGliteBridge({ /* ... */ });
|
|
532
669
|
|
|
533
670
|
const listener = (msg: unknown) => {
|
|
534
671
|
const e = msg as QueryEvent;
|
|
535
|
-
if (e.
|
|
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: `{
|
|
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: `{
|
|
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 `
|
|
553
|
-
share a process. Obtain it from the `
|
|
689
|
+
Filter on `bridgeId` to isolate events when multiple bridges
|
|
690
|
+
share a process. Obtain it from the `createPGliteBridge()` or
|
|
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 `
|
|
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.
|
|
565
|
-
transactions (one at a time), but `SET`
|
|
566
|
-
connections within a single test. `resetDb()`
|
|
567
|
-
between tests via `DISCARD ALL`.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
`
|
|
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
|