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 +230 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -9
- package/dist/index.d.mts +20 -9
- package/dist/index.mjs +230 -110
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -15
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
|
-
|
|
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
|
-
|
|
253
|
+
this.dropStaleHeldReadyForQuery();
|
|
219
254
|
this.emitRewrittenRowDescription(Buffer.from(chunk.subarray(offset, offset + totalLen)));
|
|
220
255
|
} else {
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
839
|
-
*
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
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
|
|
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
|
|
1127
|
-
|
|
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
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
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(-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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]
|
|
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.
|