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.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
|
-
|
|
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
|
-
|
|
226
|
+
this.dropStaleHeldReadyForQuery();
|
|
192
227
|
this.emitRewrittenRowDescription(Buffer.from(chunk.subarray(offset, offset + totalLen)));
|
|
193
228
|
} else {
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
812
|
-
*
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
|
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
|
|
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
|
|
1100
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
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
|
-
|
|
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(-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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]
|
|
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.
|