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.mjs CHANGED
@@ -82,6 +82,38 @@ const EQP_MESSAGES = new Set([
82
82
  const MAX_BACKEND_MESSAGE_LENGTH = 1073741824;
83
83
  //#endregion
84
84
  //#region src/duplex/row-description.ts
85
+ const readUInt32BE = (buf, offset) => {
86
+ /* c8 ignore start — callers guard fixed-width reads */
87
+ const b1 = buf[offset] ?? 0;
88
+ const b2 = buf[offset + 1] ?? 0;
89
+ const b3 = buf[offset + 2] ?? 0;
90
+ const b4 = buf[offset + 3] ?? 0;
91
+ /* c8 ignore stop */
92
+ return (b1 << 24 | b2 << 16 | b3 << 8 | b4) >>> 0;
93
+ };
94
+ const readUInt16BE = (buf, offset) => {
95
+ /* c8 ignore start — callers guard fixed-width reads */
96
+ const b1 = buf[offset] ?? 0;
97
+ const b2 = buf[offset + 1] ?? 0;
98
+ /* c8 ignore stop */
99
+ return b1 << 8 | b2;
100
+ };
101
+ const rowDescriptionNeedsRewrite = (buf, start = 0, end = buf.length) => {
102
+ if (end - start < 7) return false;
103
+ const fieldCount = readUInt16BE(buf, start + 5);
104
+ let p = start + 7;
105
+ for (let i = 0; i < fieldCount; i++) {
106
+ while (p < end && buf[p] !== 0) p++;
107
+ p++;
108
+ /* c8 ignore next — defense-in-depth: framer caller passes a complete frame */
109
+ if (p + 18 > end) return false;
110
+ const tableOID = readUInt32BE(buf, p);
111
+ p += 6;
112
+ if (readUInt32BE(buf, p) === 18 && tableOID !== 0 && tableOID < 16384) return true;
113
+ p += 12;
114
+ }
115
+ return false;
116
+ };
85
117
  /**
86
118
  * Widens any field whose dataTypeOID is 18 ("char") to oid 25 (text) — but
87
119
  * only when the field originates from a pg_catalog relation (tableOID is
@@ -94,6 +126,7 @@ const MAX_BACKEND_MESSAGE_LENGTH = 1073741824;
94
126
  * fixed-size in place.
95
127
  */
96
128
  const rewriteRowDescriptionInPlace = (buf) => {
129
+ if (!rowDescriptionNeedsRewrite(buf)) return;
97
130
  const fieldCount = buf.readInt16BE(5);
98
131
  let p = 7;
99
132
  for (let i = 0; i < fieldCount; i++) {
@@ -123,6 +156,7 @@ const rewriteRowDescriptionInPlace = (buf) => {
123
156
  */
124
157
  var BackendMessageFramer = class {
125
158
  suppressIntermediateReadyForQuery;
159
+ rewriteSystemCatalogCharOids;
126
160
  onChunk;
127
161
  onErrorResponse;
128
162
  onReadyForQuery;
@@ -138,6 +172,7 @@ var BackendMessageFramer = class {
138
172
  rowDescOffset = 0;
139
173
  constructor(options) {
140
174
  this.suppressIntermediateReadyForQuery = options.suppressIntermediateReadyForQuery ?? false;
175
+ this.rewriteSystemCatalogCharOids = options.rewriteSystemCatalogCharOids ?? true;
141
176
  this.onChunk = options.onChunk;
142
177
  this.onErrorResponse = options.onErrorResponse;
143
178
  this.onReadyForQuery = options.onReadyForQuery;
@@ -171,7 +206,7 @@ var BackendMessageFramer = class {
171
206
  if (msgType === 69) this.onErrorResponse?.();
172
207
  if (msgType === 90 && messageLength === 5) {
173
208
  flushPassthrough(offset);
174
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
209
+ this.dropStaleHeldReadyForQuery();
175
210
  /* c8 ignore next — messageLength === 5 for RFQ; payload is 1 byte */
176
211
  const status = chunk[offset + 5] ?? 0;
177
212
  this.heldRfq[0] = msgType;
@@ -186,12 +221,12 @@ var BackendMessageFramer = class {
186
221
  this.emitReadyForQuery();
187
222
  this.rfqBytesRead = 0;
188
223
  }
189
- } else if (msgType === 84) {
224
+ } else if (msgType === 84 && this.rewriteSystemCatalogCharOids && rowDescriptionNeedsRewrite(chunk, offset, offset + totalLen)) {
190
225
  flushPassthrough(offset);
191
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
226
+ this.dropStaleHeldReadyForQuery();
192
227
  this.emitRewrittenRowDescription(Buffer.from(chunk.subarray(offset, offset + totalLen)));
193
228
  } else {
194
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
229
+ this.dropStaleHeldReadyForQuery();
195
230
  if (passthroughStart < 0) passthroughStart = offset;
196
231
  }
197
232
  offset += totalLen;
@@ -199,7 +234,7 @@ var BackendMessageFramer = class {
199
234
  }
200
235
  }
201
236
  flushPassthrough(offset);
202
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
237
+ this.dropStaleHeldReadyForQuery();
203
238
  /* c8 ignore next — offset < chunk.length guaranteed by outer while */
204
239
  this.messageType = chunk[offset] ?? 0;
205
240
  this.headerBytesRead = 0;
@@ -233,7 +268,7 @@ var BackendMessageFramer = class {
233
268
  if (this.messageType === 69) this.onErrorResponse?.();
234
269
  if (this.isReadyForQueryFrame()) continue;
235
270
  this.dropHeldReadyForQuery();
236
- if (this.messageType === 84) {
271
+ if (this.messageType === 84 && this.rewriteSystemCatalogCharOids) {
237
272
  this.rowDescBuffer = Buffer.alloc(5 + this.payloadBytesRemaining);
238
273
  this.rowDescBuffer[0] = 84;
239
274
  this.rowDescBuffer.set(this.headerScratch, 1);
@@ -283,14 +318,6 @@ var BackendMessageFramer = class {
283
318
  this.rfqBytesRead = 0;
284
319
  }
285
320
  }
286
- reset() {
287
- this.messageType = void 0;
288
- this.headerBytesRead = 0;
289
- this.payloadBytesRemaining = 0;
290
- this.rfqBytesRead = 0;
291
- this.rowDescBuffer = void 0;
292
- this.rowDescOffset = 0;
293
- }
294
321
  isReadyForQueryFrame() {
295
322
  return this.messageType === 90 && this.payloadBytesRemaining === 1;
296
323
  }
@@ -307,6 +334,11 @@ var BackendMessageFramer = class {
307
334
  dropHeldReadyForQuery() {
308
335
  this.rfqBytesRead = 0;
309
336
  }
337
+ /** Drop a complete held RFQ once a following frame proves it was an
338
+ * intermediate one. No-op unless suppressing and a full RFQ is buffered. */
339
+ dropStaleHeldReadyForQuery() {
340
+ if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
341
+ }
310
342
  emitPrefix() {
311
343
  const prefix = new Uint8Array(5);
312
344
  /* c8 ignore next — messageType always set when emitPrefix is called */
@@ -475,6 +507,15 @@ var FrontendMessageBuffer = class {
475
507
  * after each sub-message (PGlite's single-user mode). These are stripped,
476
508
  * keeping only the final ReadyForQuery after Sync.
477
509
  */
510
+ const TERMINATE_MESSAGE = new Uint8Array([
511
+ 88,
512
+ 0,
513
+ 0,
514
+ 0,
515
+ 4
516
+ ]);
517
+ const PROTOCOL_CLEANUP_RAW_BYTES = 8 * 1024 * 1024;
518
+ const PROTOCOL_CLEANUP_CALLS = 32;
478
519
  /**
479
520
  * Duplex stream that bridges `pg.Client` to an in-process PGlite instance.
480
521
  *
@@ -502,6 +543,7 @@ var PGliteDuplex = class extends Duplex {
502
543
  bridgeId;
503
544
  telemetry;
504
545
  syncToFs;
546
+ rewriteSystemCatalogCharOids;
505
547
  timeout;
506
548
  duplexId;
507
549
  /** Incoming bytes framed directly from a queued chunk buffer */
@@ -526,6 +568,9 @@ var PGliteDuplex = class extends Duplex {
526
568
  /** Memoized rollback so concurrent teardown paths (e.g., `_final` then
527
569
  * `_destroy`) don't issue duplicate `ROLLBACK` statements. */
528
570
  rollbackPromise;
571
+ pendingProtocolCleanupBytes = 0;
572
+ pendingProtocolCleanupCalls = 0;
573
+ protocolCleanupUnsupported = false;
529
574
  /** Resolves once the stream has fully torn down (post-`_final` rollback,
530
575
  * post-`_destroy`). Single-shot, mirroring the `'close'` event. */
531
576
  onClose;
@@ -541,6 +586,7 @@ var PGliteDuplex = class extends Duplex {
541
586
  this.telemetry = options.telemetry;
542
587
  this.timeout = options.timeout;
543
588
  this.syncToFs = options.syncToFs ?? true;
589
+ this.rewriteSystemCatalogCharOids = options.rewriteSystemCatalogCharOids ?? true;
544
590
  this.duplexId = Symbol("duplex");
545
591
  this.onClose = new Promise((resolve) => this.once("close", () => resolve()));
546
592
  }
@@ -646,9 +692,7 @@ var PGliteDuplex = class extends Duplex {
646
692
  /* c8 ignore next — len === undefined unreachable once length ≥ 4 */
647
693
  if (len === void 0 || this.input.length < len) return;
648
694
  const message = this.input.consume(len);
649
- const session = this.acquireSession();
650
- if (session) await session;
651
- await this.runUnderRunExclusive(async () => {
695
+ await this.runUntimed(async () => {
652
696
  await this.streamProtocol(message, {
653
697
  detectErrors: false,
654
698
  suppressIntermediateRfq: false
@@ -682,6 +726,13 @@ var PGliteDuplex = class extends Duplex {
682
726
  }
683
727
  });
684
728
  }
729
+ /** Acquire the session, then run `op` under runExclusive without timing —
730
+ * the untimed path shared by startup and the no-telemetry query branch. */
731
+ async runUntimed(op) {
732
+ const session = this.acquireSession();
733
+ if (session) await session;
734
+ await this.runUnderRunExclusive(op);
735
+ }
685
736
  /**
686
737
  * Frames and processes regular wire protocol messages.
687
738
  *
@@ -739,20 +790,38 @@ var PGliteDuplex = class extends Duplex {
739
790
  let batch;
740
791
  /* c8 ignore next 3 — flushPipeline only runs after Sync is appended */
741
792
  if (messages.length === 1) batch = messages[0] ?? new Uint8Array(0);
742
- else {
743
- const total = messages.reduce((sum, p) => sum + p.length, 0);
744
- batch = new Uint8Array(total);
745
- let offset = 0;
746
- for (const part of messages) {
747
- batch.set(part, offset);
748
- offset += part.length;
749
- }
750
- }
793
+ else batch = this.tryContiguousPipelineBatch(messages) ?? this.concatPipeline(messages);
751
794
  await this.runWithTiming((detectErrors) => this.streamProtocol(batch, {
752
795
  detectErrors,
753
796
  suppressIntermediateRfq: true
754
797
  }));
755
798
  }
799
+ tryContiguousPipelineBatch(messages) {
800
+ const first = messages[0];
801
+ /* c8 ignore next — caller only passes non-empty pipelines */
802
+ if (first === void 0) return void 0;
803
+ const buffer = first.buffer;
804
+ const start = first.byteOffset;
805
+ let end = start + first.byteLength;
806
+ for (let i = 1; i < messages.length; i++) {
807
+ const part = messages[i];
808
+ /* c8 ignore next — pipeline array has no holes */
809
+ if (part === void 0) return void 0;
810
+ if (part.buffer !== buffer || part.byteOffset !== end) return;
811
+ end += part.byteLength;
812
+ }
813
+ return new Uint8Array(buffer, start, end - start);
814
+ }
815
+ concatPipeline(messages) {
816
+ const total = messages.reduce((sum, p) => sum + p.length, 0);
817
+ const batch = new Uint8Array(total);
818
+ let offset = 0;
819
+ for (const part of messages) {
820
+ batch.set(part, offset);
821
+ offset += part.length;
822
+ }
823
+ return batch;
824
+ }
756
825
  /**
757
826
  * Acquires the session, runs the op under `pglite.runExclusive`, and
758
827
  * updates internal stats and/or publishes diagnostics events when enabled.
@@ -772,9 +841,7 @@ var PGliteDuplex = class extends Duplex {
772
841
  const wantTiming = wantTelemetry || publishQuery || publishLockWait;
773
842
  const detectErrors = wantTelemetry || publishQuery;
774
843
  if (!wantTiming) {
775
- const session = this.acquireSession();
776
- if (session) await session;
777
- await this.runUnderRunExclusive(async () => {
844
+ await this.runUntimed(async () => {
778
845
  await op(false);
779
846
  });
780
847
  return;
@@ -808,10 +875,8 @@ var PGliteDuplex = class extends Duplex {
808
875
  }
809
876
  }
810
877
  /**
811
- * Sends a message (or pipelined batch) to PGlite and pushes response
812
- * chunks directly to the stream as they arrive. Avoids collecting and
813
- * concatenating for large multi-row responses (e.g., findMany 500 rows
814
- * = ~503 onRawData chunks).
878
+ * Sends a message (or pipelined batch) to PGlite and pushes the raw protocol
879
+ * response to the stream.
815
880
  *
816
881
  * For pipelined Extended Query batches, pass `suppressIntermediateRfq`
817
882
  * so only the final ReadyForQuery reaches the client.
@@ -823,6 +888,7 @@ var PGliteDuplex = class extends Duplex {
823
888
  let errSeen = false;
824
889
  const framer = new BackendMessageFramer({
825
890
  suppressIntermediateReadyForQuery: suppressIntermediateRfq,
891
+ rewriteSystemCatalogCharOids: this.rewriteSystemCatalogCharOids,
826
892
  onChunk: (chunk) => {
827
893
  /* c8 ignore next — race-only: tornDown becomes true mid-stream */
828
894
  if (!this.tornDown && chunk.length > 0) this.push(chunk);
@@ -836,15 +902,47 @@ var PGliteDuplex = class extends Duplex {
836
902
  }
837
903
  });
838
904
  await waitPGliteReady(this.pglite, this.timeout);
839
- await this.pglite.execProtocolRawStream(message, {
840
- syncToFs: this.syncToFs,
841
- onRawData: (chunk) => {
842
- framer.write(chunk);
905
+ let rawBytes = 0;
906
+ let streamFailed = false;
907
+ try {
908
+ await this.pglite.execProtocolRawStream(message, {
909
+ syncToFs: this.syncToFs,
910
+ onRawData: (chunk) => {
911
+ rawBytes += chunk.byteLength;
912
+ framer.write(chunk);
913
+ }
914
+ });
915
+ } catch (err) {
916
+ streamFailed = true;
917
+ throw err;
918
+ } finally {
919
+ this.pendingProtocolCleanupBytes += rawBytes;
920
+ this.pendingProtocolCleanupCalls++;
921
+ if (!this.protocolCleanupUnsupported && (streamFailed || this.pendingProtocolCleanupBytes >= PROTOCOL_CLEANUP_RAW_BYTES || this.pendingProtocolCleanupCalls >= PROTOCOL_CLEANUP_CALLS)) {
922
+ await this.clearPGliteProtocolMessages();
923
+ this.pendingProtocolCleanupBytes = 0;
924
+ this.pendingProtocolCleanupCalls = 0;
843
925
  }
844
- });
926
+ }
845
927
  framer.flush({ dropHeldReadyForQuery: this.tornDown });
846
928
  return !errSeen;
847
929
  }
930
+ async clearPGliteProtocolMessages() {
931
+ if (this.protocolCleanupUnsupported) return;
932
+ const { execProtocolStream } = this.pglite;
933
+ if (typeof execProtocolStream !== "function") {
934
+ this.protocolCleanupUnsupported = true;
935
+ return;
936
+ }
937
+ try {
938
+ await execProtocolStream.call(this.pglite, TERMINATE_MESSAGE, {
939
+ syncToFs: false,
940
+ throwOnError: false
941
+ });
942
+ } catch {
943
+ this.protocolCleanupUnsupported = true;
944
+ }
945
+ }
848
946
  acquireSession() {
849
947
  return this.sessionLock?.acquire(this.duplexId);
850
948
  }
@@ -995,17 +1093,12 @@ var SessionLock = class {
995
1093
  this.waitQueue = remaining;
996
1094
  return cancelled;
997
1095
  }
998
- /**
999
- * Grant ownership to the next waiter, if any.
1000
- *
1001
- * @returns `true` if a waiter was unblocked; `false` if the queue was empty.
1002
- */
1096
+ /** Grant ownership to the next waiter, if any. */
1003
1097
  drainWaitQueue() {
1004
1098
  const next = this.waitQueue.shift();
1005
- if (!next) return false;
1099
+ if (!next) return;
1006
1100
  this.owner = next.id;
1007
1101
  next.resolve();
1008
- return true;
1009
1102
  }
1010
1103
  };
1011
1104
  //#endregion
@@ -1082,7 +1175,7 @@ const wrapTypesWithFastArrayParsers = (types) => {
1082
1175
  //#endregion
1083
1176
  //#region src/pool/pg-bridge-client.ts
1084
1177
  var PgBridgeClient = class PgBridgeClient extends pg.Client {
1085
- querySubmissionChain = Promise.resolve();
1178
+ querySubmissionChain;
1086
1179
  static OptionsKey = Symbol("PgBridgeClientOptions");
1087
1180
  constructor(config) {
1088
1181
  const { [PgBridgeClient.OptionsKey]: bridge, ...clientConfig } = config ?? {};
@@ -1096,33 +1189,41 @@ var PgBridgeClient = class PgBridgeClient extends pg.Client {
1096
1189
  }
1097
1190
  query(...args) {
1098
1191
  const first = args[0];
1099
- const callSuper = () => super.query.apply(this, args);
1100
- if (first === null || first === void 0) return callSuper();
1101
- if (typeof first.submit === "function") return callSuper();
1102
- if (isObject(first) && isTypesLike(first.types)) args[0] = {
1103
- ...first,
1104
- types: wrapTypesWithFastArrayParsers(first.types)
1192
+ const submit = () => {
1193
+ return super.query.apply(this, args);
1105
1194
  };
1106
- const prior = this.querySubmissionChain;
1107
- let signalDone;
1108
- this.querySubmissionChain = new Promise((resolve) => {
1109
- signalDone = resolve;
1110
- });
1195
+ if (first === null || first === void 0) return submit();
1196
+ if (typeof first.submit === "function") return submit();
1111
1197
  const cbIndex = args.findIndex((arg) => typeof arg === "function");
1112
1198
  if (cbIndex !== -1) {
1113
1199
  const origCb = args[cbIndex];
1114
- args[cbIndex] = (err, res) => {
1115
- signalDone();
1116
- origCb(err, res);
1117
- };
1118
- prior.then(callSuper).catch((err) => {
1119
- signalDone();
1200
+ const promiseArgs = args.slice();
1201
+ promiseArgs.splice(cbIndex, 1);
1202
+ try {
1203
+ this.query(...promiseArgs).then((res) => origCb(null, res), (err) => origCb(err, void 0));
1204
+ } catch (err) {
1120
1205
  origCb(err, void 0);
1121
- });
1206
+ }
1122
1207
  return;
1123
1208
  }
1124
- const p = prior.then(callSuper);
1125
- p.then(signalDone, signalDone);
1209
+ if (isObject(first) && isTypesLike(first.types)) args[0] = {
1210
+ ...first,
1211
+ types: wrapTypesWithFastArrayParsers(first.types)
1212
+ };
1213
+ const prior = this.querySubmissionChain;
1214
+ let p;
1215
+ if (prior === void 0) try {
1216
+ p = submit();
1217
+ } catch (err) {
1218
+ return Promise.reject(err);
1219
+ }
1220
+ else p = prior.then(submit);
1221
+ let done;
1222
+ const clearChain = () => {
1223
+ if (this.querySubmissionChain === done) this.querySubmissionChain = void 0;
1224
+ };
1225
+ done = p.then(clearChain, clearChain);
1226
+ this.querySubmissionChain = done;
1126
1227
  return p;
1127
1228
  }
1128
1229
  };
@@ -1216,16 +1317,7 @@ var PgBridgePool = class extends pg.Pool {
1216
1317
  return super.end().then(cleanup);
1217
1318
  }
1218
1319
  };
1219
- //#endregion
1220
- //#region src/telemetry/bridge-stats.ts
1221
- /**
1222
- * Maximum number of recent query durations retained for percentile
1223
- * computation. Beyond this window, `recentP50QueryMs`, `recentP95QueryMs`,
1224
- * and `recentMaxQueryMs` reflect only the most recent N queries — lifetime
1225
- * counters (`queryCount`, `totalQueryMs`, `avgQueryMs`) remain complete.
1226
- */
1227
- const QUERY_DURATION_WINDOW_SIZE = 1e4;
1228
- const QUERY_DURATION_TRIM_THRESHOLD = QUERY_DURATION_WINDOW_SIZE * 2;
1320
+ const QUERY_DURATION_TRIM_THRESHOLD = 1e4 * 2;
1229
1321
  const DB_SIZE_QUERY_TIMEOUT_MS = 5e3;
1230
1322
  /**
1231
1323
  * `process.resourceUsage().maxRSS` returns kilobytes on every platform
@@ -1270,7 +1362,7 @@ var BridgeStats = class {
1270
1362
  this.queryCount += 1;
1271
1363
  this.totalQueryMs += durationMs;
1272
1364
  this.queryDurations.push(durationMs);
1273
- if (this.queryDurations.length > QUERY_DURATION_TRIM_THRESHOLD) this.queryDurations = this.queryDurations.slice(-QUERY_DURATION_WINDOW_SIZE);
1365
+ if (this.queryDurations.length > QUERY_DURATION_TRIM_THRESHOLD) this.queryDurations = this.queryDurations.slice(-1e4);
1274
1366
  if (!succeeded) this.failedQueryCount += 1;
1275
1367
  }
1276
1368
  recordLockWait(durationMs) {
@@ -1346,15 +1438,18 @@ var BridgeStats = class {
1346
1438
  }
1347
1439
  };
1348
1440
  //#endregion
1441
+ //#region src/utils/quote-ident.ts
1442
+ /** JS equivalent of PostgreSQL's `quote_ident()`; matches its escaping rules. */
1443
+ const quoteIdent = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1444
+ //#endregion
1349
1445
  //#region src/pglite-bridge/snapshot-manager.ts
1350
1446
  const SNAPSHOT_SCHEMA = "_pglite_snapshot";
1351
- const USER_TABLES_WHERE = `schemaname NOT IN ('pg_catalog', 'information_schema')
1352
- AND schemaname != '${SNAPSHOT_SCHEMA}'
1447
+ const SYSTEM_SCHEMA_EXCLUSION = `schemaname NOT IN ('pg_catalog', 'information_schema')
1448
+ AND schemaname != '${SNAPSHOT_SCHEMA}'`;
1449
+ const USER_TABLES_WHERE = `${SYSTEM_SCHEMA_EXCLUSION}
1353
1450
  AND tablename NOT LIKE '_prisma%'`;
1354
1451
  const escapeLiteral = (s) => `'${s.replace(/'/g, "''")}'`;
1355
- /** JS equivalent of PostgreSQL's `quote_ident()`; matches its escaping rules. */
1356
- const quoteIdent$1 = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1357
- const SNAPSHOT_SCHEMA_IDENT = quoteIdent$1(SNAPSHOT_SCHEMA);
1452
+ const SNAPSHOT_SCHEMA_IDENT = quoteIdent(SNAPSHOT_SCHEMA);
1358
1453
  const SNAPSHOT_SCHEMA_LITERAL = escapeLiteral(SNAPSHOT_SCHEMA);
1359
1454
  /**
1360
1455
  * Snapshot helpers backing `PGliteBridge`'s `snapshotDb` / `resetDb` /
@@ -1375,32 +1470,30 @@ var SnapshotManager = class {
1375
1470
  * the `_pglite_snapshot` schema. Replaces any previous snapshot.
1376
1471
  */
1377
1472
  async snapshotDb() {
1378
- const pglite = this.#pglite;
1379
- await pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1473
+ await this.#pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1380
1474
  try {
1381
- await pglite.exec("BEGIN");
1382
- await pglite.exec(`CREATE SCHEMA ${SNAPSHOT_SCHEMA_IDENT}`);
1383
- const { rows: tables } = await pglite.query(`SELECT schemaname, tablename,
1475
+ await this.#pglite.exec("BEGIN");
1476
+ await this.#pglite.exec(`CREATE SCHEMA ${SNAPSHOT_SCHEMA_IDENT}`);
1477
+ const { rows: tables } = await this.#pglite.query(`SELECT schemaname, tablename,
1384
1478
  quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified
1385
1479
  FROM pg_tables
1386
1480
  WHERE ${USER_TABLES_WHERE}`);
1387
- await pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__tables (snap_name text, source_schema text, source_table text)`);
1481
+ await this.#pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__tables (snap_name text, source_schema text, source_table text)`);
1388
1482
  for (const [i, { schemaname, tablename, qualified }] of tables.entries()) {
1389
1483
  const snapName = `_snap_${i}`;
1390
- await pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.${quoteIdent$1(snapName)} AS SELECT * FROM ${qualified}`);
1391
- await pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__tables VALUES (${escapeLiteral(snapName)}, ${escapeLiteral(schemaname)}, ${escapeLiteral(tablename)})`);
1484
+ await this.#pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.${quoteIdent(snapName)} AS SELECT * FROM ${qualified}`);
1485
+ await this.#pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__tables VALUES (${escapeLiteral(snapName)}, ${escapeLiteral(schemaname)}, ${escapeLiteral(tablename)})`);
1392
1486
  }
1393
- const { rows: seqs } = await pglite.query(`SELECT quote_literal(quote_ident(schemaname) || '.' || quote_ident(sequencename)) AS name, last_value::text AS value
1487
+ const { rows: seqs } = await this.#pglite.query(`SELECT quote_literal(quote_ident(schemaname) || '.' || quote_ident(sequencename)) AS name, last_value::text AS value
1394
1488
  FROM pg_sequences
1395
- WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
1396
- AND schemaname != '${SNAPSHOT_SCHEMA}'
1489
+ WHERE ${SYSTEM_SCHEMA_EXCLUSION}
1397
1490
  AND last_value IS NOT NULL`);
1398
- await pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__sequences (name text, value bigint)`);
1399
- for (const { name, value } of seqs) await pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__sequences VALUES (${name}, ${value})`);
1400
- await pglite.exec("COMMIT");
1491
+ await this.#pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__sequences (name text, value bigint)`);
1492
+ for (const { name, value } of seqs) await this.#pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__sequences VALUES (${name}, ${value})`);
1493
+ await this.#pglite.exec("COMMIT");
1401
1494
  } catch (err) {
1402
- await pglite.exec("ROLLBACK");
1403
- await pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1495
+ await this.#pglite.exec("ROLLBACK");
1496
+ await this.#pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1404
1497
  throw err;
1405
1498
  }
1406
1499
  this.#hasSnapshot = true;
@@ -1415,26 +1508,25 @@ var SnapshotManager = class {
1415
1508
  * sequence values afterwards; otherwise just truncate and `DISCARD ALL`.
1416
1509
  */
1417
1510
  async resetDb() {
1418
- const pglite = this.#pglite;
1419
1511
  if (this.#hasSnapshot) await this.#snapshotSchemaExists();
1420
1512
  const tables = await this.#getTables();
1421
1513
  if (tables) await this.#withReplicationRoleReplica(async () => {
1422
- await pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
1514
+ await this.#pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
1423
1515
  if (!this.#hasSnapshot) return;
1424
- const { rows: snapshotTables } = await pglite.query(`SELECT quote_ident(snap_name) AS snap_name_ident,
1516
+ const { rows: snapshotTables } = await this.#pglite.query(`SELECT quote_ident(snap_name) AS snap_name_ident,
1425
1517
  quote_ident(source_schema) || '.' || quote_ident(source_table) AS qualified
1426
1518
  FROM ${SNAPSHOT_SCHEMA_IDENT}.__tables`);
1427
- for (const { snap_name_ident, qualified } of snapshotTables) await pglite.exec(`INSERT INTO ${qualified} SELECT * FROM ${SNAPSHOT_SCHEMA_IDENT}.${snap_name_ident}`);
1428
- const { rows: seqs } = await pglite.query(`SELECT quote_literal(name) AS name, value::text AS value FROM ${SNAPSHOT_SCHEMA_IDENT}.__sequences`);
1429
- for (const { name, value } of seqs) await pglite.exec(`SELECT setval(${name}, ${value})`);
1519
+ for (const { snap_name_ident, qualified } of snapshotTables) await this.#pglite.exec(`INSERT INTO ${qualified} SELECT * FROM ${SNAPSHOT_SCHEMA_IDENT}.${snap_name_ident}`);
1520
+ const { rows: seqs } = await this.#pglite.query(`SELECT quote_literal(name) AS name, value::text AS value FROM ${SNAPSHOT_SCHEMA_IDENT}.__sequences`);
1521
+ for (const { name, value } of seqs) await this.#pglite.exec(`SELECT setval(${name}, ${value})`);
1430
1522
  });
1431
- await pglite.exec("DISCARD ALL");
1523
+ await this.#pglite.exec("DISCARD ALL");
1432
1524
  }
1433
1525
  async #getTables() {
1434
1526
  const { rows } = await this.#pglite.query(`SELECT quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified
1435
1527
  FROM pg_tables
1436
1528
  WHERE ${USER_TABLES_WHERE}`);
1437
- return rows.length > 0 ? rows.map((row) => row.qualified).join(", ") : "";
1529
+ return rows.map((row) => row.qualified).join(", ");
1438
1530
  }
1439
1531
  /**
1440
1532
  * Self-heal: someone (e.g. `resetSchema`) may drop `_pglite_snapshot`
@@ -1733,7 +1825,8 @@ var PGliteServer = class {
1733
1825
  socket.duplex = new PGliteDuplex(this.pglite, {
1734
1826
  sessionLock: this.#sessionLock,
1735
1827
  timeout: this.#options.timeout,
1736
- syncToFs: resolveSyncToFs(this.pglite, this.#options.syncToFs)
1828
+ syncToFs: resolveSyncToFs(this.pglite, this.#options.syncToFs),
1829
+ rewriteSystemCatalogCharOids: false
1737
1830
  });
1738
1831
  socket.duplex.on("error", () => socket.destroy());
1739
1832
  socket.once("close", () => {
@@ -1779,16 +1872,43 @@ var PGliteServer = class {
1779
1872
  }
1780
1873
  };
1781
1874
  //#endregion
1875
+ //#region src/schema/pg18-not-null.ts
1876
+ const ENGINE_DENYLIST = "contype NOT IN ('p', 'u', 'f')";
1877
+ const PATCHED_DENYLIST = "contype NOT IN ('p', 'u', 'f', 'n')";
1878
+ const rewritePg18ConstraintsSql = (sql) => {
1879
+ if (!sql.includes("pg_constraint") || !sql.includes(ENGINE_DENYLIST)) return sql;
1880
+ return sql.replace(ENGINE_DENYLIST, PATCHED_DENYLIST);
1881
+ };
1882
+ /**
1883
+ * Proxies (rather than spreads) so class-based adapters keep working: every
1884
+ * untouched member is delegated with `this` bound to the original instance,
1885
+ * and members added in future @prisma/driver-adapter-utils versions pass
1886
+ * through without changes here.
1887
+ */
1888
+ const wrapAdapter = (adapter) => new Proxy(adapter, { get(target, prop) {
1889
+ if (prop === "queryRaw") return (query) => target.queryRaw({
1890
+ ...query,
1891
+ sql: rewritePg18ConstraintsSql(query.sql)
1892
+ });
1893
+ const value = Reflect.get(target, prop);
1894
+ return typeof value === "function" ? value.bind(target) : value;
1895
+ } });
1896
+ const wrapFactoryForPg18 = (factory) => new Proxy(factory, { get(target, prop) {
1897
+ if (prop === "connect") return async () => wrapAdapter(await target.connect());
1898
+ if (prop === "connectToShadowDb") return async () => wrapAdapter(await target.connectToShadowDb());
1899
+ const value = Reflect.get(target, prop);
1900
+ return typeof value === "function" ? value.bind(target) : value;
1901
+ } });
1902
+ //#endregion
1782
1903
  //#region src/schema/index.ts
1783
1904
  const bindAdapter = async (adapter) => {
1784
1905
  const { bindMigrationAwareSqlAdapterFactory } = await import("@prisma/driver-adapter-utils");
1785
- return bindMigrationAwareSqlAdapterFactory(adapter);
1906
+ return bindMigrationAwareSqlAdapterFactory(wrapFactoryForPg18(adapter));
1786
1907
  };
1787
1908
  const emptyFilter = () => ({
1788
1909
  externalTables: [],
1789
1910
  externalEnums: []
1790
1911
  });
1791
- const quoteIdent = (name) => `"${name.replace(/"/g, "\"\"")}"`;
1792
1912
  /**
1793
1913
  * Drop every non-system schema (and recreate `public`). Issued as raw SQL
1794
1914
  * through the adapter rather than `engine.reset(...)` because the engine only
@@ -1968,7 +2088,7 @@ const hasMigrations = async (pglite) => {
1968
2088
  const { rows } = await pglite.query(`SELECT to_regclass('public._prisma_migrations') IS NOT NULL AS exists`);
1969
2089
  if (!rows[0]?.exists) return false;
1970
2090
  const { rows: applied } = await pglite.query(`SELECT count(*)::int AS count FROM _prisma_migrations WHERE finished_at IS NOT NULL`);
1971
- return applied[0].count > 0;
2091
+ return (applied[0]?.count ?? 0) > 0;
1972
2092
  };
1973
2093
  /**
1974
2094
  * Returns `true` when the `public` schema contains at least one user table.