prisma-pglite-bridge 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -109,6 +109,38 @@ const EQP_MESSAGES = new Set([
109
109
  const MAX_BACKEND_MESSAGE_LENGTH = 1073741824;
110
110
  //#endregion
111
111
  //#region src/duplex/row-description.ts
112
+ const readUInt32BE = (buf, offset) => {
113
+ /* c8 ignore start — callers guard fixed-width reads */
114
+ const b1 = buf[offset] ?? 0;
115
+ const b2 = buf[offset + 1] ?? 0;
116
+ const b3 = buf[offset + 2] ?? 0;
117
+ const b4 = buf[offset + 3] ?? 0;
118
+ /* c8 ignore stop */
119
+ return (b1 << 24 | b2 << 16 | b3 << 8 | b4) >>> 0;
120
+ };
121
+ const readUInt16BE = (buf, offset) => {
122
+ /* c8 ignore start — callers guard fixed-width reads */
123
+ const b1 = buf[offset] ?? 0;
124
+ const b2 = buf[offset + 1] ?? 0;
125
+ /* c8 ignore stop */
126
+ return b1 << 8 | b2;
127
+ };
128
+ const rowDescriptionNeedsRewrite = (buf, start = 0, end = buf.length) => {
129
+ if (end - start < 7) return false;
130
+ const fieldCount = readUInt16BE(buf, start + 5);
131
+ let p = start + 7;
132
+ for (let i = 0; i < fieldCount; i++) {
133
+ while (p < end && buf[p] !== 0) p++;
134
+ p++;
135
+ /* c8 ignore next — defense-in-depth: framer caller passes a complete frame */
136
+ if (p + 18 > end) return false;
137
+ const tableOID = readUInt32BE(buf, p);
138
+ p += 6;
139
+ if (readUInt32BE(buf, p) === 18 && tableOID !== 0 && tableOID < 16384) return true;
140
+ p += 12;
141
+ }
142
+ return false;
143
+ };
112
144
  /**
113
145
  * Widens any field whose dataTypeOID is 18 ("char") to oid 25 (text) — but
114
146
  * only when the field originates from a pg_catalog relation (tableOID is
@@ -121,6 +153,7 @@ const MAX_BACKEND_MESSAGE_LENGTH = 1073741824;
121
153
  * fixed-size in place.
122
154
  */
123
155
  const rewriteRowDescriptionInPlace = (buf) => {
156
+ if (!rowDescriptionNeedsRewrite(buf)) return;
124
157
  const fieldCount = buf.readInt16BE(5);
125
158
  let p = 7;
126
159
  for (let i = 0; i < fieldCount; i++) {
@@ -150,6 +183,7 @@ const rewriteRowDescriptionInPlace = (buf) => {
150
183
  */
151
184
  var BackendMessageFramer = class {
152
185
  suppressIntermediateReadyForQuery;
186
+ rewriteSystemCatalogCharOids;
153
187
  onChunk;
154
188
  onErrorResponse;
155
189
  onReadyForQuery;
@@ -165,6 +199,7 @@ var BackendMessageFramer = class {
165
199
  rowDescOffset = 0;
166
200
  constructor(options) {
167
201
  this.suppressIntermediateReadyForQuery = options.suppressIntermediateReadyForQuery ?? false;
202
+ this.rewriteSystemCatalogCharOids = options.rewriteSystemCatalogCharOids ?? true;
168
203
  this.onChunk = options.onChunk;
169
204
  this.onErrorResponse = options.onErrorResponse;
170
205
  this.onReadyForQuery = options.onReadyForQuery;
@@ -198,7 +233,7 @@ var BackendMessageFramer = class {
198
233
  if (msgType === 69) this.onErrorResponse?.();
199
234
  if (msgType === 90 && messageLength === 5) {
200
235
  flushPassthrough(offset);
201
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
236
+ this.dropStaleHeldReadyForQuery();
202
237
  /* c8 ignore next — messageLength === 5 for RFQ; payload is 1 byte */
203
238
  const status = chunk[offset + 5] ?? 0;
204
239
  this.heldRfq[0] = msgType;
@@ -213,12 +248,12 @@ var BackendMessageFramer = class {
213
248
  this.emitReadyForQuery();
214
249
  this.rfqBytesRead = 0;
215
250
  }
216
- } else if (msgType === 84) {
251
+ } else if (msgType === 84 && this.rewriteSystemCatalogCharOids && rowDescriptionNeedsRewrite(chunk, offset, offset + totalLen)) {
217
252
  flushPassthrough(offset);
218
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
253
+ this.dropStaleHeldReadyForQuery();
219
254
  this.emitRewrittenRowDescription(Buffer.from(chunk.subarray(offset, offset + totalLen)));
220
255
  } else {
221
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
256
+ this.dropStaleHeldReadyForQuery();
222
257
  if (passthroughStart < 0) passthroughStart = offset;
223
258
  }
224
259
  offset += totalLen;
@@ -226,7 +261,7 @@ var BackendMessageFramer = class {
226
261
  }
227
262
  }
228
263
  flushPassthrough(offset);
229
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
264
+ this.dropStaleHeldReadyForQuery();
230
265
  /* c8 ignore next — offset < chunk.length guaranteed by outer while */
231
266
  this.messageType = chunk[offset] ?? 0;
232
267
  this.headerBytesRead = 0;
@@ -260,7 +295,7 @@ var BackendMessageFramer = class {
260
295
  if (this.messageType === 69) this.onErrorResponse?.();
261
296
  if (this.isReadyForQueryFrame()) continue;
262
297
  this.dropHeldReadyForQuery();
263
- if (this.messageType === 84) {
298
+ if (this.messageType === 84 && this.rewriteSystemCatalogCharOids) {
264
299
  this.rowDescBuffer = Buffer.alloc(5 + this.payloadBytesRemaining);
265
300
  this.rowDescBuffer[0] = 84;
266
301
  this.rowDescBuffer.set(this.headerScratch, 1);
@@ -310,14 +345,6 @@ var BackendMessageFramer = class {
310
345
  this.rfqBytesRead = 0;
311
346
  }
312
347
  }
313
- reset() {
314
- this.messageType = void 0;
315
- this.headerBytesRead = 0;
316
- this.payloadBytesRemaining = 0;
317
- this.rfqBytesRead = 0;
318
- this.rowDescBuffer = void 0;
319
- this.rowDescOffset = 0;
320
- }
321
348
  isReadyForQueryFrame() {
322
349
  return this.messageType === 90 && this.payloadBytesRemaining === 1;
323
350
  }
@@ -334,6 +361,11 @@ var BackendMessageFramer = class {
334
361
  dropHeldReadyForQuery() {
335
362
  this.rfqBytesRead = 0;
336
363
  }
364
+ /** Drop a complete held RFQ once a following frame proves it was an
365
+ * intermediate one. No-op unless suppressing and a full RFQ is buffered. */
366
+ dropStaleHeldReadyForQuery() {
367
+ if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
368
+ }
337
369
  emitPrefix() {
338
370
  const prefix = new Uint8Array(5);
339
371
  /* c8 ignore next — messageType always set when emitPrefix is called */
@@ -502,6 +534,15 @@ var FrontendMessageBuffer = class {
502
534
  * after each sub-message (PGlite's single-user mode). These are stripped,
503
535
  * keeping only the final ReadyForQuery after Sync.
504
536
  */
537
+ const TERMINATE_MESSAGE = new Uint8Array([
538
+ 88,
539
+ 0,
540
+ 0,
541
+ 0,
542
+ 4
543
+ ]);
544
+ const PROTOCOL_CLEANUP_RAW_BYTES = 8 * 1024 * 1024;
545
+ const PROTOCOL_CLEANUP_CALLS = 32;
505
546
  /**
506
547
  * Duplex stream that bridges `pg.Client` to an in-process PGlite instance.
507
548
  *
@@ -529,6 +570,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
529
570
  bridgeId;
530
571
  telemetry;
531
572
  syncToFs;
573
+ rewriteSystemCatalogCharOids;
532
574
  timeout;
533
575
  duplexId;
534
576
  /** Incoming bytes framed directly from a queued chunk buffer */
@@ -553,6 +595,9 @@ var PGliteDuplex = class extends node_stream.Duplex {
553
595
  /** Memoized rollback so concurrent teardown paths (e.g., `_final` then
554
596
  * `_destroy`) don't issue duplicate `ROLLBACK` statements. */
555
597
  rollbackPromise;
598
+ pendingProtocolCleanupBytes = 0;
599
+ pendingProtocolCleanupCalls = 0;
600
+ protocolCleanupUnsupported = false;
556
601
  /** Resolves once the stream has fully torn down (post-`_final` rollback,
557
602
  * post-`_destroy`). Single-shot, mirroring the `'close'` event. */
558
603
  onClose;
@@ -568,6 +613,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
568
613
  this.telemetry = options.telemetry;
569
614
  this.timeout = options.timeout;
570
615
  this.syncToFs = options.syncToFs ?? true;
616
+ this.rewriteSystemCatalogCharOids = options.rewriteSystemCatalogCharOids ?? true;
571
617
  this.duplexId = Symbol("duplex");
572
618
  this.onClose = new Promise((resolve) => this.once("close", () => resolve()));
573
619
  }
@@ -673,9 +719,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
673
719
  /* c8 ignore next — len === undefined unreachable once length ≥ 4 */
674
720
  if (len === void 0 || this.input.length < len) return;
675
721
  const message = this.input.consume(len);
676
- const session = this.acquireSession();
677
- if (session) await session;
678
- await this.runUnderRunExclusive(async () => {
722
+ await this.runUntimed(async () => {
679
723
  await this.streamProtocol(message, {
680
724
  detectErrors: false,
681
725
  suppressIntermediateRfq: false
@@ -709,6 +753,13 @@ var PGliteDuplex = class extends node_stream.Duplex {
709
753
  }
710
754
  });
711
755
  }
756
+ /** Acquire the session, then run `op` under runExclusive without timing —
757
+ * the untimed path shared by startup and the no-telemetry query branch. */
758
+ async runUntimed(op) {
759
+ const session = this.acquireSession();
760
+ if (session) await session;
761
+ await this.runUnderRunExclusive(op);
762
+ }
712
763
  /**
713
764
  * Frames and processes regular wire protocol messages.
714
765
  *
@@ -766,20 +817,38 @@ var PGliteDuplex = class extends node_stream.Duplex {
766
817
  let batch;
767
818
  /* c8 ignore next 3 — flushPipeline only runs after Sync is appended */
768
819
  if (messages.length === 1) batch = messages[0] ?? new Uint8Array(0);
769
- else {
770
- const total = messages.reduce((sum, p) => sum + p.length, 0);
771
- batch = new Uint8Array(total);
772
- let offset = 0;
773
- for (const part of messages) {
774
- batch.set(part, offset);
775
- offset += part.length;
776
- }
777
- }
820
+ else batch = this.tryContiguousPipelineBatch(messages) ?? this.concatPipeline(messages);
778
821
  await this.runWithTiming((detectErrors) => this.streamProtocol(batch, {
779
822
  detectErrors,
780
823
  suppressIntermediateRfq: true
781
824
  }));
782
825
  }
826
+ tryContiguousPipelineBatch(messages) {
827
+ const first = messages[0];
828
+ /* c8 ignore next — caller only passes non-empty pipelines */
829
+ if (first === void 0) return void 0;
830
+ const buffer = first.buffer;
831
+ const start = first.byteOffset;
832
+ let end = start + first.byteLength;
833
+ for (let i = 1; i < messages.length; i++) {
834
+ const part = messages[i];
835
+ /* c8 ignore next — pipeline array has no holes */
836
+ if (part === void 0) return void 0;
837
+ if (part.buffer !== buffer || part.byteOffset !== end) return;
838
+ end += part.byteLength;
839
+ }
840
+ return new Uint8Array(buffer, start, end - start);
841
+ }
842
+ concatPipeline(messages) {
843
+ const total = messages.reduce((sum, p) => sum + p.length, 0);
844
+ const batch = new Uint8Array(total);
845
+ let offset = 0;
846
+ for (const part of messages) {
847
+ batch.set(part, offset);
848
+ offset += part.length;
849
+ }
850
+ return batch;
851
+ }
783
852
  /**
784
853
  * Acquires the session, runs the op under `pglite.runExclusive`, and
785
854
  * updates internal stats and/or publishes diagnostics events when enabled.
@@ -799,9 +868,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
799
868
  const wantTiming = wantTelemetry || publishQuery || publishLockWait;
800
869
  const detectErrors = wantTelemetry || publishQuery;
801
870
  if (!wantTiming) {
802
- const session = this.acquireSession();
803
- if (session) await session;
804
- await this.runUnderRunExclusive(async () => {
871
+ await this.runUntimed(async () => {
805
872
  await op(false);
806
873
  });
807
874
  return;
@@ -835,10 +902,8 @@ var PGliteDuplex = class extends node_stream.Duplex {
835
902
  }
836
903
  }
837
904
  /**
838
- * Sends a message (or pipelined batch) to PGlite and pushes response
839
- * chunks directly to the stream as they arrive. Avoids collecting and
840
- * concatenating for large multi-row responses (e.g., findMany 500 rows
841
- * = ~503 onRawData chunks).
905
+ * Sends a message (or pipelined batch) to PGlite and pushes the raw protocol
906
+ * response to the stream.
842
907
  *
843
908
  * For pipelined Extended Query batches, pass `suppressIntermediateRfq`
844
909
  * so only the final ReadyForQuery reaches the client.
@@ -850,6 +915,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
850
915
  let errSeen = false;
851
916
  const framer = new BackendMessageFramer({
852
917
  suppressIntermediateReadyForQuery: suppressIntermediateRfq,
918
+ rewriteSystemCatalogCharOids: this.rewriteSystemCatalogCharOids,
853
919
  onChunk: (chunk) => {
854
920
  /* c8 ignore next — race-only: tornDown becomes true mid-stream */
855
921
  if (!this.tornDown && chunk.length > 0) this.push(chunk);
@@ -863,15 +929,47 @@ var PGliteDuplex = class extends node_stream.Duplex {
863
929
  }
864
930
  });
865
931
  await waitPGliteReady(this.pglite, this.timeout);
866
- await this.pglite.execProtocolRawStream(message, {
867
- syncToFs: this.syncToFs,
868
- onRawData: (chunk) => {
869
- framer.write(chunk);
932
+ let rawBytes = 0;
933
+ let streamFailed = false;
934
+ try {
935
+ await this.pglite.execProtocolRawStream(message, {
936
+ syncToFs: this.syncToFs,
937
+ onRawData: (chunk) => {
938
+ rawBytes += chunk.byteLength;
939
+ framer.write(chunk);
940
+ }
941
+ });
942
+ } catch (err) {
943
+ streamFailed = true;
944
+ throw err;
945
+ } finally {
946
+ this.pendingProtocolCleanupBytes += rawBytes;
947
+ this.pendingProtocolCleanupCalls++;
948
+ if (!this.protocolCleanupUnsupported && (streamFailed || this.pendingProtocolCleanupBytes >= PROTOCOL_CLEANUP_RAW_BYTES || this.pendingProtocolCleanupCalls >= PROTOCOL_CLEANUP_CALLS)) {
949
+ await this.clearPGliteProtocolMessages();
950
+ this.pendingProtocolCleanupBytes = 0;
951
+ this.pendingProtocolCleanupCalls = 0;
870
952
  }
871
- });
953
+ }
872
954
  framer.flush({ dropHeldReadyForQuery: this.tornDown });
873
955
  return !errSeen;
874
956
  }
957
+ async clearPGliteProtocolMessages() {
958
+ if (this.protocolCleanupUnsupported) return;
959
+ const { execProtocolStream } = this.pglite;
960
+ if (typeof execProtocolStream !== "function") {
961
+ this.protocolCleanupUnsupported = true;
962
+ return;
963
+ }
964
+ try {
965
+ await execProtocolStream.call(this.pglite, TERMINATE_MESSAGE, {
966
+ syncToFs: false,
967
+ throwOnError: false
968
+ });
969
+ } catch {
970
+ this.protocolCleanupUnsupported = true;
971
+ }
972
+ }
875
973
  acquireSession() {
876
974
  return this.sessionLock?.acquire(this.duplexId);
877
975
  }
@@ -1022,17 +1120,12 @@ var SessionLock = class {
1022
1120
  this.waitQueue = remaining;
1023
1121
  return cancelled;
1024
1122
  }
1025
- /**
1026
- * Grant ownership to the next waiter, if any.
1027
- *
1028
- * @returns `true` if a waiter was unblocked; `false` if the queue was empty.
1029
- */
1123
+ /** Grant ownership to the next waiter, if any. */
1030
1124
  drainWaitQueue() {
1031
1125
  const next = this.waitQueue.shift();
1032
- if (!next) return false;
1126
+ if (!next) return;
1033
1127
  this.owner = next.id;
1034
1128
  next.resolve();
1035
- return true;
1036
1129
  }
1037
1130
  };
1038
1131
  //#endregion
@@ -1109,7 +1202,7 @@ const wrapTypesWithFastArrayParsers = (types) => {
1109
1202
  //#endregion
1110
1203
  //#region src/pool/pg-bridge-client.ts
1111
1204
  var PgBridgeClient = class PgBridgeClient extends pg.default.Client {
1112
- querySubmissionChain = Promise.resolve();
1205
+ querySubmissionChain;
1113
1206
  static OptionsKey = Symbol("PgBridgeClientOptions");
1114
1207
  constructor(config) {
1115
1208
  const { [PgBridgeClient.OptionsKey]: bridge, ...clientConfig } = config ?? {};
@@ -1123,33 +1216,41 @@ var PgBridgeClient = class PgBridgeClient extends pg.default.Client {
1123
1216
  }
1124
1217
  query(...args) {
1125
1218
  const first = args[0];
1126
- const callSuper = () => super.query.apply(this, args);
1127
- if (first === null || first === void 0) return callSuper();
1128
- if (typeof first.submit === "function") return callSuper();
1129
- if (isObject(first) && isTypesLike(first.types)) args[0] = {
1130
- ...first,
1131
- types: wrapTypesWithFastArrayParsers(first.types)
1219
+ const submit = () => {
1220
+ return super.query.apply(this, args);
1132
1221
  };
1133
- const prior = this.querySubmissionChain;
1134
- let signalDone;
1135
- this.querySubmissionChain = new Promise((resolve) => {
1136
- signalDone = resolve;
1137
- });
1222
+ if (first === null || first === void 0) return submit();
1223
+ if (typeof first.submit === "function") return submit();
1138
1224
  const cbIndex = args.findIndex((arg) => typeof arg === "function");
1139
1225
  if (cbIndex !== -1) {
1140
1226
  const origCb = args[cbIndex];
1141
- args[cbIndex] = (err, res) => {
1142
- signalDone();
1143
- origCb(err, res);
1144
- };
1145
- prior.then(callSuper).catch((err) => {
1146
- signalDone();
1227
+ const promiseArgs = args.slice();
1228
+ promiseArgs.splice(cbIndex, 1);
1229
+ try {
1230
+ this.query(...promiseArgs).then((res) => origCb(null, res), (err) => origCb(err, void 0));
1231
+ } catch (err) {
1147
1232
  origCb(err, void 0);
1148
- });
1233
+ }
1149
1234
  return;
1150
1235
  }
1151
- const p = prior.then(callSuper);
1152
- p.then(signalDone, signalDone);
1236
+ if (isObject(first) && isTypesLike(first.types)) args[0] = {
1237
+ ...first,
1238
+ types: wrapTypesWithFastArrayParsers(first.types)
1239
+ };
1240
+ const prior = this.querySubmissionChain;
1241
+ let p;
1242
+ if (prior === void 0) try {
1243
+ p = submit();
1244
+ } catch (err) {
1245
+ return Promise.reject(err);
1246
+ }
1247
+ else p = prior.then(submit);
1248
+ let done;
1249
+ const clearChain = () => {
1250
+ if (this.querySubmissionChain === done) this.querySubmissionChain = void 0;
1251
+ };
1252
+ done = p.then(clearChain, clearChain);
1253
+ this.querySubmissionChain = done;
1153
1254
  return p;
1154
1255
  }
1155
1256
  };
@@ -1243,16 +1344,7 @@ var PgBridgePool = class extends pg.default.Pool {
1243
1344
  return super.end().then(cleanup);
1244
1345
  }
1245
1346
  };
1246
- //#endregion
1247
- //#region src/telemetry/bridge-stats.ts
1248
- /**
1249
- * Maximum number of recent query durations retained for percentile
1250
- * computation. Beyond this window, `recentP50QueryMs`, `recentP95QueryMs`,
1251
- * and `recentMaxQueryMs` reflect only the most recent N queries — lifetime
1252
- * counters (`queryCount`, `totalQueryMs`, `avgQueryMs`) remain complete.
1253
- */
1254
- const QUERY_DURATION_WINDOW_SIZE = 1e4;
1255
- const QUERY_DURATION_TRIM_THRESHOLD = QUERY_DURATION_WINDOW_SIZE * 2;
1347
+ const QUERY_DURATION_TRIM_THRESHOLD = 1e4 * 2;
1256
1348
  const DB_SIZE_QUERY_TIMEOUT_MS = 5e3;
1257
1349
  /**
1258
1350
  * `process.resourceUsage().maxRSS` returns kilobytes on every platform
@@ -1297,7 +1389,7 @@ var BridgeStats = class {
1297
1389
  this.queryCount += 1;
1298
1390
  this.totalQueryMs += durationMs;
1299
1391
  this.queryDurations.push(durationMs);
1300
- if (this.queryDurations.length > QUERY_DURATION_TRIM_THRESHOLD) this.queryDurations = this.queryDurations.slice(-QUERY_DURATION_WINDOW_SIZE);
1392
+ if (this.queryDurations.length > QUERY_DURATION_TRIM_THRESHOLD) this.queryDurations = this.queryDurations.slice(-1e4);
1301
1393
  if (!succeeded) this.failedQueryCount += 1;
1302
1394
  }
1303
1395
  recordLockWait(durationMs) {
@@ -1373,15 +1465,18 @@ var BridgeStats = class {
1373
1465
  }
1374
1466
  };
1375
1467
  //#endregion
1468
+ //#region src/utils/quote-ident.ts
1469
+ /** JS equivalent of PostgreSQL's `quote_ident()`; matches its escaping rules. */
1470
+ const quoteIdent = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1471
+ //#endregion
1376
1472
  //#region src/pglite-bridge/snapshot-manager.ts
1377
1473
  const SNAPSHOT_SCHEMA = "_pglite_snapshot";
1378
- const USER_TABLES_WHERE = `schemaname NOT IN ('pg_catalog', 'information_schema')
1379
- AND schemaname != '${SNAPSHOT_SCHEMA}'
1474
+ const SYSTEM_SCHEMA_EXCLUSION = `schemaname NOT IN ('pg_catalog', 'information_schema')
1475
+ AND schemaname != '${SNAPSHOT_SCHEMA}'`;
1476
+ const USER_TABLES_WHERE = `${SYSTEM_SCHEMA_EXCLUSION}
1380
1477
  AND tablename NOT LIKE '_prisma%'`;
1381
1478
  const escapeLiteral = (s) => `'${s.replace(/'/g, "''")}'`;
1382
- /** JS equivalent of PostgreSQL's `quote_ident()`; matches its escaping rules. */
1383
- const quoteIdent$1 = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1384
- const SNAPSHOT_SCHEMA_IDENT = quoteIdent$1(SNAPSHOT_SCHEMA);
1479
+ const SNAPSHOT_SCHEMA_IDENT = quoteIdent(SNAPSHOT_SCHEMA);
1385
1480
  const SNAPSHOT_SCHEMA_LITERAL = escapeLiteral(SNAPSHOT_SCHEMA);
1386
1481
  /**
1387
1482
  * Snapshot helpers backing `PGliteBridge`'s `snapshotDb` / `resetDb` /
@@ -1402,32 +1497,30 @@ var SnapshotManager = class {
1402
1497
  * the `_pglite_snapshot` schema. Replaces any previous snapshot.
1403
1498
  */
1404
1499
  async snapshotDb() {
1405
- const pglite = this.#pglite;
1406
- await pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1500
+ await this.#pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1407
1501
  try {
1408
- await pglite.exec("BEGIN");
1409
- await pglite.exec(`CREATE SCHEMA ${SNAPSHOT_SCHEMA_IDENT}`);
1410
- const { rows: tables } = await pglite.query(`SELECT schemaname, tablename,
1502
+ await this.#pglite.exec("BEGIN");
1503
+ await this.#pglite.exec(`CREATE SCHEMA ${SNAPSHOT_SCHEMA_IDENT}`);
1504
+ const { rows: tables } = await this.#pglite.query(`SELECT schemaname, tablename,
1411
1505
  quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified
1412
1506
  FROM pg_tables
1413
1507
  WHERE ${USER_TABLES_WHERE}`);
1414
- await pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__tables (snap_name text, source_schema text, source_table text)`);
1508
+ await this.#pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__tables (snap_name text, source_schema text, source_table text)`);
1415
1509
  for (const [i, { schemaname, tablename, qualified }] of tables.entries()) {
1416
1510
  const snapName = `_snap_${i}`;
1417
- await pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.${quoteIdent$1(snapName)} AS SELECT * FROM ${qualified}`);
1418
- await pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__tables VALUES (${escapeLiteral(snapName)}, ${escapeLiteral(schemaname)}, ${escapeLiteral(tablename)})`);
1511
+ await this.#pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.${quoteIdent(snapName)} AS SELECT * FROM ${qualified}`);
1512
+ await this.#pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__tables VALUES (${escapeLiteral(snapName)}, ${escapeLiteral(schemaname)}, ${escapeLiteral(tablename)})`);
1419
1513
  }
1420
- const { rows: seqs } = await pglite.query(`SELECT quote_literal(quote_ident(schemaname) || '.' || quote_ident(sequencename)) AS name, last_value::text AS value
1514
+ const { rows: seqs } = await this.#pglite.query(`SELECT quote_literal(quote_ident(schemaname) || '.' || quote_ident(sequencename)) AS name, last_value::text AS value
1421
1515
  FROM pg_sequences
1422
- WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
1423
- AND schemaname != '${SNAPSHOT_SCHEMA}'
1516
+ WHERE ${SYSTEM_SCHEMA_EXCLUSION}
1424
1517
  AND last_value IS NOT NULL`);
1425
- await pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__sequences (name text, value bigint)`);
1426
- for (const { name, value } of seqs) await pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__sequences VALUES (${name}, ${value})`);
1427
- await pglite.exec("COMMIT");
1518
+ await this.#pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__sequences (name text, value bigint)`);
1519
+ for (const { name, value } of seqs) await this.#pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__sequences VALUES (${name}, ${value})`);
1520
+ await this.#pglite.exec("COMMIT");
1428
1521
  } catch (err) {
1429
- await pglite.exec("ROLLBACK");
1430
- await pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1522
+ await this.#pglite.exec("ROLLBACK");
1523
+ await this.#pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1431
1524
  throw err;
1432
1525
  }
1433
1526
  this.#hasSnapshot = true;
@@ -1442,26 +1535,25 @@ var SnapshotManager = class {
1442
1535
  * sequence values afterwards; otherwise just truncate and `DISCARD ALL`.
1443
1536
  */
1444
1537
  async resetDb() {
1445
- const pglite = this.#pglite;
1446
1538
  if (this.#hasSnapshot) await this.#snapshotSchemaExists();
1447
1539
  const tables = await this.#getTables();
1448
1540
  if (tables) await this.#withReplicationRoleReplica(async () => {
1449
- await pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
1541
+ await this.#pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
1450
1542
  if (!this.#hasSnapshot) return;
1451
- const { rows: snapshotTables } = await pglite.query(`SELECT quote_ident(snap_name) AS snap_name_ident,
1543
+ const { rows: snapshotTables } = await this.#pglite.query(`SELECT quote_ident(snap_name) AS snap_name_ident,
1452
1544
  quote_ident(source_schema) || '.' || quote_ident(source_table) AS qualified
1453
1545
  FROM ${SNAPSHOT_SCHEMA_IDENT}.__tables`);
1454
- for (const { snap_name_ident, qualified } of snapshotTables) await pglite.exec(`INSERT INTO ${qualified} SELECT * FROM ${SNAPSHOT_SCHEMA_IDENT}.${snap_name_ident}`);
1455
- const { rows: seqs } = await pglite.query(`SELECT quote_literal(name) AS name, value::text AS value FROM ${SNAPSHOT_SCHEMA_IDENT}.__sequences`);
1456
- for (const { name, value } of seqs) await pglite.exec(`SELECT setval(${name}, ${value})`);
1546
+ for (const { snap_name_ident, qualified } of snapshotTables) await this.#pglite.exec(`INSERT INTO ${qualified} SELECT * FROM ${SNAPSHOT_SCHEMA_IDENT}.${snap_name_ident}`);
1547
+ const { rows: seqs } = await this.#pglite.query(`SELECT quote_literal(name) AS name, value::text AS value FROM ${SNAPSHOT_SCHEMA_IDENT}.__sequences`);
1548
+ for (const { name, value } of seqs) await this.#pglite.exec(`SELECT setval(${name}, ${value})`);
1457
1549
  });
1458
- await pglite.exec("DISCARD ALL");
1550
+ await this.#pglite.exec("DISCARD ALL");
1459
1551
  }
1460
1552
  async #getTables() {
1461
1553
  const { rows } = await this.#pglite.query(`SELECT quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified
1462
1554
  FROM pg_tables
1463
1555
  WHERE ${USER_TABLES_WHERE}`);
1464
- return rows.length > 0 ? rows.map((row) => row.qualified).join(", ") : "";
1556
+ return rows.map((row) => row.qualified).join(", ");
1465
1557
  }
1466
1558
  /**
1467
1559
  * Self-heal: someone (e.g. `resetSchema`) may drop `_pglite_snapshot`
@@ -1760,7 +1852,8 @@ var PGliteServer = class {
1760
1852
  socket.duplex = new PGliteDuplex(this.pglite, {
1761
1853
  sessionLock: this.#sessionLock,
1762
1854
  timeout: this.#options.timeout,
1763
- syncToFs: resolveSyncToFs(this.pglite, this.#options.syncToFs)
1855
+ syncToFs: resolveSyncToFs(this.pglite, this.#options.syncToFs),
1856
+ rewriteSystemCatalogCharOids: false
1764
1857
  });
1765
1858
  socket.duplex.on("error", () => socket.destroy());
1766
1859
  socket.once("close", () => {
@@ -1806,16 +1899,43 @@ var PGliteServer = class {
1806
1899
  }
1807
1900
  };
1808
1901
  //#endregion
1902
+ //#region src/schema/pg18-not-null.ts
1903
+ const ENGINE_DENYLIST = "contype NOT IN ('p', 'u', 'f')";
1904
+ const PATCHED_DENYLIST = "contype NOT IN ('p', 'u', 'f', 'n')";
1905
+ const rewritePg18ConstraintsSql = (sql) => {
1906
+ if (!sql.includes("pg_constraint") || !sql.includes(ENGINE_DENYLIST)) return sql;
1907
+ return sql.replace(ENGINE_DENYLIST, PATCHED_DENYLIST);
1908
+ };
1909
+ /**
1910
+ * Proxies (rather than spreads) so class-based adapters keep working: every
1911
+ * untouched member is delegated with `this` bound to the original instance,
1912
+ * and members added in future @prisma/driver-adapter-utils versions pass
1913
+ * through without changes here.
1914
+ */
1915
+ const wrapAdapter = (adapter) => new Proxy(adapter, { get(target, prop) {
1916
+ if (prop === "queryRaw") return (query) => target.queryRaw({
1917
+ ...query,
1918
+ sql: rewritePg18ConstraintsSql(query.sql)
1919
+ });
1920
+ const value = Reflect.get(target, prop);
1921
+ return typeof value === "function" ? value.bind(target) : value;
1922
+ } });
1923
+ const wrapFactoryForPg18 = (factory) => new Proxy(factory, { get(target, prop) {
1924
+ if (prop === "connect") return async () => wrapAdapter(await target.connect());
1925
+ if (prop === "connectToShadowDb") return async () => wrapAdapter(await target.connectToShadowDb());
1926
+ const value = Reflect.get(target, prop);
1927
+ return typeof value === "function" ? value.bind(target) : value;
1928
+ } });
1929
+ //#endregion
1809
1930
  //#region src/schema/index.ts
1810
1931
  const bindAdapter = async (adapter) => {
1811
1932
  const { bindMigrationAwareSqlAdapterFactory } = await import("@prisma/driver-adapter-utils");
1812
- return bindMigrationAwareSqlAdapterFactory(adapter);
1933
+ return bindMigrationAwareSqlAdapterFactory(wrapFactoryForPg18(adapter));
1813
1934
  };
1814
1935
  const emptyFilter = () => ({
1815
1936
  externalTables: [],
1816
1937
  externalEnums: []
1817
1938
  });
1818
- const quoteIdent = (name) => `"${name.replace(/"/g, "\"\"")}"`;
1819
1939
  /**
1820
1940
  * Drop every non-system schema (and recreate `public`). Issued as raw SQL
1821
1941
  * through the adapter rather than `engine.reset(...)` because the engine only
@@ -1995,7 +2115,7 @@ const hasMigrations = async (pglite) => {
1995
2115
  const { rows } = await pglite.query(`SELECT to_regclass('public._prisma_migrations') IS NOT NULL AS exists`);
1996
2116
  if (!rows[0]?.exists) return false;
1997
2117
  const { rows: applied } = await pglite.query(`SELECT count(*)::int AS count FROM _prisma_migrations WHERE finished_at IS NOT NULL`);
1998
- return applied[0].count > 0;
2118
+ return (applied[0]?.count ?? 0) > 0;
1999
2119
  };
2000
2120
  /**
2001
2121
  * Returns `true` when the `public` schema contains at least one user table.