prisma-pglite-bridge 0.3.2 → 0.4.1
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/dist/index.cjs +151 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -3
- package/dist/index.d.mts +24 -3
- package/dist/index.mjs +151 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.d.mts
CHANGED
|
@@ -26,7 +26,15 @@ type ResetSnapshotFn = () => Promise<void>;
|
|
|
26
26
|
interface PgliteAdapter {
|
|
27
27
|
/** Prisma adapter — pass directly to `new PrismaClient({ adapter })` */
|
|
28
28
|
adapter: PrismaPg;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The underlying PGlite instance for direct SQL, snapshots, or extensions.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Direct `pglite.exec()` / `pglite.query()` calls bypass the pool's
|
|
34
|
+
* {@link SessionLock}. Avoid mixing direct calls with Prisma operations
|
|
35
|
+
* inside a transaction — use them only for setup, teardown, or utilities
|
|
36
|
+
* that run outside active Prisma transactions.
|
|
37
|
+
*/
|
|
30
38
|
pglite: _$_electric_sql_pglite0.PGlite;
|
|
31
39
|
/** Clear all user tables. Call in `beforeEach` for per-test isolation. */
|
|
32
40
|
resetDb: ResetDbFn;
|
|
@@ -67,6 +75,9 @@ interface PoolResult {
|
|
|
67
75
|
/**
|
|
68
76
|
* Creates a pg.Pool where every connection is an in-process PGlite bridge.
|
|
69
77
|
*
|
|
78
|
+
* Most users should prefer {@link createPgliteAdapter}, which wraps this
|
|
79
|
+
* function and also handles schema application and reset/snapshot lifecycle.
|
|
80
|
+
*
|
|
70
81
|
* ```typescript
|
|
71
82
|
* import { createPool } from 'prisma-pglite-bridge';
|
|
72
83
|
* import { PrismaPg } from '@prisma/adapter-pg';
|
|
@@ -76,12 +87,24 @@ interface PoolResult {
|
|
|
76
87
|
* const adapter = new PrismaPg(pool);
|
|
77
88
|
* const prisma = new PrismaClient({ adapter });
|
|
78
89
|
* ```
|
|
90
|
+
*
|
|
91
|
+
* @see {@link createPgliteAdapter} for the higher-level API with schema management.
|
|
79
92
|
*/
|
|
80
93
|
declare const createPool: (options?: CreatePoolOptions) => Promise<PoolResult>;
|
|
81
94
|
//#endregion
|
|
82
95
|
//#region src/session-lock.d.ts
|
|
83
96
|
/** Opaque bridge identity token */
|
|
84
97
|
type BridgeId = symbol;
|
|
98
|
+
/**
|
|
99
|
+
* Coordinates PGlite access across concurrent pool connections.
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* PGlite runs PostgreSQL in single-user mode — one session shared by all
|
|
103
|
+
* bridges. The session lock tracks which bridge owns the session during
|
|
104
|
+
* transactions, preventing interleaving. Used internally by {@link PGliteBridge}
|
|
105
|
+
* and created automatically by {@link createPool}. Only instantiate directly
|
|
106
|
+
* if building a custom pool setup.
|
|
107
|
+
*/
|
|
85
108
|
declare class SessionLock {
|
|
86
109
|
private owner;
|
|
87
110
|
private waitQueue;
|
|
@@ -124,7 +147,6 @@ declare class PGliteBridge extends Duplex {
|
|
|
124
147
|
private readonly bridgeId;
|
|
125
148
|
/** Incoming bytes not yet compacted into buf */
|
|
126
149
|
private pending;
|
|
127
|
-
private pendingLen;
|
|
128
150
|
/** Compacted input buffer for message framing */
|
|
129
151
|
private buf;
|
|
130
152
|
private phase;
|
|
@@ -134,7 +156,6 @@ declare class PGliteBridge extends Duplex {
|
|
|
134
156
|
private drainQueue;
|
|
135
157
|
/** Buffered EQP messages awaiting Sync */
|
|
136
158
|
private pipeline;
|
|
137
|
-
private pipelineLen;
|
|
138
159
|
constructor(pglite: PGlite, sessionLock?: SessionLock);
|
|
139
160
|
connect(): this;
|
|
140
161
|
setKeepAlive(): this;
|
package/dist/index.mjs
CHANGED
|
@@ -35,6 +35,16 @@ const extractRfqStatus = (response) => {
|
|
|
35
35
|
if (response[i] === 90 && response[i + 1] === 0 && response[i + 2] === 0 && response[i + 3] === 0 && response[i + 4] === 5) return response[i + 5] ?? null;
|
|
36
36
|
return null;
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Coordinates PGlite access across concurrent pool connections.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* PGlite runs PostgreSQL in single-user mode — one session shared by all
|
|
43
|
+
* bridges. The session lock tracks which bridge owns the session during
|
|
44
|
+
* transactions, preventing interleaving. Used internally by {@link PGliteBridge}
|
|
45
|
+
* and created automatically by {@link createPool}. Only instantiate directly
|
|
46
|
+
* if building a custom pool setup.
|
|
47
|
+
*/
|
|
38
48
|
var SessionLock = class {
|
|
39
49
|
owner = null;
|
|
40
50
|
waitQueue = [];
|
|
@@ -43,7 +53,8 @@ var SessionLock = class {
|
|
|
43
53
|
* active or if this bridge owns the current transaction. Queues otherwise.
|
|
44
54
|
*/
|
|
45
55
|
async acquire(id) {
|
|
46
|
-
if (this.owner === null
|
|
56
|
+
if (this.owner === null) return;
|
|
57
|
+
if (this.owner === id) return;
|
|
47
58
|
return new Promise((resolve) => {
|
|
48
59
|
this.waitQueue.push({
|
|
49
60
|
id,
|
|
@@ -74,9 +85,10 @@ var SessionLock = class {
|
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
drainWaitQueue() {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
const next = this.waitQueue.shift();
|
|
89
|
+
if (!next) return;
|
|
90
|
+
this.owner = next.id;
|
|
91
|
+
next.resolve();
|
|
80
92
|
}
|
|
81
93
|
};
|
|
82
94
|
//#endregion
|
|
@@ -202,7 +214,6 @@ var PGliteBridge = class extends Duplex {
|
|
|
202
214
|
bridgeId;
|
|
203
215
|
/** Incoming bytes not yet compacted into buf */
|
|
204
216
|
pending = [];
|
|
205
|
-
pendingLen = 0;
|
|
206
217
|
/** Compacted input buffer for message framing */
|
|
207
218
|
buf = Buffer.alloc(0);
|
|
208
219
|
phase = "pre_startup";
|
|
@@ -212,7 +223,6 @@ var PGliteBridge = class extends Duplex {
|
|
|
212
223
|
drainQueue = [];
|
|
213
224
|
/** Buffered EQP messages awaiting Sync */
|
|
214
225
|
pipeline = [];
|
|
215
|
-
pipelineLen = 0;
|
|
216
226
|
constructor(pglite, sessionLock) {
|
|
217
227
|
super();
|
|
218
228
|
this.pglite = pglite;
|
|
@@ -241,15 +251,11 @@ var PGliteBridge = class extends Duplex {
|
|
|
241
251
|
_read() {}
|
|
242
252
|
_write(chunk, _encoding, callback) {
|
|
243
253
|
this.pending.push(chunk);
|
|
244
|
-
this.pendingLen += chunk.length;
|
|
245
254
|
this.enqueue(callback);
|
|
246
255
|
}
|
|
247
256
|
/** Handles corked batches — pg.Client corks during prepared queries (P+B+D+E+S) */
|
|
248
257
|
_writev(chunks, callback) {
|
|
249
|
-
for (const { chunk } of chunks)
|
|
250
|
-
this.pending.push(chunk);
|
|
251
|
-
this.pendingLen += chunk.length;
|
|
252
|
-
}
|
|
258
|
+
for (const { chunk } of chunks) this.pending.push(chunk);
|
|
253
259
|
this.enqueue(callback);
|
|
254
260
|
}
|
|
255
261
|
_final(callback) {
|
|
@@ -260,9 +266,7 @@ var PGliteBridge = class extends Duplex {
|
|
|
260
266
|
_destroy(error, callback) {
|
|
261
267
|
this.tornDown = true;
|
|
262
268
|
this.pipeline.length = 0;
|
|
263
|
-
this.pipelineLen = 0;
|
|
264
269
|
this.pending.length = 0;
|
|
265
|
-
this.pendingLen = 0;
|
|
266
270
|
this.sessionLock?.release(this.bridgeId);
|
|
267
271
|
const callbacks = this.drainQueue;
|
|
268
272
|
this.drainQueue = [];
|
|
@@ -275,7 +279,6 @@ var PGliteBridge = class extends Duplex {
|
|
|
275
279
|
if (this.buf.length === 0 && this.pending.length === 1) this.buf = this.pending[0];
|
|
276
280
|
else this.buf = Buffer.concat([this.buf, ...this.pending]);
|
|
277
281
|
this.pending.length = 0;
|
|
278
|
-
this.pendingLen = 0;
|
|
279
282
|
}
|
|
280
283
|
/**
|
|
281
284
|
* Enqueue a write callback and start draining if not already running.
|
|
@@ -354,12 +357,10 @@ var PGliteBridge = class extends Duplex {
|
|
|
354
357
|
}
|
|
355
358
|
if (EQP_MESSAGES.has(msgType)) {
|
|
356
359
|
this.pipeline.push(message);
|
|
357
|
-
this.pipelineLen += message.length;
|
|
358
360
|
continue;
|
|
359
361
|
}
|
|
360
362
|
if (msgType === SYNC) {
|
|
361
363
|
this.pipeline.push(message);
|
|
362
|
-
this.pipelineLen += message.length;
|
|
363
364
|
await this.flushPipeline();
|
|
364
365
|
continue;
|
|
365
366
|
}
|
|
@@ -380,19 +381,8 @@ var PGliteBridge = class extends Duplex {
|
|
|
380
381
|
*/
|
|
381
382
|
async flushPipeline() {
|
|
382
383
|
const messages = this.pipeline;
|
|
383
|
-
const totalLen = this.pipelineLen;
|
|
384
384
|
this.pipeline = [];
|
|
385
|
-
|
|
386
|
-
let batch;
|
|
387
|
-
if (messages.length === 1) batch = messages[0] ?? new Uint8Array(0);
|
|
388
|
-
else {
|
|
389
|
-
batch = new Uint8Array(totalLen);
|
|
390
|
-
let offset = 0;
|
|
391
|
-
for (const msg of messages) {
|
|
392
|
-
batch.set(msg, offset);
|
|
393
|
-
offset += msg.length;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
385
|
+
const batch = concat(messages);
|
|
396
386
|
await this.acquireSession();
|
|
397
387
|
await this.pglite.runExclusive(async () => {
|
|
398
388
|
const chunks = [];
|
|
@@ -452,6 +442,9 @@ const { Client, Pool } = pg;
|
|
|
452
442
|
/**
|
|
453
443
|
* Creates a pg.Pool where every connection is an in-process PGlite bridge.
|
|
454
444
|
*
|
|
445
|
+
* Most users should prefer {@link createPgliteAdapter}, which wraps this
|
|
446
|
+
* function and also handles schema application and reset/snapshot lifecycle.
|
|
447
|
+
*
|
|
455
448
|
* ```typescript
|
|
456
449
|
* import { createPool } from 'prisma-pglite-bridge';
|
|
457
450
|
* import { PrismaPg } from '@prisma/adapter-pg';
|
|
@@ -461,6 +454,8 @@ const { Client, Pool } = pg;
|
|
|
461
454
|
* const adapter = new PrismaPg(pool);
|
|
462
455
|
* const prisma = new PrismaClient({ adapter });
|
|
463
456
|
* ```
|
|
457
|
+
*
|
|
458
|
+
* @see {@link createPgliteAdapter} for the higher-level API with schema management.
|
|
464
459
|
*/
|
|
465
460
|
const createPool = async (options = {}) => {
|
|
466
461
|
const { dataDir, extensions, max = 5 } = options;
|
|
@@ -511,6 +506,15 @@ const createPool = async (options = {}) => {
|
|
|
511
506
|
* ```
|
|
512
507
|
*/
|
|
513
508
|
const SNAPSHOT_SCHEMA = "_pglite_snapshot";
|
|
509
|
+
const SENTINEL_SCHEMA = "_pglite_bridge";
|
|
510
|
+
const SENTINEL_TABLE = "__initialized";
|
|
511
|
+
const SENTINEL_MARKER = "prisma-pglite-bridge:init:v1";
|
|
512
|
+
const USER_TABLES_WHERE = `schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
513
|
+
AND schemaname != '${SNAPSHOT_SCHEMA}'
|
|
514
|
+
AND schemaname != '${SENTINEL_SCHEMA}'
|
|
515
|
+
AND tablename NOT LIKE '_prisma%'`;
|
|
516
|
+
const escapeLiteral = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
517
|
+
const isValidSentinelRow = (rows) => rows.length === 1 && rows[0]?.marker === SENTINEL_MARKER && rows[0]?.version === 1;
|
|
514
518
|
/**
|
|
515
519
|
* Discover the migrations directory via Prisma's config API.
|
|
516
520
|
* Uses the same resolution as `prisma migrate dev` — reads prisma.config.ts,
|
|
@@ -563,8 +567,10 @@ const resolveSQL = async (options) => {
|
|
|
563
567
|
if (migrationsPath) {
|
|
564
568
|
const sql = tryReadMigrationFiles(migrationsPath);
|
|
565
569
|
if (sql) return sql;
|
|
570
|
+
throw new Error(`No migration.sql files found in auto-discovered path ${migrationsPath}. Run \`prisma migrate dev\` to generate migration files, or pass pre-generated SQL via the \`sql\` option.`);
|
|
566
571
|
}
|
|
567
|
-
throw new Error(
|
|
572
|
+
if (options.configRoot) throw new Error(`prisma.config.ts loaded from configRoot (${options.configRoot}) but no schema or migrations path could be resolved. Ensure your config specifies a schema path, or pass pre-generated SQL via the \`sql\` option.`);
|
|
573
|
+
throw new Error("No migration files found and no prisma.config.ts could be loaded. Run `prisma migrate dev` to generate them, or pass pre-generated SQL via the `sql` option.");
|
|
568
574
|
};
|
|
569
575
|
/**
|
|
570
576
|
* Creates a Prisma adapter backed by an in-process PGlite instance.
|
|
@@ -573,16 +579,88 @@ const resolveSQL = async (options) => {
|
|
|
573
579
|
* function for clearing tables between tests.
|
|
574
580
|
*/
|
|
575
581
|
const createPgliteAdapter = async (options = {}) => {
|
|
576
|
-
const sql = await resolveSQL(options);
|
|
577
582
|
const { pool, pglite, close: poolClose } = await createPool({
|
|
578
583
|
dataDir: options.dataDir,
|
|
579
584
|
extensions: options.extensions,
|
|
580
585
|
max: options.max
|
|
581
586
|
});
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
587
|
+
const sentinelStatements = [
|
|
588
|
+
`CREATE SCHEMA IF NOT EXISTS "${SENTINEL_SCHEMA}"`,
|
|
589
|
+
`CREATE TABLE IF NOT EXISTS "${SENTINEL_SCHEMA}"."${SENTINEL_TABLE}" (marker text PRIMARY KEY, version int NOT NULL)`,
|
|
590
|
+
`INSERT INTO "${SENTINEL_SCHEMA}"."${SENTINEL_TABLE}" (marker, version) VALUES (${escapeLiteral(SENTINEL_MARKER)}, 1) ON CONFLICT (marker) DO NOTHING`
|
|
591
|
+
];
|
|
592
|
+
const collisionError = () => `"${SENTINEL_SCHEMA}"."${SENTINEL_TABLE}" exists but is not owned by prisma-pglite-bridge. The "${SENTINEL_SCHEMA}" schema is reserved for library metadata.`;
|
|
593
|
+
const writeSentinel = async () => {
|
|
594
|
+
let committed = false;
|
|
595
|
+
try {
|
|
596
|
+
await pglite.exec(`BEGIN;\n${sentinelStatements.join(";\n")}`);
|
|
597
|
+
const { rows } = await pglite.query(`SELECT marker, version FROM "${SENTINEL_SCHEMA}"."${SENTINEL_TABLE}"`);
|
|
598
|
+
if (!isValidSentinelRow(rows)) throw new Error(collisionError());
|
|
599
|
+
await pglite.exec("COMMIT");
|
|
600
|
+
committed = true;
|
|
601
|
+
} catch (err) {
|
|
602
|
+
if (!committed) await pglite.exec("ROLLBACK");
|
|
603
|
+
throw err instanceof Error && err.message === collisionError() ? err : new Error(collisionError(), { cause: err });
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
const isInitialized = async () => {
|
|
607
|
+
const { rows: tableExists } = await pglite.query(`SELECT EXISTS (
|
|
608
|
+
SELECT 1 FROM pg_tables
|
|
609
|
+
WHERE schemaname = '${SENTINEL_SCHEMA}' AND tablename = '${SENTINEL_TABLE}'
|
|
610
|
+
) AS found`);
|
|
611
|
+
if (tableExists[0]?.found) {
|
|
612
|
+
try {
|
|
613
|
+
const { rows: allRows } = await pglite.query(`SELECT marker, version FROM "${SENTINEL_SCHEMA}"."${SENTINEL_TABLE}"`);
|
|
614
|
+
if (isValidSentinelRow(allRows)) return true;
|
|
615
|
+
} catch {}
|
|
616
|
+
throw new Error(collisionError());
|
|
617
|
+
}
|
|
618
|
+
const { rows: schemaExists } = await pglite.query(`SELECT EXISTS (
|
|
619
|
+
SELECT 1 FROM pg_namespace WHERE nspname = '${SENTINEL_SCHEMA}'
|
|
620
|
+
) AS found`);
|
|
621
|
+
if (schemaExists[0]?.found) throw new Error(`Schema "${SENTINEL_SCHEMA}" exists but is not owned by prisma-pglite-bridge. The "${SENTINEL_SCHEMA}" schema is reserved for library metadata.`);
|
|
622
|
+
const { rows: legacy } = await pglite.query(`SELECT EXISTS (
|
|
623
|
+
SELECT 1 FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
624
|
+
WHERE n.nspname = 'public'
|
|
625
|
+
UNION ALL
|
|
626
|
+
SELECT 1 FROM pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
627
|
+
WHERE n.nspname = 'public' AND t.typtype NOT IN ('b', 'p')
|
|
628
|
+
UNION ALL
|
|
629
|
+
SELECT 1 FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
630
|
+
WHERE n.nspname = 'public'
|
|
631
|
+
UNION ALL
|
|
632
|
+
SELECT 1 FROM pg_namespace
|
|
633
|
+
WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'public')
|
|
634
|
+
) AS initialized`);
|
|
635
|
+
if (legacy[0]?.initialized) {
|
|
636
|
+
await writeSentinel();
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
return false;
|
|
640
|
+
};
|
|
641
|
+
if (!options.dataDir || !await isInitialized()) {
|
|
642
|
+
const sql = await resolveSQL(options);
|
|
643
|
+
if (!options.sql) {
|
|
644
|
+
let committed = false;
|
|
645
|
+
try {
|
|
646
|
+
await pglite.exec(`BEGIN;\n${sql};\n${sentinelStatements.join(";\n")}`);
|
|
647
|
+
const { rows: verify } = await pglite.query(`SELECT marker, version FROM "${SENTINEL_SCHEMA}"."${SENTINEL_TABLE}"`);
|
|
648
|
+
if (!isValidSentinelRow(verify)) throw new Error(collisionError());
|
|
649
|
+
await pglite.exec("COMMIT");
|
|
650
|
+
committed = true;
|
|
651
|
+
} catch (err) {
|
|
652
|
+
if (!committed) await pglite.exec("ROLLBACK");
|
|
653
|
+
if (err instanceof Error && err.message === collisionError()) throw err;
|
|
654
|
+
throw new Error("Failed to apply schema SQL to PGlite. Check your schema or migration files.", { cause: err });
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
try {
|
|
658
|
+
await pglite.exec(sql);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
throw new Error("Failed to apply schema SQL to PGlite. Check your schema or migration files.", { cause: err });
|
|
661
|
+
}
|
|
662
|
+
await writeSentinel();
|
|
663
|
+
}
|
|
586
664
|
}
|
|
587
665
|
const adapter = new PrismaPg(pool);
|
|
588
666
|
let cachedTables = null;
|
|
@@ -591,23 +669,38 @@ const createPgliteAdapter = async (options = {}) => {
|
|
|
591
669
|
if (cachedTables !== null) return cachedTables;
|
|
592
670
|
const { rows } = await pglite.query(`SELECT quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified
|
|
593
671
|
FROM pg_tables
|
|
594
|
-
WHERE
|
|
595
|
-
AND schemaname != '${SNAPSHOT_SCHEMA}'
|
|
596
|
-
AND tablename NOT LIKE '_prisma%'`);
|
|
672
|
+
WHERE ${USER_TABLES_WHERE}`);
|
|
597
673
|
cachedTables = rows.length > 0 ? rows.map((r) => r.qualified).join(", ") : "";
|
|
598
674
|
return cachedTables;
|
|
599
675
|
};
|
|
600
676
|
const snapshotDb = async () => {
|
|
601
677
|
await pglite.exec(`DROP SCHEMA IF EXISTS "${SNAPSHOT_SCHEMA}" CASCADE`);
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
678
|
+
try {
|
|
679
|
+
await pglite.exec("BEGIN");
|
|
680
|
+
await pglite.exec(`CREATE SCHEMA "${SNAPSHOT_SCHEMA}"`);
|
|
681
|
+
const { rows: tables } = await pglite.query(`SELECT schemaname, tablename,
|
|
682
|
+
quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified
|
|
683
|
+
FROM pg_tables
|
|
684
|
+
WHERE ${USER_TABLES_WHERE}`);
|
|
685
|
+
await pglite.exec(`CREATE TABLE "${SNAPSHOT_SCHEMA}".__tables (snap_name text, source_schema text, source_table text)`);
|
|
686
|
+
for (const [i, { schemaname, tablename, qualified }] of tables.entries()) {
|
|
687
|
+
const snapName = `_snap_${i}`;
|
|
688
|
+
await pglite.exec(`CREATE TABLE "${SNAPSHOT_SCHEMA}"."${snapName}" AS SELECT * FROM ${qualified}`);
|
|
689
|
+
await pglite.exec(`INSERT INTO "${SNAPSHOT_SCHEMA}".__tables VALUES (${escapeLiteral(snapName)}, ${escapeLiteral(schemaname)}, ${escapeLiteral(tablename)})`);
|
|
690
|
+
}
|
|
691
|
+
const { rows: seqs } = await pglite.query(`SELECT quote_literal(schemaname || '.' || sequencename) AS name, last_value::text AS value
|
|
692
|
+
FROM pg_sequences
|
|
693
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
694
|
+
AND schemaname != '${SNAPSHOT_SCHEMA}'
|
|
695
|
+
AND last_value IS NOT NULL`);
|
|
696
|
+
await pglite.exec(`CREATE TABLE "${SNAPSHOT_SCHEMA}".__sequences (name text, value bigint)`);
|
|
697
|
+
for (const { name, value } of seqs) await pglite.exec(`INSERT INTO "${SNAPSHOT_SCHEMA}".__sequences VALUES (${name}, ${value})`);
|
|
698
|
+
await pglite.exec("COMMIT");
|
|
699
|
+
} catch (err) {
|
|
700
|
+
await pglite.exec("ROLLBACK");
|
|
701
|
+
await pglite.exec(`DROP SCHEMA IF EXISTS "${SNAPSHOT_SCHEMA}" CASCADE`);
|
|
702
|
+
throw err;
|
|
703
|
+
}
|
|
611
704
|
hasSnapshot = true;
|
|
612
705
|
};
|
|
613
706
|
const resetSnapshot = async () => {
|
|
@@ -615,23 +708,22 @@ const createPgliteAdapter = async (options = {}) => {
|
|
|
615
708
|
await pglite.exec(`DROP SCHEMA IF EXISTS "${SNAPSHOT_SCHEMA}" CASCADE`);
|
|
616
709
|
};
|
|
617
710
|
const resetDb = async () => {
|
|
711
|
+
cachedTables = null;
|
|
618
712
|
const tables = await discoverTables();
|
|
619
|
-
if (hasSnapshot && tables) {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
AND tablename != '__sequences'`);
|
|
626
|
-
for (const { tablename } of snapshotTables) await pglite.exec(`INSERT INTO public.${tablename} SELECT * FROM "${SNAPSHOT_SCHEMA}".${tablename}`);
|
|
627
|
-
} finally {
|
|
628
|
-
await pglite.exec("SET session_replication_role = DEFAULT");
|
|
629
|
-
}
|
|
713
|
+
if (hasSnapshot && tables) try {
|
|
714
|
+
await pglite.exec("SET session_replication_role = replica");
|
|
715
|
+
await pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
|
|
716
|
+
const { rows: snapshotTables } = await pglite.query(`SELECT snap_name, quote_ident(source_schema) || '.' || quote_ident(source_table) AS qualified
|
|
717
|
+
FROM "${SNAPSHOT_SCHEMA}".__tables`);
|
|
718
|
+
for (const { snap_name, qualified } of snapshotTables) await pglite.exec(`INSERT INTO ${qualified} SELECT * FROM "${SNAPSHOT_SCHEMA}"."${snap_name}"`);
|
|
630
719
|
const { rows: seqs } = await pglite.query(`SELECT quote_literal(name) AS name, value::text AS value FROM "${SNAPSHOT_SCHEMA}".__sequences`);
|
|
631
720
|
for (const { name, value } of seqs) await pglite.exec(`SELECT setval(${name}, ${value})`);
|
|
632
|
-
}
|
|
721
|
+
} finally {
|
|
722
|
+
await pglite.exec("SET session_replication_role = DEFAULT");
|
|
723
|
+
}
|
|
724
|
+
else if (tables) try {
|
|
633
725
|
await pglite.exec("SET session_replication_role = replica");
|
|
634
|
-
await pglite.exec(`TRUNCATE TABLE ${tables} CASCADE`);
|
|
726
|
+
await pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
|
|
635
727
|
} finally {
|
|
636
728
|
await pglite.exec("SET session_replication_role = DEFAULT");
|
|
637
729
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/session-lock.ts","../src/pglite-bridge.ts","../src/create-pool.ts","../src/create-pglite-adapter.ts"],"sourcesContent":["/**\n * Session-level lock for PGlite's single-session model.\n *\n * PGlite runs PostgreSQL in single-user mode — one session shared by all\n * bridges. runExclusive serializes individual operations, but transactions\n * span multiple operations. Without session-level locking, Bridge A's BEGIN\n * and Bridge B's query interleave, corrupting transaction boundaries.\n *\n * The session lock tracks which bridge owns the session. When PGlite enters\n * transaction state (ReadyForQuery status 'T' or 'E'), the owning bridge\n * gets exclusive access until the transaction completes (status returns to 'I').\n *\n * Non-transactional operations from any bridge are allowed when no transaction\n * is active — they serialize naturally through runExclusive.\n */\n\n// ReadyForQuery status bytes\nconst STATUS_IDLE = 0x49; // 'I' — no transaction\nconst STATUS_IN_TRANSACTION = 0x54; // 'T' — in transaction block\nconst STATUS_FAILED = 0x45; // 'E' — failed transaction block\n\n/** Opaque bridge identity token */\nexport type BridgeId = symbol;\n\nexport const createBridgeId = (): BridgeId => Symbol('bridge');\n\n/**\n * Extracts the ReadyForQuery status byte from a response buffer.\n * Scans from the end since RFQ is always the last message.\n * Returns null if no RFQ found.\n */\nexport const extractRfqStatus = (response: Uint8Array): number | null => {\n // RFQ is always 6 bytes: Z(5a) + length(00000005) + status\n // It's the last message in the response\n if (response.length < 6) return null;\n const i = response.length - 6;\n if (\n response[i] === 0x5a &&\n response[i + 1] === 0x00 &&\n response[i + 2] === 0x00 &&\n response[i + 3] === 0x00 &&\n response[i + 4] === 0x05\n ) {\n return response[i + 5] ?? null;\n }\n return null;\n};\n\nexport class SessionLock {\n private owner: BridgeId | null = null;\n private waitQueue: Array<{ id: BridgeId; resolve: () => void }> = [];\n\n /**\n * Acquire access to PGlite. Resolves immediately if no transaction is\n * active or if this bridge owns the current transaction. Queues otherwise.\n */\n async acquire(id: BridgeId): Promise<void> {\n if (this.owner === null || this.owner === id) return;\n\n // Another bridge owns the session (in a transaction) — wait\n return new Promise<void>((resolve) => {\n this.waitQueue.push({ id, resolve });\n });\n }\n\n /**\n * Update session state based on the ReadyForQuery status byte.\n * Call after every PGlite response that contains RFQ.\n */\n updateStatus(id: BridgeId, status: number): void {\n if (status === STATUS_IN_TRANSACTION || status === STATUS_FAILED) {\n // This bridge now owns the session\n this.owner = id;\n } else if (status === STATUS_IDLE) {\n // Transaction complete — release ownership\n if (this.owner === id) {\n this.owner = null;\n this.drainWaitQueue();\n }\n }\n }\n\n /**\n * Release ownership (e.g., when a bridge is destroyed mid-transaction).\n */\n release(id: BridgeId): void {\n if (this.owner === id) {\n this.owner = null;\n this.drainWaitQueue();\n }\n }\n\n private drainWaitQueue(): void {\n // Wake all waiting bridges — they'll serialize through runExclusive\n const waiters = this.waitQueue;\n this.waitQueue = [];\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n}\n","/**\n * PGlite bridge stream.\n *\n * A Duplex stream that replaces the TCP socket in pg.Client, routing\n * wire protocol messages directly to an in-process PGlite instance.\n *\n * pg.Client writes wire protocol bytes → bridge frames messages →\n * PGlite processes via execProtocolRawStream → bridge pushes responses back.\n *\n * Extended Query Protocol pipelines (Parse→Bind→Describe→Execute→Sync) are\n * concatenated into a single buffer and sent as one atomic execProtocolRawStream\n * call within one runExclusive. This prevents portal interleaving between\n * concurrent bridges AND reduces async overhead (1 WASM call instead of 5).\n *\n * The response from a batched pipeline contains spurious ReadyForQuery messages\n * after each sub-message (PGlite's single-user mode). These are stripped,\n * keeping only the final ReadyForQuery after Sync.\n */\nimport { Duplex } from 'node:stream';\nimport type { PGlite } from '@electric-sql/pglite';\nimport {\n type BridgeId,\n createBridgeId,\n extractRfqStatus,\n type SessionLock,\n} from './session-lock.ts';\n\n// Frontend message types\nconst PARSE = 0x50; // P\nconst BIND = 0x42; // B\nconst DESCRIBE = 0x44; // D\nconst EXECUTE = 0x45; // E\nconst CLOSE = 0x43; // C\nconst FLUSH = 0x48; // H\nconst SYNC = 0x53; // S (frontend)\nconst TERMINATE = 0x58; // X\n\n// Backend message type\nconst READY_FOR_QUERY = 0x5a; // Z — 6 bytes: Z + length(5) + status\n\n// Extended Query Protocol message types — must be batched until Sync\nconst EQP_MESSAGES = new Set([PARSE, BIND, DESCRIBE, EXECUTE, CLOSE, FLUSH]);\n\n/**\n * Strips all intermediate ReadyForQuery messages from a response, keeping\n * only the last one. PGlite's single-user mode emits RFQ after every\n * sub-message; pg.Client expects exactly one after Sync.\n *\n * Operates in-place on the response by building a list of byte ranges to\n * keep, then assembling the result. Returns the original buffer (no copy)\n * if there are 0 or 1 RFQ messages.\n */\n/** @internal — exported for testing only */\nexport const stripIntermediateReadyForQuery = (response: Uint8Array): Uint8Array => {\n // Quick scan: count RFQ occurrences and find their positions\n const rfqPositions: number[] = [];\n let offset = 0;\n\n while (offset < response.length) {\n if (offset + 5 >= response.length) break;\n\n if (\n response[offset] === READY_FOR_QUERY &&\n response[offset + 1] === 0x00 &&\n response[offset + 2] === 0x00 &&\n response[offset + 3] === 0x00 &&\n response[offset + 4] === 0x05\n ) {\n rfqPositions.push(offset);\n offset += 6;\n } else {\n // Skip this backend message: type(1) + length(4, big-endian)\n const b1 = response[offset + 1];\n const b2 = response[offset + 2];\n const b3 = response[offset + 3];\n const b4 = response[offset + 4];\n if (b1 === undefined || b2 === undefined || b3 === undefined || b4 === undefined) break;\n const msgLen = ((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) >>> 0;\n if (msgLen < 4) break; // malformed — minimum length field is 4 (includes itself)\n offset += 1 + msgLen;\n }\n }\n\n if (rfqPositions.length <= 1) return response;\n\n // Build result: copy everything except intermediate RFQ messages (all but last)\n const removeCount = rfqPositions.length - 1;\n const resultLen = response.length - removeCount * 6;\n const result = new Uint8Array(resultLen);\n let src = 0;\n let dst = 0;\n let removeIdx = 0;\n\n while (src < response.length) {\n const nextRemove =\n removeIdx < removeCount ? (rfqPositions[removeIdx] ?? response.length) : response.length;\n if (src < nextRemove) {\n const copyLen = nextRemove - src;\n result.set(response.subarray(src, src + copyLen), dst);\n dst += copyLen;\n src += copyLen;\n }\n if (removeIdx < removeCount && src === rfqPositions[removeIdx]) {\n src += 6;\n removeIdx++;\n }\n }\n\n return result;\n};\n\n/**\n * Concatenates multiple Uint8Array views into one contiguous buffer.\n */\nconst concat = (parts: Uint8Array[]): Uint8Array => {\n if (parts.length === 1) return parts[0] ?? new Uint8Array(0);\n const total = parts.reduce((sum, p) => sum + p.length, 0);\n const result = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n result.set(part, offset);\n offset += part.length;\n }\n return result;\n};\n\n/**\n * Duplex stream that bridges `pg.Client` to an in-process PGlite instance.\n *\n * Replaces the TCP socket in `pg.Client` via the `stream` option. Speaks\n * PostgreSQL wire protocol directly to PGlite — no TCP, no serialization\n * overhead beyond what the wire protocol requires.\n *\n * Pass to `pg.Client` or use via `createPool()` / `createPgliteAdapter()`:\n *\n * ```typescript\n * const client = new pg.Client({\n * stream: () => new PGliteBridge(pglite),\n * });\n * ```\n */\nexport class PGliteBridge extends Duplex {\n private readonly pglite: PGlite;\n private readonly sessionLock: SessionLock | null;\n private readonly bridgeId: BridgeId;\n /** Incoming bytes not yet compacted into buf */\n private pending: Buffer[] = [];\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: used via this.pendingLen\n private pendingLen = 0;\n /** Compacted input buffer for message framing */\n private buf: Buffer = Buffer.alloc(0);\n private phase: 'pre_startup' | 'ready' = 'pre_startup';\n private draining = false;\n private tornDown = false;\n /** Callbacks waiting for drain to process their data */\n private drainQueue: Array<(error?: Error | null) => void> = [];\n /** Buffered EQP messages awaiting Sync */\n private pipeline: Uint8Array[] = [];\n private pipelineLen = 0;\n\n constructor(pglite: PGlite, sessionLock?: SessionLock) {\n super();\n this.pglite = pglite;\n this.sessionLock = sessionLock ?? null;\n this.bridgeId = createBridgeId();\n }\n\n // ── Socket compatibility (called by pg's Connection) ──\n\n connect(): this {\n setImmediate(() => this.emit('connect'));\n return this;\n }\n\n setKeepAlive(): this {\n return this;\n }\n\n setNoDelay(): this {\n return this;\n }\n\n setTimeout(): this {\n return this;\n }\n\n ref(): this {\n return this;\n }\n\n unref(): this {\n return this;\n }\n\n // ── Duplex implementation ──\n\n override _read(): void {\n // Data is pushed proactively when PGlite responses arrive\n }\n\n override _write(\n chunk: Buffer,\n _encoding: BufferEncoding,\n callback: (error?: Error | null) => void,\n ): void {\n this.pending.push(chunk);\n this.pendingLen += chunk.length;\n this.enqueue(callback);\n }\n\n /** Handles corked batches — pg.Client corks during prepared queries (P+B+D+E+S) */\n override _writev(\n chunks: Array<{ chunk: Buffer; encoding: BufferEncoding }>,\n callback: (error?: Error | null) => void,\n ): void {\n for (const { chunk } of chunks) {\n this.pending.push(chunk);\n this.pendingLen += chunk.length;\n }\n this.enqueue(callback);\n }\n\n override _final(callback: (error?: Error | null) => void): void {\n this.sessionLock?.release(this.bridgeId);\n this.push(null);\n callback();\n }\n\n override _destroy(error: Error | null, callback: (error?: Error | null) => void): void {\n this.tornDown = true;\n this.pipeline.length = 0;\n this.pipelineLen = 0;\n this.pending.length = 0;\n this.pendingLen = 0;\n this.sessionLock?.release(this.bridgeId);\n\n // Flush pending write callbacks so pg.Client doesn't hang\n const callbacks = this.drainQueue;\n this.drainQueue = [];\n for (const cb of callbacks) {\n cb(error);\n }\n\n callback(error);\n }\n\n // ── Message processing ──\n\n /** Merge pending chunks into buf only when needed for framing */\n private compact(): void {\n if (this.pending.length === 0) return;\n if (this.buf.length === 0 && this.pending.length === 1) {\n this.buf = this.pending[0] as Buffer;\n } else {\n this.buf = Buffer.concat([this.buf, ...this.pending]);\n }\n this.pending.length = 0;\n this.pendingLen = 0;\n }\n\n /**\n * Enqueue a write callback and start draining if not already running.\n * The callback is NOT called until drain has processed the data.\n */\n private enqueue(callback: (error?: Error | null) => void): void {\n this.drainQueue.push(callback);\n if (!this.draining) {\n // Errors are propagated through drainQueue callbacks, not through this promise\n this.drain().catch(() => {});\n }\n }\n\n /**\n * Process all pending data, looping until no new data arrives.\n * Fires all queued callbacks on completion or error.\n */\n private async drain(): Promise<void> {\n if (this.draining) return;\n this.draining = true;\n\n let error: Error | null = null;\n\n try {\n // Loop until no more pending data to process\n while (this.pending.length > 0 || this.buf.length > 0) {\n if (this.tornDown) break;\n\n if (this.phase === 'pre_startup') {\n await this.processPreStartup();\n }\n if (this.phase === 'ready') {\n await this.processMessages();\n }\n\n // If processMessages couldn't consume anything (incomplete message),\n // stop looping — more data will arrive via _write\n if (this.pending.length === 0) break;\n }\n } catch (err) {\n error = err instanceof Error ? err : new Error(String(err));\n // Release session lock on error — prevents permanent deadlock if\n // PGlite crashes mid-transaction (other bridges would wait forever)\n this.sessionLock?.release(this.bridgeId);\n } finally {\n this.draining = false;\n\n // Fire all waiting callbacks\n const callbacks = this.drainQueue;\n this.drainQueue = [];\n for (const cb of callbacks) {\n cb(error);\n }\n }\n }\n\n /**\n * Frames and processes the startup message.\n *\n * Format: [4 bytes: total length] [4 bytes: protocol version] [key\\0value\\0 pairs]\n * No type byte — length includes itself.\n */\n private async processPreStartup(): Promise<void> {\n this.compact();\n if (this.buf.length < 4) return;\n const len = this.buf.readInt32BE(0);\n if (this.buf.length < len) return;\n\n const message = this.buf.subarray(0, len);\n this.buf = this.buf.subarray(len);\n\n await this.acquireSession();\n await this.pglite.runExclusive(async () => {\n await this.execAndPush(message);\n });\n\n this.phase = 'ready';\n }\n\n /**\n * Frames and processes regular wire protocol messages.\n *\n * Extended Query Protocol messages (Parse, Bind, Describe, Execute, Close,\n * Flush) are buffered in `this.pipeline`. When Sync arrives, the entire\n * pipeline is concatenated and sent to PGlite as one atomic\n * execProtocolRawStream call within one runExclusive.\n *\n * SimpleQuery messages are sent directly (they're self-contained).\n */\n private async processMessages(): Promise<void> {\n this.compact();\n while (this.buf.length >= 5) {\n const len = 1 + this.buf.readInt32BE(1);\n if (len < 5 || this.buf.length < len) break;\n\n const message = this.buf.subarray(0, len);\n this.buf = this.buf.subarray(len);\n const msgType = message[0] ?? 0;\n\n if (msgType === TERMINATE) {\n this.sessionLock?.release(this.bridgeId);\n this.push(null);\n return;\n }\n\n if (EQP_MESSAGES.has(msgType)) {\n this.pipeline.push(message);\n this.pipelineLen += message.length;\n continue;\n }\n\n if (msgType === SYNC) {\n this.pipeline.push(message);\n this.pipelineLen += message.length;\n await this.flushPipeline();\n continue;\n }\n\n // SimpleQuery or other standalone message\n await this.acquireSession();\n await this.pglite.runExclusive(async () => {\n await this.execAndPush(message);\n });\n }\n }\n\n /**\n * Sends the accumulated EQP pipeline as one atomic operation.\n *\n * All buffered messages are concatenated into a single buffer and sent\n * as one execProtocolRawStream call. This is both correct (prevents\n * portal interleaving) and fast (1 WASM call + 1 async boundary instead\n * of 5). Intermediate ReadyForQuery messages are stripped from the\n * combined response.\n */\n private async flushPipeline(): Promise<void> {\n const messages = this.pipeline;\n const totalLen = this.pipelineLen;\n this.pipeline = [];\n this.pipelineLen = 0;\n\n // Concatenate pipeline into one buffer\n let batch: Uint8Array;\n if (messages.length === 1) {\n batch = messages[0] ?? new Uint8Array(0);\n } else {\n batch = new Uint8Array(totalLen);\n let offset = 0;\n for (const msg of messages) {\n batch.set(msg, offset);\n offset += msg.length;\n }\n }\n\n await this.acquireSession();\n await this.pglite.runExclusive(async () => {\n const chunks: Uint8Array[] = [];\n\n await this.pglite.execProtocolRawStream(batch, {\n onRawData: (chunk: Uint8Array) => chunks.push(chunk),\n });\n\n if (this.tornDown || chunks.length === 0) return;\n\n // Single chunk: strip intermediate RFQ and push\n if (chunks.length === 1) {\n const raw = chunks[0] ?? new Uint8Array(0);\n this.trackSessionStatus(raw);\n const cleaned = stripIntermediateReadyForQuery(raw);\n if (cleaned.length > 0) this.push(cleaned);\n return;\n }\n\n // Multiple chunks: concat first, then strip\n const combined = concat(chunks);\n this.trackSessionStatus(combined);\n const cleaned = stripIntermediateReadyForQuery(combined);\n if (cleaned.length > 0) this.push(cleaned);\n });\n }\n\n /**\n * Sends a message to PGlite and pushes response chunks directly to the\n * stream as they arrive. Avoids collecting and concatenating for large\n * multi-row responses (e.g., findMany 500 rows = ~503 onRawData chunks).\n *\n * Must be called inside runExclusive.\n */\n private async execAndPush(message: Uint8Array): Promise<void> {\n let lastChunk: Uint8Array | null = null;\n await this.pglite.execProtocolRawStream(message, {\n onRawData: (chunk: Uint8Array) => {\n if (!this.tornDown && chunk.length > 0) {\n this.push(chunk);\n lastChunk = chunk;\n }\n },\n });\n if (lastChunk) this.trackSessionStatus(lastChunk);\n }\n\n // ── Session lock helpers ──\n\n private async acquireSession(): Promise<void> {\n await this.sessionLock?.acquire(this.bridgeId);\n }\n\n private trackSessionStatus(response: Uint8Array): void {\n if (!this.sessionLock) return;\n const status = extractRfqStatus(response);\n if (status !== null) {\n this.sessionLock.updateStatus(this.bridgeId, status);\n }\n }\n}\n","/**\n * Pool factory — creates a pg.Pool backed by an in-process PGlite instance.\n *\n * Each pool connection gets its own PGliteBridge stream, all sharing the\n * same PGlite WASM instance and SessionLock. The session lock ensures\n * transaction isolation: when one bridge starts a transaction (BEGIN),\n * it gets exclusive PGlite access until COMMIT/ROLLBACK. Non-transactional\n * operations from any bridge serialize through PGlite's runExclusive mutex.\n */\nimport { type Extensions, PGlite } from '@electric-sql/pglite';\nimport pg from 'pg';\nimport { PGliteBridge } from './pglite-bridge.ts';\nimport { SessionLock } from './session-lock.ts';\n\nconst { Client, Pool } = pg;\n\nexport interface CreatePoolOptions {\n /** PGlite data directory. Omit for in-memory. */\n dataDir?: string;\n\n /** PGlite extensions (e.g., `{ uuid_ossp: uuidOssp() }`) */\n extensions?: Extensions;\n\n /** Maximum pool connections (default: 5) */\n max?: number;\n\n /** Existing PGlite instance to use instead of creating one */\n pglite?: PGlite;\n}\n\nexport interface PoolResult {\n /** pg.Pool backed by PGlite — pass to PrismaPg */\n pool: pg.Pool;\n\n /** The underlying PGlite instance */\n pglite: PGlite;\n\n /** Shut down pool and PGlite */\n close: () => Promise<void>;\n}\n\n/**\n * Creates a pg.Pool where every connection is an in-process PGlite bridge.\n *\n * ```typescript\n * import { createPool } from 'prisma-pglite-bridge';\n * import { PrismaPg } from '@prisma/adapter-pg';\n * import { PrismaClient } from '@prisma/client';\n *\n * const { pool, close } = await createPool();\n * const adapter = new PrismaPg(pool);\n * const prisma = new PrismaClient({ adapter });\n * ```\n */\nexport const createPool = async (options: CreatePoolOptions = {}): Promise<PoolResult> => {\n const { dataDir, extensions, max = 5 } = options;\n const ownsInstance = !options.pglite;\n\n const pglite = options.pglite ?? new PGlite(dataDir, extensions ? { extensions } : undefined);\n await pglite.waitReady;\n\n const sessionLock = new SessionLock();\n\n // Subclass pg.Client to inject PGliteBridge as the stream\n const BridgedClient = class extends Client {\n constructor(config?: string | pg.ClientConfig) {\n const cfg = typeof config === 'string' ? { connectionString: config } : (config ?? {});\n super({\n ...cfg,\n user: 'postgres',\n database: 'postgres',\n stream: (() => new PGliteBridge(pglite, sessionLock)) as pg.ClientConfig['stream'],\n });\n }\n };\n\n const pool = new Pool({\n Client: BridgedClient as typeof Client,\n max,\n });\n\n const close = async () => {\n await pool.end();\n if (ownsInstance) {\n await pglite.close();\n }\n };\n\n return { pool, pglite, close };\n};\n","/**\n * Creates a Prisma adapter backed by in-process PGlite.\n *\n * No TCP, no Docker, no worker threads — everything runs in the same process.\n * Works for testing, development, seeding, and scripts.\n *\n * ```typescript\n * import { createPgliteAdapter } from 'prisma-pglite-bridge';\n * import { PrismaClient } from '@prisma/client';\n *\n * const { adapter, resetDb } = await createPgliteAdapter();\n * const prisma = new PrismaClient({ adapter });\n *\n * beforeEach(() => resetDb());\n * ```\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { PrismaPg } from '@prisma/adapter-pg';\nimport { createPool } from './create-pool.ts';\n\nconst SNAPSHOT_SCHEMA = '_pglite_snapshot';\n\nexport interface CreatePgliteAdapterOptions {\n /** Path to prisma/migrations/ directory (auto-discovered via prisma.config.ts if omitted) */\n migrationsPath?: string;\n\n /** Pre-generated SQL to apply instead of auto-generating from schema */\n sql?: string;\n\n /** Root directory for prisma.config.ts discovery (default: process.cwd()). Set this in monorepos where tests run from the workspace root. */\n configRoot?: string;\n\n /** PGlite data directory. Omit for in-memory. */\n dataDir?: string;\n\n /** PGlite extensions (e.g., `{ uuid_ossp: uuidOssp() }`) */\n extensions?: import('@electric-sql/pglite').Extensions;\n\n /** Maximum pool connections (default: 5) */\n max?: number;\n}\n\n/** Clear all user tables. Call in `beforeEach` for per-test isolation. */\nexport type ResetDbFn = () => Promise<void>;\n\nexport type SnapshotDbFn = () => Promise<void>;\n\nexport type ResetSnapshotFn = () => Promise<void>;\n\nexport interface PgliteAdapter {\n /** Prisma adapter — pass directly to `new PrismaClient({ adapter })` */\n adapter: PrismaPg;\n\n /** The underlying PGlite instance for direct SQL, snapshots, or extensions. */\n pglite: import('@electric-sql/pglite').PGlite;\n\n /** Clear all user tables. Call in `beforeEach` for per-test isolation. */\n resetDb: ResetDbFn;\n\n /** Snapshot current DB state. Subsequent `resetDb` calls restore to this snapshot. */\n snapshotDb: SnapshotDbFn;\n\n /** Discard the current snapshot. Subsequent `resetDb` calls truncate to empty. */\n resetSnapshot: ResetSnapshotFn;\n\n /** Shut down pool and PGlite. Not needed in tests (process exit handles it). */\n close: () => Promise<void>;\n}\n\n/**\n * Discover the migrations directory via Prisma's config API.\n * Uses the same resolution as `prisma migrate dev` — reads prisma.config.ts,\n * resolves paths relative to config file location.\n *\n * Returns null if @prisma/config is not available or config cannot be loaded.\n */\nconst discoverMigrationsPath = async (configRoot?: string): Promise<string | null> => {\n try {\n const { loadConfigFromFile } = await import('@prisma/config');\n const { config, error } = await loadConfigFromFile({ configRoot: configRoot ?? process.cwd() });\n if (error) return null;\n\n // Explicit migrations path from prisma.config.ts\n if (config.migrations?.path) return config.migrations.path;\n\n // Fallback: Prisma convention is {schemaDir}/migrations\n const schemaPath = config.schema;\n if (schemaPath) return join(dirname(schemaPath), 'migrations');\n\n return null;\n } catch {\n return null;\n }\n};\n\n/**\n * Read migration SQL files from a migrations directory in directory order.\n * Returns null if the directory doesn't exist or has no migration files.\n */\nconst tryReadMigrationFiles = (migrationsPath: string): string | null => {\n if (!existsSync(migrationsPath)) return null;\n\n const dirs = readdirSync(migrationsPath)\n .filter((d) => statSync(join(migrationsPath, d)).isDirectory())\n .sort();\n\n const sqlParts: string[] = [];\n for (const dir of dirs) {\n const sqlPath = join(migrationsPath, dir, 'migration.sql');\n if (existsSync(sqlPath)) {\n sqlParts.push(readFileSync(sqlPath, 'utf8'));\n }\n }\n\n return sqlParts.length > 0 ? sqlParts.join('\\n') : null;\n};\n\n/**\n * Resolve schema SQL. Priority:\n * 1. Explicit `sql` option — use directly\n * 2. Explicit `migrationsPath` — read migration files\n * 3. Auto-discovered migrations (via prisma.config.ts) — read migration files\n * 4. Error — tell the user to generate migration files\n */\nconst resolveSQL = async (options: CreatePgliteAdapterOptions): Promise<string> => {\n if (options.sql) return options.sql;\n\n // Explicit migrationsPath\n if (options.migrationsPath) {\n const sql = tryReadMigrationFiles(options.migrationsPath);\n if (sql) return sql;\n throw new Error(\n `No migration.sql files found in ${options.migrationsPath}. Run \\`prisma migrate dev\\` to generate migration files.`,\n );\n }\n\n // Auto-discover via Prisma config\n const migrationsPath = await discoverMigrationsPath(options.configRoot);\n\n if (migrationsPath) {\n const sql = tryReadMigrationFiles(migrationsPath);\n if (sql) return sql;\n }\n\n throw new Error(\n 'No migration files found. Run `prisma migrate dev` to generate them, ' +\n 'or pass pre-generated SQL via the `sql` option.',\n );\n};\n\n/**\n * Creates a Prisma adapter backed by an in-process PGlite instance.\n *\n * Applies the schema and returns a ready-to-use adapter + a `resetDb`\n * function for clearing tables between tests.\n */\nexport const createPgliteAdapter = async (\n options: CreatePgliteAdapterOptions = {},\n): Promise<PgliteAdapter> => {\n const sql = await resolveSQL(options);\n const {\n pool,\n pglite,\n close: poolClose,\n } = await createPool({\n dataDir: options.dataDir,\n extensions: options.extensions,\n max: options.max,\n });\n\n try {\n await pglite.exec(sql);\n } catch (err) {\n throw new Error('Failed to apply schema SQL to PGlite. Check your schema or migration files.', {\n cause: err,\n });\n }\n\n const adapter = new PrismaPg(pool);\n\n let cachedTables: string | null = null;\n let hasSnapshot = false;\n\n const discoverTables = async () => {\n if (cachedTables !== null) return cachedTables;\n const { rows } = await pglite.query<{ qualified: string }>(\n `SELECT quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified\n FROM pg_tables\n WHERE schemaname NOT IN ('pg_catalog', 'information_schema')\n AND schemaname != '${SNAPSHOT_SCHEMA}'\n AND tablename NOT LIKE '_prisma%'`,\n );\n cachedTables = rows.length > 0 ? rows.map((r) => r.qualified).join(', ') : '';\n return cachedTables;\n };\n\n const snapshotDb: SnapshotDbFn = async () => {\n await pglite.exec(`DROP SCHEMA IF EXISTS \"${SNAPSHOT_SCHEMA}\" CASCADE`);\n await pglite.exec(`CREATE SCHEMA \"${SNAPSHOT_SCHEMA}\"`);\n\n const { rows: tables } = await pglite.query<{ tablename: string }>(\n `SELECT quote_ident(tablename) AS tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND tablename NOT LIKE '_prisma%'`,\n );\n\n for (const { tablename } of tables) {\n await pglite.exec(\n `CREATE TABLE \"${SNAPSHOT_SCHEMA}\".${tablename} AS SELECT * FROM public.${tablename}`,\n );\n }\n\n const { rows: seqs } = await pglite.query<{ name: string; value: string }>(\n `SELECT quote_literal(sequencename) AS name, last_value::text AS value\n FROM pg_sequences WHERE schemaname = 'public' AND last_value IS NOT NULL`,\n );\n\n await pglite.exec(`CREATE TABLE \"${SNAPSHOT_SCHEMA}\".__sequences (name text, value bigint)`);\n for (const { name, value } of seqs) {\n await pglite.exec(`INSERT INTO \"${SNAPSHOT_SCHEMA}\".__sequences VALUES (${name}, ${value})`);\n }\n\n hasSnapshot = true;\n };\n\n const resetSnapshot: ResetSnapshotFn = async () => {\n hasSnapshot = false;\n await pglite.exec(`DROP SCHEMA IF EXISTS \"${SNAPSHOT_SCHEMA}\" CASCADE`);\n };\n\n const resetDb = async () => {\n const tables = await discoverTables();\n\n if (hasSnapshot && tables) {\n try {\n await pglite.exec('SET session_replication_role = replica');\n await pglite.exec(`TRUNCATE TABLE ${tables} CASCADE`);\n\n const { rows: snapshotTables } = await pglite.query<{ tablename: string }>(\n `SELECT quote_ident(tablename) AS tablename FROM pg_tables\n WHERE schemaname = '${SNAPSHOT_SCHEMA}'\n AND tablename != '__sequences'`,\n );\n\n for (const { tablename } of snapshotTables) {\n await pglite.exec(\n `INSERT INTO public.${tablename} SELECT * FROM \"${SNAPSHOT_SCHEMA}\".${tablename}`,\n );\n }\n } finally {\n await pglite.exec('SET session_replication_role = DEFAULT');\n }\n\n const { rows: seqs } = await pglite.query<{ name: string; value: string }>(\n `SELECT quote_literal(name) AS name, value::text AS value FROM \"${SNAPSHOT_SCHEMA}\".__sequences`,\n );\n\n for (const { name, value } of seqs) {\n await pglite.exec(`SELECT setval(${name}, ${value})`);\n }\n } else if (tables) {\n try {\n await pglite.exec('SET session_replication_role = replica');\n await pglite.exec(`TRUNCATE TABLE ${tables} CASCADE`);\n } finally {\n await pglite.exec('SET session_replication_role = DEFAULT');\n }\n }\n\n await pglite.exec('RESET ALL');\n await pglite.exec('DEALLOCATE ALL');\n };\n\n return { adapter, pglite, resetDb, snapshotDb, resetSnapshot, close: poolClose };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/session-lock.ts","../src/pglite-bridge.ts","../src/create-pool.ts","../src/create-pglite-adapter.ts"],"sourcesContent":["/**\n * Session-level lock for PGlite's single-session model.\n *\n * PGlite runs PostgreSQL in single-user mode — one session shared by all\n * bridges. runExclusive serializes individual operations, but transactions\n * span multiple operations. Without session-level locking, Bridge A's BEGIN\n * and Bridge B's query interleave, corrupting transaction boundaries.\n *\n * The session lock tracks which bridge owns the session. When PGlite enters\n * transaction state (ReadyForQuery status 'T' or 'E'), the owning bridge\n * gets exclusive access until the transaction completes (status returns to 'I').\n *\n * Non-transactional operations from any bridge are allowed when no transaction\n * is active — they serialize naturally through runExclusive.\n */\n\n// ReadyForQuery status bytes\nconst STATUS_IDLE = 0x49; // 'I' — no transaction\nconst STATUS_IN_TRANSACTION = 0x54; // 'T' — in transaction block\nconst STATUS_FAILED = 0x45; // 'E' — failed transaction block\n\n/** Opaque bridge identity token */\nexport type BridgeId = symbol;\n\nexport const createBridgeId = (): BridgeId => Symbol('bridge');\n\n/**\n * Extracts the ReadyForQuery status byte from a response buffer.\n * Scans from the end since RFQ is always the last message.\n * Returns null if no RFQ found.\n */\nexport const extractRfqStatus = (response: Uint8Array): number | null => {\n // RFQ is always 6 bytes: Z(5a) + length(00000005) + status\n // It's the last message in the response\n if (response.length < 6) return null;\n const i = response.length - 6;\n if (\n response[i] === 0x5a &&\n response[i + 1] === 0x00 &&\n response[i + 2] === 0x00 &&\n response[i + 3] === 0x00 &&\n response[i + 4] === 0x05\n ) {\n return response[i + 5] ?? null;\n }\n return null;\n};\n\n/**\n * Coordinates PGlite access across concurrent pool connections.\n *\n * @remarks\n * PGlite runs PostgreSQL in single-user mode — one session shared by all\n * bridges. The session lock tracks which bridge owns the session during\n * transactions, preventing interleaving. Used internally by {@link PGliteBridge}\n * and created automatically by {@link createPool}. Only instantiate directly\n * if building a custom pool setup.\n */\nexport class SessionLock {\n private owner: BridgeId | null = null;\n private waitQueue: Array<{ id: BridgeId; resolve: () => void }> = [];\n\n /**\n * Acquire access to PGlite. Resolves immediately if no transaction is\n * active or if this bridge owns the current transaction. Queues otherwise.\n */\n async acquire(id: BridgeId): Promise<void> {\n // No owner — session is free\n if (this.owner === null) return;\n // Re-entrant — same bridge already owns the session\n if (this.owner === id) return;\n\n // Another bridge owns the session — wait\n return new Promise<void>((resolve) => {\n this.waitQueue.push({ id, resolve });\n });\n }\n\n /**\n * Update session state based on the ReadyForQuery status byte.\n * Call after every PGlite response that contains RFQ.\n */\n updateStatus(id: BridgeId, status: number): void {\n if (status === STATUS_IN_TRANSACTION || status === STATUS_FAILED) {\n // This bridge now owns the session\n this.owner = id;\n } else if (status === STATUS_IDLE) {\n // Transaction complete — release ownership\n if (this.owner === id) {\n this.owner = null;\n this.drainWaitQueue();\n }\n }\n }\n\n /**\n * Release ownership (e.g., when a bridge is destroyed mid-transaction).\n */\n release(id: BridgeId): void {\n if (this.owner === id) {\n this.owner = null;\n this.drainWaitQueue();\n }\n }\n\n private drainWaitQueue(): void {\n // Release one waiter at a time and grant ownership before resolving.\n // The waiter's operation will call updateStatus when it completes —\n // if IDLE, ownership is cleared and the next waiter is released.\n // This prevents interleaving where multiple waiters race past acquire\n // and one starts a transaction while others proceed unserialized.\n const next = this.waitQueue.shift();\n if (!next) return;\n this.owner = next.id;\n next.resolve();\n }\n}\n","/**\n * PGlite bridge stream.\n *\n * A Duplex stream that replaces the TCP socket in pg.Client, routing\n * wire protocol messages directly to an in-process PGlite instance.\n *\n * pg.Client writes wire protocol bytes → bridge frames messages →\n * PGlite processes via execProtocolRawStream → bridge pushes responses back.\n *\n * Extended Query Protocol pipelines (Parse→Bind→Describe→Execute→Sync) are\n * concatenated into a single buffer and sent as one atomic execProtocolRawStream\n * call within one runExclusive. This prevents portal interleaving between\n * concurrent bridges AND reduces async overhead (1 WASM call instead of 5).\n *\n * The response from a batched pipeline contains spurious ReadyForQuery messages\n * after each sub-message (PGlite's single-user mode). These are stripped,\n * keeping only the final ReadyForQuery after Sync.\n */\nimport { Duplex } from 'node:stream';\nimport type { PGlite } from '@electric-sql/pglite';\nimport {\n type BridgeId,\n createBridgeId,\n extractRfqStatus,\n type SessionLock,\n} from './session-lock.ts';\n\n// Frontend message types\nconst PARSE = 0x50; // P\nconst BIND = 0x42; // B\nconst DESCRIBE = 0x44; // D\nconst EXECUTE = 0x45; // E\nconst CLOSE = 0x43; // C\nconst FLUSH = 0x48; // H\nconst SYNC = 0x53; // S (frontend)\nconst TERMINATE = 0x58; // X\n\n// Backend message type\nconst READY_FOR_QUERY = 0x5a; // Z — 6 bytes: Z + length(5) + status\n\n// Extended Query Protocol message types — must be batched until Sync\nconst EQP_MESSAGES = new Set([PARSE, BIND, DESCRIBE, EXECUTE, CLOSE, FLUSH]);\n\n/**\n * Strips all intermediate ReadyForQuery messages from a response, keeping\n * only the last one. PGlite's single-user mode emits RFQ after every\n * sub-message; pg.Client expects exactly one after Sync.\n *\n * Operates in-place on the response by building a list of byte ranges to\n * keep, then assembling the result. Returns the original buffer (no copy)\n * if there are 0 or 1 RFQ messages.\n */\n/** @internal — exported for testing only */\nexport const stripIntermediateReadyForQuery = (response: Uint8Array): Uint8Array => {\n // Quick scan: count RFQ occurrences and find their positions\n const rfqPositions: number[] = [];\n let offset = 0;\n\n while (offset < response.length) {\n if (offset + 5 >= response.length) break;\n\n if (\n response[offset] === READY_FOR_QUERY &&\n response[offset + 1] === 0x00 &&\n response[offset + 2] === 0x00 &&\n response[offset + 3] === 0x00 &&\n response[offset + 4] === 0x05\n ) {\n rfqPositions.push(offset);\n offset += 6;\n } else {\n // Skip this backend message: type(1) + length(4, big-endian)\n const b1 = response[offset + 1];\n const b2 = response[offset + 2];\n const b3 = response[offset + 3];\n const b4 = response[offset + 4];\n if (b1 === undefined || b2 === undefined || b3 === undefined || b4 === undefined) break;\n const msgLen = ((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) >>> 0;\n if (msgLen < 4) break; // malformed — minimum length field is 4 (includes itself)\n offset += 1 + msgLen;\n }\n }\n\n if (rfqPositions.length <= 1) return response;\n\n // Build result: copy everything except intermediate RFQ messages (all but last)\n const removeCount = rfqPositions.length - 1;\n const resultLen = response.length - removeCount * 6;\n const result = new Uint8Array(resultLen);\n let src = 0;\n let dst = 0;\n let removeIdx = 0;\n\n while (src < response.length) {\n const nextRemove =\n removeIdx < removeCount ? (rfqPositions[removeIdx] ?? response.length) : response.length;\n if (src < nextRemove) {\n const copyLen = nextRemove - src;\n result.set(response.subarray(src, src + copyLen), dst);\n dst += copyLen;\n src += copyLen;\n }\n if (removeIdx < removeCount && src === rfqPositions[removeIdx]) {\n src += 6;\n removeIdx++;\n }\n }\n\n return result;\n};\n\n/**\n * Concatenates multiple Uint8Array views into one contiguous buffer.\n */\nconst concat = (parts: Uint8Array[]): Uint8Array => {\n if (parts.length === 1) return parts[0] ?? new Uint8Array(0);\n const total = parts.reduce((sum, p) => sum + p.length, 0);\n const result = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n result.set(part, offset);\n offset += part.length;\n }\n return result;\n};\n\n/**\n * Duplex stream that bridges `pg.Client` to an in-process PGlite instance.\n *\n * Replaces the TCP socket in `pg.Client` via the `stream` option. Speaks\n * PostgreSQL wire protocol directly to PGlite — no TCP, no serialization\n * overhead beyond what the wire protocol requires.\n *\n * Pass to `pg.Client` or use via `createPool()` / `createPgliteAdapter()`:\n *\n * ```typescript\n * const client = new pg.Client({\n * stream: () => new PGliteBridge(pglite),\n * });\n * ```\n */\nexport class PGliteBridge extends Duplex {\n private readonly pglite: PGlite;\n private readonly sessionLock: SessionLock | null;\n private readonly bridgeId: BridgeId;\n /** Incoming bytes not yet compacted into buf */\n private pending: Buffer[] = [];\n /** Compacted input buffer for message framing */\n private buf: Buffer = Buffer.alloc(0);\n private phase: 'pre_startup' | 'ready' = 'pre_startup';\n private draining = false;\n private tornDown = false;\n /** Callbacks waiting for drain to process their data */\n private drainQueue: Array<(error?: Error | null) => void> = [];\n /** Buffered EQP messages awaiting Sync */\n private pipeline: Uint8Array[] = [];\n\n constructor(pglite: PGlite, sessionLock?: SessionLock) {\n super();\n this.pglite = pglite;\n this.sessionLock = sessionLock ?? null;\n this.bridgeId = createBridgeId();\n }\n\n // ── Socket compatibility (called by pg's Connection) ──\n\n connect(): this {\n setImmediate(() => this.emit('connect'));\n return this;\n }\n\n setKeepAlive(): this {\n return this;\n }\n\n setNoDelay(): this {\n return this;\n }\n\n setTimeout(): this {\n return this;\n }\n\n ref(): this {\n return this;\n }\n\n unref(): this {\n return this;\n }\n\n // ── Duplex implementation ──\n\n override _read(): void {\n // Data is pushed proactively when PGlite responses arrive\n }\n\n override _write(\n chunk: Buffer,\n _encoding: BufferEncoding,\n callback: (error?: Error | null) => void,\n ): void {\n this.pending.push(chunk);\n this.enqueue(callback);\n }\n\n /** Handles corked batches — pg.Client corks during prepared queries (P+B+D+E+S) */\n override _writev(\n chunks: Array<{ chunk: Buffer; encoding: BufferEncoding }>,\n callback: (error?: Error | null) => void,\n ): void {\n for (const { chunk } of chunks) {\n this.pending.push(chunk);\n }\n this.enqueue(callback);\n }\n\n override _final(callback: (error?: Error | null) => void): void {\n this.sessionLock?.release(this.bridgeId);\n this.push(null);\n callback();\n }\n\n override _destroy(error: Error | null, callback: (error?: Error | null) => void): void {\n this.tornDown = true;\n this.pipeline.length = 0;\n this.pending.length = 0;\n this.sessionLock?.release(this.bridgeId);\n\n // Flush pending write callbacks so pg.Client doesn't hang\n const callbacks = this.drainQueue;\n this.drainQueue = [];\n for (const cb of callbacks) {\n cb(error);\n }\n\n callback(error);\n }\n\n // ── Message processing ──\n\n /** Merge pending chunks into buf only when needed for framing */\n private compact(): void {\n if (this.pending.length === 0) return;\n if (this.buf.length === 0 && this.pending.length === 1) {\n this.buf = this.pending[0] as Buffer;\n } else {\n this.buf = Buffer.concat([this.buf, ...this.pending]);\n }\n this.pending.length = 0;\n }\n\n /**\n * Enqueue a write callback and start draining if not already running.\n * The callback is NOT called until drain has processed the data.\n */\n private enqueue(callback: (error?: Error | null) => void): void {\n this.drainQueue.push(callback);\n if (!this.draining) {\n // Errors are propagated through drainQueue callbacks, not through this promise\n this.drain().catch(() => {});\n }\n }\n\n /**\n * Process all pending data, looping until no new data arrives.\n * Fires all queued callbacks on completion or error.\n */\n private async drain(): Promise<void> {\n if (this.draining) return;\n this.draining = true;\n\n let error: Error | null = null;\n\n try {\n // Loop until no more pending data to process\n while (this.pending.length > 0 || this.buf.length > 0) {\n if (this.tornDown) break;\n\n if (this.phase === 'pre_startup') {\n await this.processPreStartup();\n }\n if (this.phase === 'ready') {\n await this.processMessages();\n }\n\n // If processMessages couldn't consume anything (incomplete message),\n // stop looping — more data will arrive via _write\n if (this.pending.length === 0) break;\n }\n } catch (err) {\n error = err instanceof Error ? err : new Error(String(err));\n // Release session lock on error — prevents permanent deadlock if\n // PGlite crashes mid-transaction (other bridges would wait forever)\n this.sessionLock?.release(this.bridgeId);\n } finally {\n this.draining = false;\n\n // Fire all waiting callbacks\n const callbacks = this.drainQueue;\n this.drainQueue = [];\n for (const cb of callbacks) {\n cb(error);\n }\n }\n }\n\n /**\n * Frames and processes the startup message.\n *\n * Format: [4 bytes: total length] [4 bytes: protocol version] [key\\0value\\0 pairs]\n * No type byte — length includes itself.\n */\n private async processPreStartup(): Promise<void> {\n this.compact();\n if (this.buf.length < 4) return;\n const len = this.buf.readInt32BE(0);\n if (this.buf.length < len) return;\n\n const message = this.buf.subarray(0, len);\n this.buf = this.buf.subarray(len);\n\n await this.acquireSession();\n await this.pglite.runExclusive(async () => {\n await this.execAndPush(message);\n });\n\n this.phase = 'ready';\n }\n\n /**\n * Frames and processes regular wire protocol messages.\n *\n * Extended Query Protocol messages (Parse, Bind, Describe, Execute, Close,\n * Flush) are buffered in `this.pipeline`. When Sync arrives, the entire\n * pipeline is concatenated and sent to PGlite as one atomic\n * execProtocolRawStream call within one runExclusive.\n *\n * SimpleQuery messages are sent directly (they're self-contained).\n */\n private async processMessages(): Promise<void> {\n this.compact();\n while (this.buf.length >= 5) {\n const len = 1 + this.buf.readInt32BE(1);\n if (len < 5 || this.buf.length < len) break;\n\n const message = this.buf.subarray(0, len);\n this.buf = this.buf.subarray(len);\n const msgType = message[0] ?? 0;\n\n if (msgType === TERMINATE) {\n this.sessionLock?.release(this.bridgeId);\n this.push(null);\n return;\n }\n\n if (EQP_MESSAGES.has(msgType)) {\n this.pipeline.push(message);\n continue;\n }\n\n if (msgType === SYNC) {\n this.pipeline.push(message);\n await this.flushPipeline();\n continue;\n }\n\n // SimpleQuery or other standalone message\n await this.acquireSession();\n await this.pglite.runExclusive(async () => {\n await this.execAndPush(message);\n });\n }\n }\n\n /**\n * Sends the accumulated EQP pipeline as one atomic operation.\n *\n * All buffered messages are concatenated into a single buffer and sent\n * as one execProtocolRawStream call. This is both correct (prevents\n * portal interleaving) and fast (1 WASM call + 1 async boundary instead\n * of 5). Intermediate ReadyForQuery messages are stripped from the\n * combined response.\n */\n private async flushPipeline(): Promise<void> {\n const messages = this.pipeline;\n this.pipeline = [];\n const batch = concat(messages);\n\n await this.acquireSession();\n await this.pglite.runExclusive(async () => {\n const chunks: Uint8Array[] = [];\n\n await this.pglite.execProtocolRawStream(batch, {\n onRawData: (chunk: Uint8Array) => chunks.push(chunk),\n });\n\n if (this.tornDown || chunks.length === 0) return;\n\n // Single chunk: strip intermediate RFQ and push\n if (chunks.length === 1) {\n const raw = chunks[0] ?? new Uint8Array(0);\n this.trackSessionStatus(raw);\n const cleaned = stripIntermediateReadyForQuery(raw);\n if (cleaned.length > 0) this.push(cleaned);\n return;\n }\n\n // Multiple chunks: concat first, then strip\n const combined = concat(chunks);\n this.trackSessionStatus(combined);\n const cleaned = stripIntermediateReadyForQuery(combined);\n if (cleaned.length > 0) this.push(cleaned);\n });\n }\n\n /**\n * Sends a message to PGlite and pushes response chunks directly to the\n * stream as they arrive. Avoids collecting and concatenating for large\n * multi-row responses (e.g., findMany 500 rows = ~503 onRawData chunks).\n *\n * Must be called inside runExclusive.\n */\n private async execAndPush(message: Uint8Array): Promise<void> {\n let lastChunk: Uint8Array | null = null;\n await this.pglite.execProtocolRawStream(message, {\n onRawData: (chunk: Uint8Array) => {\n if (!this.tornDown && chunk.length > 0) {\n this.push(chunk);\n lastChunk = chunk;\n }\n },\n });\n if (lastChunk) this.trackSessionStatus(lastChunk);\n }\n\n // ── Session lock helpers ──\n\n private async acquireSession(): Promise<void> {\n await this.sessionLock?.acquire(this.bridgeId);\n }\n\n private trackSessionStatus(response: Uint8Array): void {\n if (!this.sessionLock) return;\n const status = extractRfqStatus(response);\n if (status !== null) {\n this.sessionLock.updateStatus(this.bridgeId, status);\n }\n }\n}\n","/**\n * Pool factory — creates a pg.Pool backed by an in-process PGlite instance.\n *\n * Each pool connection gets its own PGliteBridge stream, all sharing the\n * same PGlite WASM instance and SessionLock. The session lock ensures\n * transaction isolation: when one bridge starts a transaction (BEGIN),\n * it gets exclusive PGlite access until COMMIT/ROLLBACK. Non-transactional\n * operations from any bridge serialize through PGlite's runExclusive mutex.\n */\nimport { type Extensions, PGlite } from '@electric-sql/pglite';\nimport pg from 'pg';\nimport { PGliteBridge } from './pglite-bridge.ts';\nimport { SessionLock } from './session-lock.ts';\n\nconst { Client, Pool } = pg;\n\nexport interface CreatePoolOptions {\n /** PGlite data directory. Omit for in-memory. */\n dataDir?: string;\n\n /** PGlite extensions (e.g., `{ uuid_ossp: uuidOssp() }`) */\n extensions?: Extensions;\n\n /** Maximum pool connections (default: 5) */\n max?: number;\n\n /** Existing PGlite instance to use instead of creating one */\n pglite?: PGlite;\n}\n\nexport interface PoolResult {\n /** pg.Pool backed by PGlite — pass to PrismaPg */\n pool: pg.Pool;\n\n /** The underlying PGlite instance */\n pglite: PGlite;\n\n /** Shut down pool and PGlite */\n close: () => Promise<void>;\n}\n\n/**\n * Creates a pg.Pool where every connection is an in-process PGlite bridge.\n *\n * Most users should prefer {@link createPgliteAdapter}, which wraps this\n * function and also handles schema application and reset/snapshot lifecycle.\n *\n * ```typescript\n * import { createPool } from 'prisma-pglite-bridge';\n * import { PrismaPg } from '@prisma/adapter-pg';\n * import { PrismaClient } from '@prisma/client';\n *\n * const { pool, close } = await createPool();\n * const adapter = new PrismaPg(pool);\n * const prisma = new PrismaClient({ adapter });\n * ```\n *\n * @see {@link createPgliteAdapter} for the higher-level API with schema management.\n */\nexport const createPool = async (options: CreatePoolOptions = {}): Promise<PoolResult> => {\n const { dataDir, extensions, max = 5 } = options;\n const ownsInstance = !options.pglite;\n\n const pglite = options.pglite ?? new PGlite(dataDir, extensions ? { extensions } : undefined);\n await pglite.waitReady;\n\n const sessionLock = new SessionLock();\n\n // Subclass pg.Client to inject PGliteBridge as the stream\n const BridgedClient = class extends Client {\n constructor(config?: string | pg.ClientConfig) {\n const cfg = typeof config === 'string' ? { connectionString: config } : (config ?? {});\n super({\n ...cfg,\n user: 'postgres',\n database: 'postgres',\n stream: (() => new PGliteBridge(pglite, sessionLock)) as pg.ClientConfig['stream'],\n });\n }\n };\n\n const pool = new Pool({\n Client: BridgedClient as typeof Client,\n max,\n });\n\n const close = async () => {\n await pool.end();\n if (ownsInstance) {\n await pglite.close();\n }\n };\n\n return { pool, pglite, close };\n};\n","/**\n * Creates a Prisma adapter backed by in-process PGlite.\n *\n * No TCP, no Docker, no worker threads — everything runs in the same process.\n * Works for testing, development, seeding, and scripts.\n *\n * ```typescript\n * import { createPgliteAdapter } from 'prisma-pglite-bridge';\n * import { PrismaClient } from '@prisma/client';\n *\n * const { adapter, resetDb } = await createPgliteAdapter();\n * const prisma = new PrismaClient({ adapter });\n *\n * beforeEach(() => resetDb());\n * ```\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { PrismaPg } from '@prisma/adapter-pg';\nimport { createPool } from './create-pool.ts';\n\nconst SNAPSHOT_SCHEMA = '_pglite_snapshot';\nconst SENTINEL_SCHEMA = '_pglite_bridge';\nconst SENTINEL_TABLE = '__initialized';\nconst SENTINEL_MARKER = 'prisma-pglite-bridge:init:v1';\n\nconst USER_TABLES_WHERE = `schemaname NOT IN ('pg_catalog', 'information_schema')\n AND schemaname != '${SNAPSHOT_SCHEMA}'\n AND schemaname != '${SENTINEL_SCHEMA}'\n AND tablename NOT LIKE '_prisma%'`;\n\nconst escapeLiteral = (s: string) => `'${s.replace(/'/g, \"''\")}'`;\n\nconst isValidSentinelRow = (rows: Array<{ marker: string; version: number }>) =>\n rows.length === 1 && rows[0]?.marker === SENTINEL_MARKER && rows[0]?.version === 1;\n\nexport interface CreatePgliteAdapterOptions {\n /** Path to prisma/migrations/ directory (auto-discovered via prisma.config.ts if omitted) */\n migrationsPath?: string;\n\n /** Pre-generated SQL to apply instead of auto-generating from schema */\n sql?: string;\n\n /** Root directory for prisma.config.ts discovery (default: process.cwd()). Set this in monorepos where tests run from the workspace root. */\n configRoot?: string;\n\n /** PGlite data directory. Omit for in-memory. */\n dataDir?: string;\n\n /** PGlite extensions (e.g., `{ uuid_ossp: uuidOssp() }`) */\n extensions?: import('@electric-sql/pglite').Extensions;\n\n /** Maximum pool connections (default: 5) */\n max?: number;\n}\n\n/** Clear all user tables. Call in `beforeEach` for per-test isolation. */\nexport type ResetDbFn = () => Promise<void>;\n\nexport type SnapshotDbFn = () => Promise<void>;\n\nexport type ResetSnapshotFn = () => Promise<void>;\n\nexport interface PgliteAdapter {\n /** Prisma adapter — pass directly to `new PrismaClient({ adapter })` */\n adapter: PrismaPg;\n\n /**\n * The underlying PGlite instance for direct SQL, snapshots, or extensions.\n *\n * @remarks\n * Direct `pglite.exec()` / `pglite.query()` calls bypass the pool's\n * {@link SessionLock}. Avoid mixing direct calls with Prisma operations\n * inside a transaction — use them only for setup, teardown, or utilities\n * that run outside active Prisma transactions.\n */\n pglite: import('@electric-sql/pglite').PGlite;\n\n /** Clear all user tables. Call in `beforeEach` for per-test isolation. */\n resetDb: ResetDbFn;\n\n /** Snapshot current DB state. Subsequent `resetDb` calls restore to this snapshot. */\n snapshotDb: SnapshotDbFn;\n\n /** Discard the current snapshot. Subsequent `resetDb` calls truncate to empty. */\n resetSnapshot: ResetSnapshotFn;\n\n /** Shut down pool and PGlite. Not needed in tests (process exit handles it). */\n close: () => Promise<void>;\n}\n\n/**\n * Discover the migrations directory via Prisma's config API.\n * Uses the same resolution as `prisma migrate dev` — reads prisma.config.ts,\n * resolves paths relative to config file location.\n *\n * Returns null if @prisma/config is not available or config cannot be loaded.\n */\nconst discoverMigrationsPath = async (configRoot?: string): Promise<string | null> => {\n try {\n const { loadConfigFromFile } = await import('@prisma/config');\n const { config, error } = await loadConfigFromFile({ configRoot: configRoot ?? process.cwd() });\n if (error) return null;\n\n // Explicit migrations path from prisma.config.ts\n if (config.migrations?.path) return config.migrations.path;\n\n // Fallback: Prisma convention is {schemaDir}/migrations\n const schemaPath = config.schema;\n if (schemaPath) return join(dirname(schemaPath), 'migrations');\n\n return null;\n } catch {\n return null;\n }\n};\n\n/**\n * Read migration SQL files from a migrations directory in directory order.\n * Returns null if the directory doesn't exist or has no migration files.\n */\nconst tryReadMigrationFiles = (migrationsPath: string): string | null => {\n if (!existsSync(migrationsPath)) return null;\n\n const dirs = readdirSync(migrationsPath)\n .filter((d) => statSync(join(migrationsPath, d)).isDirectory())\n .sort();\n\n const sqlParts: string[] = [];\n for (const dir of dirs) {\n const sqlPath = join(migrationsPath, dir, 'migration.sql');\n if (existsSync(sqlPath)) {\n sqlParts.push(readFileSync(sqlPath, 'utf8'));\n }\n }\n\n return sqlParts.length > 0 ? sqlParts.join('\\n') : null;\n};\n\n/**\n * Resolve schema SQL. Priority:\n * 1. Explicit `sql` option — use directly\n * 2. Explicit `migrationsPath` — read migration files\n * 3. Auto-discovered migrations (via prisma.config.ts) — read migration files\n * 4. Error — tell the user to generate migration files\n */\nconst resolveSQL = async (options: CreatePgliteAdapterOptions): Promise<string> => {\n if (options.sql) return options.sql;\n\n // Explicit migrationsPath\n if (options.migrationsPath) {\n const sql = tryReadMigrationFiles(options.migrationsPath);\n if (sql) return sql;\n throw new Error(\n `No migration.sql files found in ${options.migrationsPath}. Run \\`prisma migrate dev\\` to generate migration files.`,\n );\n }\n\n // Auto-discover via Prisma config\n const migrationsPath = await discoverMigrationsPath(options.configRoot);\n\n if (migrationsPath) {\n const sql = tryReadMigrationFiles(migrationsPath);\n if (sql) return sql;\n\n throw new Error(\n `No migration.sql files found in auto-discovered path ${migrationsPath}. ` +\n 'Run `prisma migrate dev` to generate migration files, ' +\n 'or pass pre-generated SQL via the `sql` option.',\n );\n }\n\n if (options.configRoot) {\n throw new Error(\n `prisma.config.ts loaded from configRoot (${options.configRoot}) but no schema ` +\n 'or migrations path could be resolved. Ensure your config specifies a schema path, ' +\n 'or pass pre-generated SQL via the `sql` option.',\n );\n }\n\n throw new Error(\n 'No migration files found and no prisma.config.ts could be loaded. ' +\n 'Run `prisma migrate dev` to generate them, ' +\n 'or pass pre-generated SQL via the `sql` option.',\n );\n};\n\n/**\n * Creates a Prisma adapter backed by an in-process PGlite instance.\n *\n * Applies the schema and returns a ready-to-use adapter + a `resetDb`\n * function for clearing tables between tests.\n */\nexport const createPgliteAdapter = async (\n options: CreatePgliteAdapterOptions = {},\n): Promise<PgliteAdapter> => {\n const {\n pool,\n pglite,\n close: poolClose,\n } = await createPool({\n dataDir: options.dataDir,\n extensions: options.extensions,\n max: options.max,\n });\n\n const sentinelStatements = [\n `CREATE SCHEMA IF NOT EXISTS \"${SENTINEL_SCHEMA}\"`,\n `CREATE TABLE IF NOT EXISTS \"${SENTINEL_SCHEMA}\".\"${SENTINEL_TABLE}\" (marker text PRIMARY KEY, version int NOT NULL)`,\n `INSERT INTO \"${SENTINEL_SCHEMA}\".\"${SENTINEL_TABLE}\" (marker, version) VALUES (${escapeLiteral(SENTINEL_MARKER)}, 1) ON CONFLICT (marker) DO NOTHING`,\n ];\n\n const collisionError = () =>\n `\"${SENTINEL_SCHEMA}\".\"${SENTINEL_TABLE}\" exists but is not owned by prisma-pglite-bridge. ` +\n `The \"${SENTINEL_SCHEMA}\" schema is reserved for library metadata.`;\n\n const writeSentinel = async () => {\n let committed = false;\n try {\n await pglite.exec(`BEGIN;\\n${sentinelStatements.join(';\\n')}`);\n\n const { rows } = await pglite.query<{ marker: string; version: number }>(\n `SELECT marker, version FROM \"${SENTINEL_SCHEMA}\".\"${SENTINEL_TABLE}\"`,\n );\n if (!isValidSentinelRow(rows)) throw new Error(collisionError());\n await pglite.exec('COMMIT');\n committed = true;\n } catch (err) {\n if (!committed) await pglite.exec('ROLLBACK');\n throw err instanceof Error && err.message === collisionError()\n ? err\n : new Error(collisionError(), { cause: err });\n }\n };\n\n const isInitialized = async () => {\n const { rows: tableExists } = await pglite.query<{ found: boolean }>(\n `SELECT EXISTS (\n SELECT 1 FROM pg_tables\n WHERE schemaname = '${SENTINEL_SCHEMA}' AND tablename = '${SENTINEL_TABLE}'\n ) AS found`,\n );\n\n if (tableExists[0]?.found) {\n try {\n const { rows: allRows } = await pglite.query<{ marker: string; version: number }>(\n `SELECT marker, version FROM \"${SENTINEL_SCHEMA}\".\"${SENTINEL_TABLE}\"`,\n );\n if (isValidSentinelRow(allRows)) return true;\n } catch {\n // Table has incompatible columns — fall through to collision error\n }\n\n throw new Error(collisionError());\n }\n\n const { rows: schemaExists } = await pglite.query<{ found: boolean }>(\n `SELECT EXISTS (\n SELECT 1 FROM pg_namespace WHERE nspname = '${SENTINEL_SCHEMA}'\n ) AS found`,\n );\n if (schemaExists[0]?.found) {\n throw new Error(\n `Schema \"${SENTINEL_SCHEMA}\" exists but is not owned by prisma-pglite-bridge. ` +\n `The \"${SENTINEL_SCHEMA}\" schema is reserved for library metadata.`,\n );\n }\n\n const { rows: legacy } = await pglite.query<{ initialized: boolean }>(\n `SELECT EXISTS (\n SELECT 1 FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'public'\n UNION ALL\n SELECT 1 FROM pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname = 'public' AND t.typtype NOT IN ('b', 'p')\n UNION ALL\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid\n WHERE n.nspname = 'public'\n UNION ALL\n SELECT 1 FROM pg_namespace\n WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'public')\n ) AS initialized`,\n );\n if (legacy[0]?.initialized) {\n await writeSentinel();\n return true;\n }\n\n return false;\n };\n\n if (!options.dataDir || !(await isInitialized())) {\n const sql = await resolveSQL(options);\n const isMigrationSQL = !options.sql;\n\n if (isMigrationSQL) {\n let committed = false;\n try {\n await pglite.exec(`BEGIN;\\n${sql};\\n${sentinelStatements.join(';\\n')}`);\n\n const { rows: verify } = await pglite.query<{ marker: string; version: number }>(\n `SELECT marker, version FROM \"${SENTINEL_SCHEMA}\".\"${SENTINEL_TABLE}\"`,\n );\n if (!isValidSentinelRow(verify)) throw new Error(collisionError());\n await pglite.exec('COMMIT');\n committed = true;\n } catch (err) {\n if (!committed) await pglite.exec('ROLLBACK');\n if (err instanceof Error && err.message === collisionError()) throw err;\n throw new Error(\n 'Failed to apply schema SQL to PGlite. Check your schema or migration files.',\n { cause: err },\n );\n }\n } else {\n try {\n await pglite.exec(sql);\n } catch (err) {\n throw new Error(\n 'Failed to apply schema SQL to PGlite. Check your schema or migration files.',\n { cause: err },\n );\n }\n await writeSentinel();\n }\n }\n\n const adapter = new PrismaPg(pool);\n\n let cachedTables: string | null = null;\n let hasSnapshot = false;\n\n const discoverTables = async () => {\n if (cachedTables !== null) return cachedTables;\n const { rows } = await pglite.query<{ qualified: string }>(\n `SELECT quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified\n FROM pg_tables\n WHERE ${USER_TABLES_WHERE}`,\n );\n cachedTables = rows.length > 0 ? rows.map((r) => r.qualified).join(', ') : '';\n return cachedTables;\n };\n\n const snapshotDb: SnapshotDbFn = async () => {\n await pglite.exec(`DROP SCHEMA IF EXISTS \"${SNAPSHOT_SCHEMA}\" CASCADE`);\n\n try {\n await pglite.exec('BEGIN');\n await pglite.exec(`CREATE SCHEMA \"${SNAPSHOT_SCHEMA}\"`);\n\n const { rows: tables } = await pglite.query<{\n schemaname: string;\n tablename: string;\n qualified: string;\n }>(\n `SELECT schemaname, tablename,\n quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified\n FROM pg_tables\n WHERE ${USER_TABLES_WHERE}`,\n );\n\n await pglite.exec(\n `CREATE TABLE \"${SNAPSHOT_SCHEMA}\".__tables (snap_name text, source_schema text, source_table text)`,\n );\n\n for (const [i, { schemaname, tablename, qualified }] of tables.entries()) {\n const snapName = `_snap_${i}`;\n await pglite.exec(\n `CREATE TABLE \"${SNAPSHOT_SCHEMA}\".\"${snapName}\" AS SELECT * FROM ${qualified}`,\n );\n await pglite.exec(\n `INSERT INTO \"${SNAPSHOT_SCHEMA}\".__tables VALUES (${escapeLiteral(snapName)}, ${escapeLiteral(schemaname)}, ${escapeLiteral(tablename)})`,\n );\n }\n\n const { rows: seqs } = await pglite.query<{ name: string; value: string }>(\n `SELECT quote_literal(schemaname || '.' || sequencename) AS name, last_value::text AS value\n FROM pg_sequences\n WHERE schemaname NOT IN ('pg_catalog', 'information_schema')\n AND schemaname != '${SNAPSHOT_SCHEMA}'\n AND last_value IS NOT NULL`,\n );\n\n await pglite.exec(`CREATE TABLE \"${SNAPSHOT_SCHEMA}\".__sequences (name text, value bigint)`);\n for (const { name, value } of seqs) {\n await pglite.exec(\n `INSERT INTO \"${SNAPSHOT_SCHEMA}\".__sequences VALUES (${name}, ${value})`,\n );\n }\n\n await pglite.exec('COMMIT');\n } catch (err) {\n await pglite.exec('ROLLBACK');\n await pglite.exec(`DROP SCHEMA IF EXISTS \"${SNAPSHOT_SCHEMA}\" CASCADE`);\n throw err;\n }\n\n hasSnapshot = true;\n };\n\n const resetSnapshot: ResetSnapshotFn = async () => {\n hasSnapshot = false;\n await pglite.exec(`DROP SCHEMA IF EXISTS \"${SNAPSHOT_SCHEMA}\" CASCADE`);\n };\n\n const resetDb = async () => {\n cachedTables = null;\n const tables = await discoverTables();\n\n if (hasSnapshot && tables) {\n try {\n await pglite.exec('SET session_replication_role = replica');\n await pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);\n\n const { rows: snapshotTables } = await pglite.query<{\n snap_name: string;\n qualified: string;\n }>(\n `SELECT snap_name, quote_ident(source_schema) || '.' || quote_ident(source_table) AS qualified\n FROM \"${SNAPSHOT_SCHEMA}\".__tables`,\n );\n\n for (const { snap_name, qualified } of snapshotTables) {\n await pglite.exec(\n `INSERT INTO ${qualified} SELECT * FROM \"${SNAPSHOT_SCHEMA}\".\"${snap_name}\"`,\n );\n }\n\n const { rows: seqs } = await pglite.query<{ name: string; value: string }>(\n `SELECT quote_literal(name) AS name, value::text AS value FROM \"${SNAPSHOT_SCHEMA}\".__sequences`,\n );\n\n for (const { name, value } of seqs) {\n await pglite.exec(`SELECT setval(${name}, ${value})`);\n }\n } finally {\n await pglite.exec('SET session_replication_role = DEFAULT');\n }\n } else if (tables) {\n try {\n await pglite.exec('SET session_replication_role = replica');\n await pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);\n } finally {\n await pglite.exec('SET session_replication_role = DEFAULT');\n }\n }\n\n await pglite.exec('RESET ALL');\n await pglite.exec('DEALLOCATE ALL');\n };\n\n return { adapter, pglite, resetDb, snapshotDb, resetSnapshot, close: poolClose };\n};\n"],"mappings":"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"}
|