prisma-pglite-bridge 0.3.0 → 0.3.2

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.mjs ADDED
@@ -0,0 +1,653 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { PrismaPg } from "@prisma/adapter-pg";
4
+ import { PGlite } from "@electric-sql/pglite";
5
+ import pg from "pg";
6
+ import { Duplex } from "node:stream";
7
+ //#region src/session-lock.ts
8
+ /**
9
+ * Session-level lock for PGlite's single-session model.
10
+ *
11
+ * PGlite runs PostgreSQL in single-user mode — one session shared by all
12
+ * bridges. runExclusive serializes individual operations, but transactions
13
+ * span multiple operations. Without session-level locking, Bridge A's BEGIN
14
+ * and Bridge B's query interleave, corrupting transaction boundaries.
15
+ *
16
+ * The session lock tracks which bridge owns the session. When PGlite enters
17
+ * transaction state (ReadyForQuery status 'T' or 'E'), the owning bridge
18
+ * gets exclusive access until the transaction completes (status returns to 'I').
19
+ *
20
+ * Non-transactional operations from any bridge are allowed when no transaction
21
+ * is active — they serialize naturally through runExclusive.
22
+ */
23
+ const STATUS_IDLE = 73;
24
+ const STATUS_IN_TRANSACTION = 84;
25
+ const STATUS_FAILED = 69;
26
+ const createBridgeId = () => Symbol("bridge");
27
+ /**
28
+ * Extracts the ReadyForQuery status byte from a response buffer.
29
+ * Scans from the end since RFQ is always the last message.
30
+ * Returns null if no RFQ found.
31
+ */
32
+ const extractRfqStatus = (response) => {
33
+ if (response.length < 6) return null;
34
+ const i = response.length - 6;
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
+ return null;
37
+ };
38
+ var SessionLock = class {
39
+ owner = null;
40
+ waitQueue = [];
41
+ /**
42
+ * Acquire access to PGlite. Resolves immediately if no transaction is
43
+ * active or if this bridge owns the current transaction. Queues otherwise.
44
+ */
45
+ async acquire(id) {
46
+ if (this.owner === null || this.owner === id) return;
47
+ return new Promise((resolve) => {
48
+ this.waitQueue.push({
49
+ id,
50
+ resolve
51
+ });
52
+ });
53
+ }
54
+ /**
55
+ * Update session state based on the ReadyForQuery status byte.
56
+ * Call after every PGlite response that contains RFQ.
57
+ */
58
+ updateStatus(id, status) {
59
+ if (status === STATUS_IN_TRANSACTION || status === STATUS_FAILED) this.owner = id;
60
+ else if (status === STATUS_IDLE) {
61
+ if (this.owner === id) {
62
+ this.owner = null;
63
+ this.drainWaitQueue();
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Release ownership (e.g., when a bridge is destroyed mid-transaction).
69
+ */
70
+ release(id) {
71
+ if (this.owner === id) {
72
+ this.owner = null;
73
+ this.drainWaitQueue();
74
+ }
75
+ }
76
+ drainWaitQueue() {
77
+ const waiters = this.waitQueue;
78
+ this.waitQueue = [];
79
+ for (const waiter of waiters) waiter.resolve();
80
+ }
81
+ };
82
+ //#endregion
83
+ //#region src/pglite-bridge.ts
84
+ /**
85
+ * PGlite bridge stream.
86
+ *
87
+ * A Duplex stream that replaces the TCP socket in pg.Client, routing
88
+ * wire protocol messages directly to an in-process PGlite instance.
89
+ *
90
+ * pg.Client writes wire protocol bytes → bridge frames messages →
91
+ * PGlite processes via execProtocolRawStream → bridge pushes responses back.
92
+ *
93
+ * Extended Query Protocol pipelines (Parse→Bind→Describe→Execute→Sync) are
94
+ * concatenated into a single buffer and sent as one atomic execProtocolRawStream
95
+ * call within one runExclusive. This prevents portal interleaving between
96
+ * concurrent bridges AND reduces async overhead (1 WASM call instead of 5).
97
+ *
98
+ * The response from a batched pipeline contains spurious ReadyForQuery messages
99
+ * after each sub-message (PGlite's single-user mode). These are stripped,
100
+ * keeping only the final ReadyForQuery after Sync.
101
+ */
102
+ const PARSE = 80;
103
+ const BIND = 66;
104
+ const DESCRIBE = 68;
105
+ const EXECUTE = 69;
106
+ const CLOSE = 67;
107
+ const FLUSH = 72;
108
+ const SYNC = 83;
109
+ const TERMINATE = 88;
110
+ const READY_FOR_QUERY = 90;
111
+ const EQP_MESSAGES = new Set([
112
+ PARSE,
113
+ BIND,
114
+ DESCRIBE,
115
+ EXECUTE,
116
+ CLOSE,
117
+ FLUSH
118
+ ]);
119
+ /**
120
+ * Strips all intermediate ReadyForQuery messages from a response, keeping
121
+ * only the last one. PGlite's single-user mode emits RFQ after every
122
+ * sub-message; pg.Client expects exactly one after Sync.
123
+ *
124
+ * Operates in-place on the response by building a list of byte ranges to
125
+ * keep, then assembling the result. Returns the original buffer (no copy)
126
+ * if there are 0 or 1 RFQ messages.
127
+ */
128
+ /** @internal — exported for testing only */
129
+ const stripIntermediateReadyForQuery = (response) => {
130
+ const rfqPositions = [];
131
+ let offset = 0;
132
+ while (offset < response.length) {
133
+ if (offset + 5 >= response.length) break;
134
+ if (response[offset] === READY_FOR_QUERY && response[offset + 1] === 0 && response[offset + 2] === 0 && response[offset + 3] === 0 && response[offset + 4] === 5) {
135
+ rfqPositions.push(offset);
136
+ offset += 6;
137
+ } else {
138
+ const b1 = response[offset + 1];
139
+ const b2 = response[offset + 2];
140
+ const b3 = response[offset + 3];
141
+ const b4 = response[offset + 4];
142
+ if (b1 === void 0 || b2 === void 0 || b3 === void 0 || b4 === void 0) break;
143
+ const msgLen = (b1 << 24 | b2 << 16 | b3 << 8 | b4) >>> 0;
144
+ if (msgLen < 4) break;
145
+ offset += 1 + msgLen;
146
+ }
147
+ }
148
+ if (rfqPositions.length <= 1) return response;
149
+ const removeCount = rfqPositions.length - 1;
150
+ const resultLen = response.length - removeCount * 6;
151
+ const result = new Uint8Array(resultLen);
152
+ let src = 0;
153
+ let dst = 0;
154
+ let removeIdx = 0;
155
+ while (src < response.length) {
156
+ const nextRemove = removeIdx < removeCount ? rfqPositions[removeIdx] ?? response.length : response.length;
157
+ if (src < nextRemove) {
158
+ const copyLen = nextRemove - src;
159
+ result.set(response.subarray(src, src + copyLen), dst);
160
+ dst += copyLen;
161
+ src += copyLen;
162
+ }
163
+ if (removeIdx < removeCount && src === rfqPositions[removeIdx]) {
164
+ src += 6;
165
+ removeIdx++;
166
+ }
167
+ }
168
+ return result;
169
+ };
170
+ /**
171
+ * Concatenates multiple Uint8Array views into one contiguous buffer.
172
+ */
173
+ const concat = (parts) => {
174
+ if (parts.length === 1) return parts[0] ?? new Uint8Array(0);
175
+ const total = parts.reduce((sum, p) => sum + p.length, 0);
176
+ const result = new Uint8Array(total);
177
+ let offset = 0;
178
+ for (const part of parts) {
179
+ result.set(part, offset);
180
+ offset += part.length;
181
+ }
182
+ return result;
183
+ };
184
+ /**
185
+ * Duplex stream that bridges `pg.Client` to an in-process PGlite instance.
186
+ *
187
+ * Replaces the TCP socket in `pg.Client` via the `stream` option. Speaks
188
+ * PostgreSQL wire protocol directly to PGlite — no TCP, no serialization
189
+ * overhead beyond what the wire protocol requires.
190
+ *
191
+ * Pass to `pg.Client` or use via `createPool()` / `createPgliteAdapter()`:
192
+ *
193
+ * ```typescript
194
+ * const client = new pg.Client({
195
+ * stream: () => new PGliteBridge(pglite),
196
+ * });
197
+ * ```
198
+ */
199
+ var PGliteBridge = class extends Duplex {
200
+ pglite;
201
+ sessionLock;
202
+ bridgeId;
203
+ /** Incoming bytes not yet compacted into buf */
204
+ pending = [];
205
+ pendingLen = 0;
206
+ /** Compacted input buffer for message framing */
207
+ buf = Buffer.alloc(0);
208
+ phase = "pre_startup";
209
+ draining = false;
210
+ tornDown = false;
211
+ /** Callbacks waiting for drain to process their data */
212
+ drainQueue = [];
213
+ /** Buffered EQP messages awaiting Sync */
214
+ pipeline = [];
215
+ pipelineLen = 0;
216
+ constructor(pglite, sessionLock) {
217
+ super();
218
+ this.pglite = pglite;
219
+ this.sessionLock = sessionLock ?? null;
220
+ this.bridgeId = createBridgeId();
221
+ }
222
+ connect() {
223
+ setImmediate(() => this.emit("connect"));
224
+ return this;
225
+ }
226
+ setKeepAlive() {
227
+ return this;
228
+ }
229
+ setNoDelay() {
230
+ return this;
231
+ }
232
+ setTimeout() {
233
+ return this;
234
+ }
235
+ ref() {
236
+ return this;
237
+ }
238
+ unref() {
239
+ return this;
240
+ }
241
+ _read() {}
242
+ _write(chunk, _encoding, callback) {
243
+ this.pending.push(chunk);
244
+ this.pendingLen += chunk.length;
245
+ this.enqueue(callback);
246
+ }
247
+ /** Handles corked batches — pg.Client corks during prepared queries (P+B+D+E+S) */
248
+ _writev(chunks, callback) {
249
+ for (const { chunk } of chunks) {
250
+ this.pending.push(chunk);
251
+ this.pendingLen += chunk.length;
252
+ }
253
+ this.enqueue(callback);
254
+ }
255
+ _final(callback) {
256
+ this.sessionLock?.release(this.bridgeId);
257
+ this.push(null);
258
+ callback();
259
+ }
260
+ _destroy(error, callback) {
261
+ this.tornDown = true;
262
+ this.pipeline.length = 0;
263
+ this.pipelineLen = 0;
264
+ this.pending.length = 0;
265
+ this.pendingLen = 0;
266
+ this.sessionLock?.release(this.bridgeId);
267
+ const callbacks = this.drainQueue;
268
+ this.drainQueue = [];
269
+ for (const cb of callbacks) cb(error);
270
+ callback(error);
271
+ }
272
+ /** Merge pending chunks into buf only when needed for framing */
273
+ compact() {
274
+ if (this.pending.length === 0) return;
275
+ if (this.buf.length === 0 && this.pending.length === 1) this.buf = this.pending[0];
276
+ else this.buf = Buffer.concat([this.buf, ...this.pending]);
277
+ this.pending.length = 0;
278
+ this.pendingLen = 0;
279
+ }
280
+ /**
281
+ * Enqueue a write callback and start draining if not already running.
282
+ * The callback is NOT called until drain has processed the data.
283
+ */
284
+ enqueue(callback) {
285
+ this.drainQueue.push(callback);
286
+ if (!this.draining) this.drain().catch(() => {});
287
+ }
288
+ /**
289
+ * Process all pending data, looping until no new data arrives.
290
+ * Fires all queued callbacks on completion or error.
291
+ */
292
+ async drain() {
293
+ if (this.draining) return;
294
+ this.draining = true;
295
+ let error = null;
296
+ try {
297
+ while (this.pending.length > 0 || this.buf.length > 0) {
298
+ if (this.tornDown) break;
299
+ if (this.phase === "pre_startup") await this.processPreStartup();
300
+ if (this.phase === "ready") await this.processMessages();
301
+ if (this.pending.length === 0) break;
302
+ }
303
+ } catch (err) {
304
+ error = err instanceof Error ? err : new Error(String(err));
305
+ this.sessionLock?.release(this.bridgeId);
306
+ } finally {
307
+ this.draining = false;
308
+ const callbacks = this.drainQueue;
309
+ this.drainQueue = [];
310
+ for (const cb of callbacks) cb(error);
311
+ }
312
+ }
313
+ /**
314
+ * Frames and processes the startup message.
315
+ *
316
+ * Format: [4 bytes: total length] [4 bytes: protocol version] [key\0value\0 pairs]
317
+ * No type byte — length includes itself.
318
+ */
319
+ async processPreStartup() {
320
+ this.compact();
321
+ if (this.buf.length < 4) return;
322
+ const len = this.buf.readInt32BE(0);
323
+ if (this.buf.length < len) return;
324
+ const message = this.buf.subarray(0, len);
325
+ this.buf = this.buf.subarray(len);
326
+ await this.acquireSession();
327
+ await this.pglite.runExclusive(async () => {
328
+ await this.execAndPush(message);
329
+ });
330
+ this.phase = "ready";
331
+ }
332
+ /**
333
+ * Frames and processes regular wire protocol messages.
334
+ *
335
+ * Extended Query Protocol messages (Parse, Bind, Describe, Execute, Close,
336
+ * Flush) are buffered in `this.pipeline`. When Sync arrives, the entire
337
+ * pipeline is concatenated and sent to PGlite as one atomic
338
+ * execProtocolRawStream call within one runExclusive.
339
+ *
340
+ * SimpleQuery messages are sent directly (they're self-contained).
341
+ */
342
+ async processMessages() {
343
+ this.compact();
344
+ while (this.buf.length >= 5) {
345
+ const len = 1 + this.buf.readInt32BE(1);
346
+ if (len < 5 || this.buf.length < len) break;
347
+ const message = this.buf.subarray(0, len);
348
+ this.buf = this.buf.subarray(len);
349
+ const msgType = message[0] ?? 0;
350
+ if (msgType === TERMINATE) {
351
+ this.sessionLock?.release(this.bridgeId);
352
+ this.push(null);
353
+ return;
354
+ }
355
+ if (EQP_MESSAGES.has(msgType)) {
356
+ this.pipeline.push(message);
357
+ this.pipelineLen += message.length;
358
+ continue;
359
+ }
360
+ if (msgType === SYNC) {
361
+ this.pipeline.push(message);
362
+ this.pipelineLen += message.length;
363
+ await this.flushPipeline();
364
+ continue;
365
+ }
366
+ await this.acquireSession();
367
+ await this.pglite.runExclusive(async () => {
368
+ await this.execAndPush(message);
369
+ });
370
+ }
371
+ }
372
+ /**
373
+ * Sends the accumulated EQP pipeline as one atomic operation.
374
+ *
375
+ * All buffered messages are concatenated into a single buffer and sent
376
+ * as one execProtocolRawStream call. This is both correct (prevents
377
+ * portal interleaving) and fast (1 WASM call + 1 async boundary instead
378
+ * of 5). Intermediate ReadyForQuery messages are stripped from the
379
+ * combined response.
380
+ */
381
+ async flushPipeline() {
382
+ const messages = this.pipeline;
383
+ const totalLen = this.pipelineLen;
384
+ this.pipeline = [];
385
+ this.pipelineLen = 0;
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
+ }
396
+ await this.acquireSession();
397
+ await this.pglite.runExclusive(async () => {
398
+ const chunks = [];
399
+ await this.pglite.execProtocolRawStream(batch, { onRawData: (chunk) => chunks.push(chunk) });
400
+ if (this.tornDown || chunks.length === 0) return;
401
+ if (chunks.length === 1) {
402
+ const raw = chunks[0] ?? new Uint8Array(0);
403
+ this.trackSessionStatus(raw);
404
+ const cleaned = stripIntermediateReadyForQuery(raw);
405
+ if (cleaned.length > 0) this.push(cleaned);
406
+ return;
407
+ }
408
+ const combined = concat(chunks);
409
+ this.trackSessionStatus(combined);
410
+ const cleaned = stripIntermediateReadyForQuery(combined);
411
+ if (cleaned.length > 0) this.push(cleaned);
412
+ });
413
+ }
414
+ /**
415
+ * Sends a message to PGlite and pushes response chunks directly to the
416
+ * stream as they arrive. Avoids collecting and concatenating for large
417
+ * multi-row responses (e.g., findMany 500 rows = ~503 onRawData chunks).
418
+ *
419
+ * Must be called inside runExclusive.
420
+ */
421
+ async execAndPush(message) {
422
+ let lastChunk = null;
423
+ await this.pglite.execProtocolRawStream(message, { onRawData: (chunk) => {
424
+ if (!this.tornDown && chunk.length > 0) {
425
+ this.push(chunk);
426
+ lastChunk = chunk;
427
+ }
428
+ } });
429
+ if (lastChunk) this.trackSessionStatus(lastChunk);
430
+ }
431
+ async acquireSession() {
432
+ await this.sessionLock?.acquire(this.bridgeId);
433
+ }
434
+ trackSessionStatus(response) {
435
+ if (!this.sessionLock) return;
436
+ const status = extractRfqStatus(response);
437
+ if (status !== null) this.sessionLock.updateStatus(this.bridgeId, status);
438
+ }
439
+ };
440
+ //#endregion
441
+ //#region src/create-pool.ts
442
+ /**
443
+ * Pool factory — creates a pg.Pool backed by an in-process PGlite instance.
444
+ *
445
+ * Each pool connection gets its own PGliteBridge stream, all sharing the
446
+ * same PGlite WASM instance and SessionLock. The session lock ensures
447
+ * transaction isolation: when one bridge starts a transaction (BEGIN),
448
+ * it gets exclusive PGlite access until COMMIT/ROLLBACK. Non-transactional
449
+ * operations from any bridge serialize through PGlite's runExclusive mutex.
450
+ */
451
+ const { Client, Pool } = pg;
452
+ /**
453
+ * Creates a pg.Pool where every connection is an in-process PGlite bridge.
454
+ *
455
+ * ```typescript
456
+ * import { createPool } from 'prisma-pglite-bridge';
457
+ * import { PrismaPg } from '@prisma/adapter-pg';
458
+ * import { PrismaClient } from '@prisma/client';
459
+ *
460
+ * const { pool, close } = await createPool();
461
+ * const adapter = new PrismaPg(pool);
462
+ * const prisma = new PrismaClient({ adapter });
463
+ * ```
464
+ */
465
+ const createPool = async (options = {}) => {
466
+ const { dataDir, extensions, max = 5 } = options;
467
+ const ownsInstance = !options.pglite;
468
+ const pglite = options.pglite ?? new PGlite(dataDir, extensions ? { extensions } : void 0);
469
+ await pglite.waitReady;
470
+ const sessionLock = new SessionLock();
471
+ const BridgedClient = class extends Client {
472
+ constructor(config) {
473
+ super({
474
+ ...typeof config === "string" ? { connectionString: config } : config ?? {},
475
+ user: "postgres",
476
+ database: "postgres",
477
+ stream: (() => new PGliteBridge(pglite, sessionLock))
478
+ });
479
+ }
480
+ };
481
+ const pool = new Pool({
482
+ Client: BridgedClient,
483
+ max
484
+ });
485
+ const close = async () => {
486
+ await pool.end();
487
+ if (ownsInstance) await pglite.close();
488
+ };
489
+ return {
490
+ pool,
491
+ pglite,
492
+ close
493
+ };
494
+ };
495
+ //#endregion
496
+ //#region src/create-pglite-adapter.ts
497
+ /**
498
+ * Creates a Prisma adapter backed by in-process PGlite.
499
+ *
500
+ * No TCP, no Docker, no worker threads — everything runs in the same process.
501
+ * Works for testing, development, seeding, and scripts.
502
+ *
503
+ * ```typescript
504
+ * import { createPgliteAdapter } from 'prisma-pglite-bridge';
505
+ * import { PrismaClient } from '@prisma/client';
506
+ *
507
+ * const { adapter, resetDb } = await createPgliteAdapter();
508
+ * const prisma = new PrismaClient({ adapter });
509
+ *
510
+ * beforeEach(() => resetDb());
511
+ * ```
512
+ */
513
+ const SNAPSHOT_SCHEMA = "_pglite_snapshot";
514
+ /**
515
+ * Discover the migrations directory via Prisma's config API.
516
+ * Uses the same resolution as `prisma migrate dev` — reads prisma.config.ts,
517
+ * resolves paths relative to config file location.
518
+ *
519
+ * Returns null if @prisma/config is not available or config cannot be loaded.
520
+ */
521
+ const discoverMigrationsPath = async (configRoot) => {
522
+ try {
523
+ const { loadConfigFromFile } = await import("@prisma/config");
524
+ const { config, error } = await loadConfigFromFile({ configRoot: configRoot ?? process.cwd() });
525
+ if (error) return null;
526
+ if (config.migrations?.path) return config.migrations.path;
527
+ const schemaPath = config.schema;
528
+ if (schemaPath) return join(dirname(schemaPath), "migrations");
529
+ return null;
530
+ } catch {
531
+ return null;
532
+ }
533
+ };
534
+ /**
535
+ * Read migration SQL files from a migrations directory in directory order.
536
+ * Returns null if the directory doesn't exist or has no migration files.
537
+ */
538
+ const tryReadMigrationFiles = (migrationsPath) => {
539
+ if (!existsSync(migrationsPath)) return null;
540
+ const dirs = readdirSync(migrationsPath).filter((d) => statSync(join(migrationsPath, d)).isDirectory()).sort();
541
+ const sqlParts = [];
542
+ for (const dir of dirs) {
543
+ const sqlPath = join(migrationsPath, dir, "migration.sql");
544
+ if (existsSync(sqlPath)) sqlParts.push(readFileSync(sqlPath, "utf8"));
545
+ }
546
+ return sqlParts.length > 0 ? sqlParts.join("\n") : null;
547
+ };
548
+ /**
549
+ * Resolve schema SQL. Priority:
550
+ * 1. Explicit `sql` option — use directly
551
+ * 2. Explicit `migrationsPath` — read migration files
552
+ * 3. Auto-discovered migrations (via prisma.config.ts) — read migration files
553
+ * 4. Error — tell the user to generate migration files
554
+ */
555
+ const resolveSQL = async (options) => {
556
+ if (options.sql) return options.sql;
557
+ if (options.migrationsPath) {
558
+ const sql = tryReadMigrationFiles(options.migrationsPath);
559
+ if (sql) return sql;
560
+ throw new Error(`No migration.sql files found in ${options.migrationsPath}. Run \`prisma migrate dev\` to generate migration files.`);
561
+ }
562
+ const migrationsPath = await discoverMigrationsPath(options.configRoot);
563
+ if (migrationsPath) {
564
+ const sql = tryReadMigrationFiles(migrationsPath);
565
+ if (sql) return sql;
566
+ }
567
+ throw new Error("No migration files found. Run `prisma migrate dev` to generate them, or pass pre-generated SQL via the `sql` option.");
568
+ };
569
+ /**
570
+ * Creates a Prisma adapter backed by an in-process PGlite instance.
571
+ *
572
+ * Applies the schema and returns a ready-to-use adapter + a `resetDb`
573
+ * function for clearing tables between tests.
574
+ */
575
+ const createPgliteAdapter = async (options = {}) => {
576
+ const sql = await resolveSQL(options);
577
+ const { pool, pglite, close: poolClose } = await createPool({
578
+ dataDir: options.dataDir,
579
+ extensions: options.extensions,
580
+ max: options.max
581
+ });
582
+ try {
583
+ await pglite.exec(sql);
584
+ } catch (err) {
585
+ throw new Error("Failed to apply schema SQL to PGlite. Check your schema or migration files.", { cause: err });
586
+ }
587
+ const adapter = new PrismaPg(pool);
588
+ let cachedTables = null;
589
+ let hasSnapshot = false;
590
+ const discoverTables = async () => {
591
+ if (cachedTables !== null) return cachedTables;
592
+ const { rows } = await pglite.query(`SELECT quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified
593
+ FROM pg_tables
594
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
595
+ AND schemaname != '${SNAPSHOT_SCHEMA}'
596
+ AND tablename NOT LIKE '_prisma%'`);
597
+ cachedTables = rows.length > 0 ? rows.map((r) => r.qualified).join(", ") : "";
598
+ return cachedTables;
599
+ };
600
+ const snapshotDb = async () => {
601
+ await pglite.exec(`DROP SCHEMA IF EXISTS "${SNAPSHOT_SCHEMA}" CASCADE`);
602
+ await pglite.exec(`CREATE SCHEMA "${SNAPSHOT_SCHEMA}"`);
603
+ const { rows: tables } = await pglite.query(`SELECT quote_ident(tablename) AS tablename FROM pg_tables
604
+ WHERE schemaname = 'public'
605
+ AND tablename NOT LIKE '_prisma%'`);
606
+ for (const { tablename } of tables) await pglite.exec(`CREATE TABLE "${SNAPSHOT_SCHEMA}".${tablename} AS SELECT * FROM public.${tablename}`);
607
+ const { rows: seqs } = await pglite.query(`SELECT quote_literal(sequencename) AS name, last_value::text AS value
608
+ FROM pg_sequences WHERE schemaname = 'public' AND last_value IS NOT NULL`);
609
+ await pglite.exec(`CREATE TABLE "${SNAPSHOT_SCHEMA}".__sequences (name text, value bigint)`);
610
+ for (const { name, value } of seqs) await pglite.exec(`INSERT INTO "${SNAPSHOT_SCHEMA}".__sequences VALUES (${name}, ${value})`);
611
+ hasSnapshot = true;
612
+ };
613
+ const resetSnapshot = async () => {
614
+ hasSnapshot = false;
615
+ await pglite.exec(`DROP SCHEMA IF EXISTS "${SNAPSHOT_SCHEMA}" CASCADE`);
616
+ };
617
+ const resetDb = async () => {
618
+ const tables = await discoverTables();
619
+ if (hasSnapshot && tables) {
620
+ try {
621
+ await pglite.exec("SET session_replication_role = replica");
622
+ await pglite.exec(`TRUNCATE TABLE ${tables} CASCADE`);
623
+ const { rows: snapshotTables } = await pglite.query(`SELECT quote_ident(tablename) AS tablename FROM pg_tables
624
+ WHERE schemaname = '${SNAPSHOT_SCHEMA}'
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
+ }
630
+ const { rows: seqs } = await pglite.query(`SELECT quote_literal(name) AS name, value::text AS value FROM "${SNAPSHOT_SCHEMA}".__sequences`);
631
+ for (const { name, value } of seqs) await pglite.exec(`SELECT setval(${name}, ${value})`);
632
+ } else if (tables) try {
633
+ await pglite.exec("SET session_replication_role = replica");
634
+ await pglite.exec(`TRUNCATE TABLE ${tables} CASCADE`);
635
+ } finally {
636
+ await pglite.exec("SET session_replication_role = DEFAULT");
637
+ }
638
+ await pglite.exec("RESET ALL");
639
+ await pglite.exec("DEALLOCATE ALL");
640
+ };
641
+ return {
642
+ adapter,
643
+ pglite,
644
+ resetDb,
645
+ snapshotDb,
646
+ resetSnapshot,
647
+ close: poolClose
648
+ };
649
+ };
650
+ //#endregion
651
+ export { PGliteBridge, SessionLock, createPgliteAdapter, createPool };
652
+
653
+ //# sourceMappingURL=index.mjs.map