@effect/sql-pg 4.0.0-rc.111 → 4.0.0-rc.112

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.
@@ -0,0 +1,1160 @@
1
+ /**
2
+ * Wire codec for the PostgreSQL frontend/backend protocol, version 3.0.
3
+ *
4
+ * The module encodes frontend messages and decodes backend messages. Every
5
+ * function is pure: bytes in, bytes or plain data out. Nothing here opens a
6
+ * socket, negotiates TLS, or tracks session state, and nothing here decodes
7
+ * column values - `DataRow` fields stay raw bytes for `PgTypes` to interpret.
8
+ *
9
+ * Typed messages are a type byte, an `int32` length that counts itself but not
10
+ * the type byte, and a payload. Integers are big-endian and strings are
11
+ * NUL-terminated UTF-8 unless they are explicitly length-prefixed.
12
+ *
13
+ * Encoded frames and decoded byte fields are views into pooled buffers that
14
+ * are written once and never rewritten. They stay valid for as long as they
15
+ * are held, but holding one keeps its whole pool buffer alive, so copy
16
+ * anything that has to outlive the message it came from.
17
+ *
18
+ * @since 4.0.0
19
+ */
20
+ import * as Data from "effect/Data";
21
+ import * as Result from "effect/Result";
22
+ /**
23
+ * Default `maxMessageSize` for `makeParser`: 16 MiB.
24
+ *
25
+ * @category constants
26
+ * @since 4.0.0
27
+ */
28
+ export const defaultMaxMessageSize = 16 * 1024 * 1024;
29
+ /** Where a parser stops growing its buffer pool. */
30
+ const maxBufferSize = 64 * 1024;
31
+ /**
32
+ * Creates a `Parser`.
33
+ *
34
+ * Special pre-startup replies have no type byte and are not handled here; use
35
+ * `decodeSslResponse` for those.
36
+ *
37
+ * @category constructors
38
+ * @since 4.0.0
39
+ */
40
+ export const makeParser = options => {
41
+ const maxMessageSize = options?.maxMessageSize ?? defaultMaxMessageSize;
42
+ const reader = new Reader();
43
+ let bufferSize = 8192;
44
+ let buffer = new Uint8Array(bufferSize);
45
+ let store = buffer.buffer;
46
+ let start = 0;
47
+ let end = 0;
48
+ let failed = false;
49
+ // Bytes already handed to the caller are never overwritten, so a full buffer
50
+ // is replaced rather than compacted in place. That lets `DataRow` fields be
51
+ // views instead of copies, which is the difference between one allocation per
52
+ // buffer and one per column.
53
+ //
54
+ // Every refill therefore allocates, so the pool doubles up to
55
+ // `maxBufferSize`: a busy connection spreads the allocation over more
56
+ // messages while a low-volume one stays small. A single oversized message
57
+ // grows its buffer beyond the pool without raising the pool itself.
58
+ const append = chunk => {
59
+ if (end + chunk.length > buffer.length) {
60
+ const pending = end - start;
61
+ if (bufferSize < maxBufferSize) bufferSize *= 2;
62
+ let capacity = bufferSize;
63
+ while (capacity < pending + chunk.length) capacity *= 2;
64
+ const next = new Uint8Array(capacity);
65
+ next.set(buffer.subarray(start, end));
66
+ buffer = next;
67
+ store = next.buffer;
68
+ start = 0;
69
+ end = pending;
70
+ }
71
+ buffer.set(chunk, end);
72
+ end += chunk.length;
73
+ };
74
+ return {
75
+ readField: options?.readField,
76
+ push(chunk) {
77
+ if (failed) {
78
+ throw new ParseError({
79
+ message: "Parser cannot be reused after a failure"
80
+ });
81
+ }
82
+ try {
83
+ append(chunk);
84
+ const messages = [];
85
+ while (end - start >= 5) {
86
+ const length = buffer[start + 1] << 24 | buffer[start + 2] << 16 | buffer[start + 3] << 8 | buffer[start + 4];
87
+ if (length < 4) {
88
+ throw new ParseError({
89
+ message: `Invalid message length: ${length}`
90
+ });
91
+ }
92
+ if (length > maxMessageSize) {
93
+ throw new ParseError({
94
+ message: `Message length ${length} exceeds maxMessageSize ${maxMessageSize}`
95
+ });
96
+ }
97
+ if (end - start < length + 1) break;
98
+ const type = buffer[start];
99
+ const body = start + 5;
100
+ const limit = start + 1 + length;
101
+ start = limit;
102
+ if (type === BackendType.DataRow) {
103
+ // `buffer` always starts at byte 0 of `store`, so offsets index both.
104
+ messages.push(decodeDataRow(buffer, store, 0, body, limit, this.readField));
105
+ } else {
106
+ reader.reset(buffer, body, limit);
107
+ const message = decodeBackend(type, reader);
108
+ if (reader.offset !== limit) {
109
+ throw new ParseError({
110
+ message: `Message has ${limit - reader.offset} trailing byte(s)`
111
+ });
112
+ }
113
+ messages.push(message);
114
+ }
115
+ }
116
+ return messages;
117
+ } catch (error) {
118
+ failed = true;
119
+ throw error;
120
+ }
121
+ }
122
+ };
123
+ };
124
+ /**
125
+ * Error produced when bytes cannot be interpreted as a protocol message.
126
+ *
127
+ * @category errors
128
+ * @since 4.0.0
129
+ */
130
+ export class ParseError extends /*#__PURE__*/Data.TaggedError("PgProtocolParseError") {}
131
+ /**
132
+ * Error returned when a frontend message cannot be encoded.
133
+ *
134
+ * @category errors
135
+ * @since 4.0.0
136
+ */
137
+ export class EncodeError extends /*#__PURE__*/Data.TaggedError("PgProtocolEncodeError") {}
138
+ const textEncoder = /*#__PURE__*/new TextEncoder();
139
+ const textDecoder = /*#__PURE__*/new TextDecoder("utf-8", {
140
+ fatal: true
141
+ });
142
+ /**
143
+ * Above this length `TextEncoder.encodeInto` beats a per-character loop, below
144
+ * it the call overhead dominates. Measured on V8: the loop runs at about a
145
+ * nanosecond per character and `encodeInto` costs about 50 ns whatever the
146
+ * length, so the crossover is around 50 characters.
147
+ */
148
+ const asciiEncodeLimit = 48;
149
+ /**
150
+ * A view over part of a cached backing store. Slicing runs once per column of
151
+ * every row, and both obvious spellings are slower than this one: `subarray`
152
+ * consults the constructor's `Symbol.species` before it can allocate, and
153
+ * reading `.buffer` off a typed array is an accessor call rather than a field
154
+ * load. Callers hold the store and the array's byte offset instead.
155
+ */
156
+ const view = (store, offset, length) => new Uint8Array(store, offset, length);
157
+ /** Up to this many bytes a copy loop beats `Uint8Array.prototype.set`. */
158
+ const smallCopyLimit = 8;
159
+ /**
160
+ * Writes messages back to back into a pooled buffer and hands out a view of
161
+ * each one, so encoding a message costs no allocation of its own. Bytes below
162
+ * `start` have already been handed out and are never rewritten; when the pool
163
+ * runs out it is replaced rather than reused.
164
+ */
165
+ class Writer {
166
+ poolSize;
167
+ bytes;
168
+ view;
169
+ /** Where the message currently being written begins. */
170
+ start = 0;
171
+ offset = 0;
172
+ /** Set by `sqlNull`, read and cleared by the `makeBindEncoder` loop. */
173
+ isNull = false;
174
+ constructor(poolSize) {
175
+ this.poolSize = poolSize;
176
+ this.bytes = new Uint8Array(poolSize);
177
+ this.view = new DataView(this.bytes.buffer);
178
+ }
179
+ reserve(size) {
180
+ if (this.offset + size <= this.bytes.length) return;
181
+ const pending = this.offset - this.start;
182
+ let capacity = this.poolSize;
183
+ while (capacity < pending + size) capacity *= 2;
184
+ const next = new Uint8Array(capacity);
185
+ next.set(this.bytes.subarray(this.start, this.offset));
186
+ this.bytes = next;
187
+ this.view = new DataView(next.buffer);
188
+ this.start = 0;
189
+ this.offset = pending;
190
+ }
191
+ /** Starts a message, dropping anything a failed write left behind. */
192
+ begin() {
193
+ this.start = this.offset;
194
+ }
195
+ uint8(value) {
196
+ this.reserve(1);
197
+ this.bytes[this.offset++] = value;
198
+ }
199
+ int16(value) {
200
+ this.reserve(2);
201
+ const bytes = this.bytes;
202
+ const offset = this.offset;
203
+ bytes[offset] = value >>> 8;
204
+ bytes[offset + 1] = value;
205
+ this.offset = offset + 2;
206
+ }
207
+ int32(value) {
208
+ this.reserve(4);
209
+ this.setInt32(this.offset, value);
210
+ this.offset += 4;
211
+ }
212
+ setInt32(offset, value) {
213
+ const bytes = this.bytes;
214
+ bytes[offset] = value >>> 24;
215
+ bytes[offset + 1] = value >>> 16;
216
+ bytes[offset + 2] = value >>> 8;
217
+ bytes[offset + 3] = value;
218
+ }
219
+ float32(value) {
220
+ this.reserve(4);
221
+ this.view.setFloat32(this.offset, value);
222
+ this.offset += 4;
223
+ }
224
+ float64(value) {
225
+ this.reserve(8);
226
+ this.view.setFloat64(this.offset, value);
227
+ this.offset += 8;
228
+ }
229
+ bigInt64(value) {
230
+ this.reserve(8);
231
+ this.view.setBigInt64(this.offset, value);
232
+ this.offset += 8;
233
+ }
234
+ raw(value) {
235
+ const length = value.length;
236
+ this.reserve(length);
237
+ const bytes = this.bytes;
238
+ const offset = this.offset;
239
+ if (length <= smallCopyLimit) {
240
+ for (let index = 0; index < length; index++) bytes[offset + index] = value[index];
241
+ } else {
242
+ bytes.set(value, offset);
243
+ }
244
+ this.offset = offset + length;
245
+ }
246
+ sqlNull() {
247
+ this.isNull = true;
248
+ }
249
+ beginLength() {
250
+ // Relative to `start`, because a later write may move the message to a new
251
+ // pool buffer, which rebases `start` and `offset` but not the distance
252
+ // between them.
253
+ const token = this.offset - this.start;
254
+ this.int32(0);
255
+ return token;
256
+ }
257
+ endLength(token) {
258
+ this.setInt32(this.start + token, this.offset - this.start - token - 4);
259
+ }
260
+ utf8(value, nul = false) {
261
+ const length = value.length;
262
+ if (length <= asciiEncodeLimit) {
263
+ this.reserve(length + (nul ? 1 : 0));
264
+ const bytes = this.bytes;
265
+ const start = this.offset;
266
+ let i = 0;
267
+ for (; i < length; i++) {
268
+ const code = value.charCodeAt(i);
269
+ if (code > 0x7f) break;
270
+ bytes[start + i] = code;
271
+ }
272
+ if (i === length) {
273
+ const offset = start + length;
274
+ if (nul) bytes[offset] = 0;
275
+ this.offset = offset + (nul ? 1 : 0);
276
+ return;
277
+ }
278
+ }
279
+ // UTF-8 takes at most three bytes per UTF-16 code unit, and four for the
280
+ // two units of a surrogate pair, so this covers any string.
281
+ this.reserve(length * 3 + (nul ? 1 : 0));
282
+ this.offset += textEncoder.encodeInto(value, this.bytes.subarray(this.offset)).written;
283
+ if (nul) this.bytes[this.offset++] = 0;
284
+ }
285
+ cString(value) {
286
+ this.utf8(value, true);
287
+ }
288
+ finish() {
289
+ const value = view(this.bytes.buffer, this.bytes.byteOffset + this.start, this.offset - this.start);
290
+ if (this.bytes.length > this.poolSize) {
291
+ // An oversized message grew the pool; do not keep the rest of it around.
292
+ this.bytes = new Uint8Array(this.poolSize);
293
+ this.view = new DataView(this.bytes.buffer);
294
+ this.start = 0;
295
+ this.offset = 0;
296
+ } else {
297
+ this.start = this.offset;
298
+ }
299
+ return value;
300
+ }
301
+ }
302
+ const emptyBytes = /*#__PURE__*/new Uint8Array(0);
303
+ /**
304
+ * A cursor over a message payload. The parser reuses one instance pointed at a
305
+ * window of its own buffer, so decoding a message allocates nothing beyond the
306
+ * message itself.
307
+ */
308
+ class Reader {
309
+ bytes = emptyBytes;
310
+ /** The backing store of `bytes` and its offset into it, resolved per message. */
311
+ store = emptyBytes.buffer;
312
+ base = 0;
313
+ offset = 0;
314
+ limit = 0;
315
+ reset(bytes, offset, limit) {
316
+ this.bytes = bytes;
317
+ this.store = bytes.buffer;
318
+ this.base = bytes.byteOffset;
319
+ this.offset = offset;
320
+ this.limit = limit;
321
+ }
322
+ require(size) {
323
+ if (size < 0) {
324
+ throw new ParseError({
325
+ message: `Invalid read of ${size} byte(s)`
326
+ });
327
+ }
328
+ if (this.offset + size > this.limit) {
329
+ throw new ParseError({
330
+ message: `Truncated message: expected ${size} more byte(s)`
331
+ });
332
+ }
333
+ }
334
+ uint8() {
335
+ this.require(1);
336
+ return this.bytes[this.offset++];
337
+ }
338
+ int16() {
339
+ this.require(2);
340
+ const bytes = this.bytes;
341
+ const offset = this.offset;
342
+ this.offset = offset + 2;
343
+ return (bytes[offset] << 8 | bytes[offset + 1]) << 16 >> 16;
344
+ }
345
+ int32() {
346
+ this.require(4);
347
+ const bytes = this.bytes;
348
+ const offset = this.offset;
349
+ this.offset = offset + 4;
350
+ return bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3];
351
+ }
352
+ uint32() {
353
+ return this.int32() >>> 0;
354
+ }
355
+ raw(size) {
356
+ this.require(size);
357
+ const value = view(this.store, this.base + this.offset, size);
358
+ this.offset += size;
359
+ return value;
360
+ }
361
+ rest() {
362
+ return this.raw(this.limit - this.offset);
363
+ }
364
+ cString() {
365
+ const end = this.bytes.indexOf(0, this.offset);
366
+ if (end === -1 || end >= this.limit) {
367
+ throw new ParseError({
368
+ message: "Unterminated string"
369
+ });
370
+ }
371
+ const value = decodeUtf8(this.bytes, this.offset, end - this.offset);
372
+ this.offset = end + 1;
373
+ return value;
374
+ }
375
+ }
376
+ /** Below this length a per-character loop beats `TextDecoder.decode`. */
377
+ const asciiDecodeLimit = 10;
378
+ /**
379
+ * Node's own UTF-8 decoder; see the note on its `PgTypes` counterpart. A
380
+ * result containing a replacement character goes to the strict decoder, so
381
+ * invalid bytes still fail exactly as they did.
382
+ */
383
+ const utf8Slice = globalThis.Buffer?.prototype?.utf8Slice;
384
+ const decodeUtf8 = (bytes, offset, size) => {
385
+ if (size <= asciiDecodeLimit) {
386
+ let text = "";
387
+ let index = 0;
388
+ for (; index < size; index++) {
389
+ const code = bytes[offset + index];
390
+ if (code > 0x7f) break;
391
+ text += String.fromCharCode(code);
392
+ }
393
+ if (index === size) return text;
394
+ }
395
+ if (utf8Slice !== undefined) {
396
+ const text = utf8Slice.call(bytes, offset, offset + size);
397
+ if (text.indexOf("\ufffd") === -1) return text;
398
+ }
399
+ try {
400
+ return textDecoder.decode(view(bytes.buffer, bytes.byteOffset + offset, size));
401
+ } catch {
402
+ throw new ParseError({
403
+ message: "Invalid UTF-8 in message"
404
+ });
405
+ }
406
+ };
407
+ const sharedWriter = /*#__PURE__*/new Writer(8192);
408
+ /**
409
+ * Opens a typed message and leaves room for its length. Paired with `end`,
410
+ * which backfills it. A pair rather than a `write` callback, because a
411
+ * callback allocates a closure over the caller's options on every message.
412
+ */
413
+ const begin = type => {
414
+ const writer = sharedWriter;
415
+ writer.begin();
416
+ writer.reserve(5);
417
+ const bytes = writer.bytes;
418
+ const offset = writer.offset;
419
+ bytes[offset] = type;
420
+ writer.offset = offset + 5;
421
+ return writer;
422
+ };
423
+ const end = () => {
424
+ // Relative to `start`, because writing may have moved the message to a new
425
+ // pool buffer. The length counts itself but not the type byte.
426
+ sharedWriter.setInt32(sharedWriter.start + 1, sharedWriter.offset - sharedWriter.start - 1);
427
+ return sharedWriter.finish();
428
+ };
429
+ const empty = type => {
430
+ const writer = begin(type);
431
+ writer.setInt32(writer.start + 1, 4);
432
+ return writer.finish();
433
+ };
434
+ const targetByte = target => target === "statement" ? 0x53 : 0x50;
435
+ const requireInt16Count = (count, name) => {
436
+ if (count > 0x7fff) throw new EncodeError({
437
+ message: `${name} count exceeds 32767: ${count}`
438
+ });
439
+ return count;
440
+ };
441
+ const encodeResult = evaluate => {
442
+ try {
443
+ return Result.succeed(evaluate());
444
+ } catch (error) {
445
+ if (error instanceof EncodeError) return Result.fail(error);
446
+ throw error;
447
+ }
448
+ };
449
+ /**
450
+ * Encodes a `Parse` message, returning `EncodeError` when its parameter count
451
+ * is outside the signed int16 wire range.
452
+ *
453
+ * @category encoding
454
+ * @since 4.0.0
455
+ */
456
+ const encodeParseUnsafe = options => {
457
+ const writer = begin(0x50);
458
+ writer.cString(options.name);
459
+ writer.cString(options.query);
460
+ const parameterTypes = options.parameterTypes;
461
+ const count = requireInt16Count(parameterTypes.length, "Parse parameter type");
462
+ writer.reserve(2 + count * 4);
463
+ const bytes = writer.bytes;
464
+ let offset = writer.offset;
465
+ bytes[offset] = count >>> 8;
466
+ bytes[offset + 1] = count;
467
+ offset += 2;
468
+ for (let index = 0; index < count; index++) {
469
+ const oid = parameterTypes[index];
470
+ bytes[offset] = oid >>> 24;
471
+ bytes[offset + 1] = oid >>> 16;
472
+ bytes[offset + 2] = oid >>> 8;
473
+ bytes[offset + 3] = oid;
474
+ offset += 4;
475
+ }
476
+ writer.offset = offset;
477
+ return end();
478
+ };
479
+ export const encodeParse = options => encodeResult(() => encodeParseUnsafe(options));
480
+ /**
481
+ * Encodes a `Bind` message using the binary format code for parameters and
482
+ * results, returning `EncodeError` when its parameter count is outside the
483
+ * signed int16 wire range.
484
+ *
485
+ * @category encoding
486
+ * @since 4.0.0
487
+ */
488
+ const encodeBindUnsafe = options => {
489
+ const writer = begin(0x42);
490
+ writer.cString(options.portal);
491
+ writer.cString(options.statement);
492
+ const parameters = options.parameters;
493
+ const count = requireInt16Count(parameters.length, "Bind parameter");
494
+ // Sizing the rest of the frame up front turns every remaining write into a
495
+ // plain store: one bounds check for the message instead of one per field.
496
+ let size = 10 + count * 4;
497
+ for (let index = 0; index < count; index++) {
498
+ const parameter = parameters[index];
499
+ if (parameter !== null) size += parameter.length;
500
+ }
501
+ writer.reserve(size);
502
+ const bytes = writer.bytes;
503
+ let offset = writer.offset;
504
+ // One parameter format code, binary, for every parameter.
505
+ bytes[offset] = 0;
506
+ bytes[offset + 1] = 1;
507
+ bytes[offset + 2] = 0;
508
+ bytes[offset + 3] = 1;
509
+ bytes[offset + 4] = count >>> 8;
510
+ bytes[offset + 5] = count;
511
+ offset += 6;
512
+ for (let index = 0; index < count; index++) {
513
+ const parameter = parameters[index];
514
+ if (parameter === null) {
515
+ bytes[offset] = 0xff;
516
+ bytes[offset + 1] = 0xff;
517
+ bytes[offset + 2] = 0xff;
518
+ bytes[offset + 3] = 0xff;
519
+ offset += 4;
520
+ } else {
521
+ const length = parameter.length;
522
+ bytes[offset] = length >>> 24;
523
+ bytes[offset + 1] = length >>> 16;
524
+ bytes[offset + 2] = length >>> 8;
525
+ bytes[offset + 3] = length;
526
+ offset += 4;
527
+ if (length <= smallCopyLimit) {
528
+ for (let byte = 0; byte < length; byte++) bytes[offset + byte] = parameter[byte];
529
+ } else {
530
+ bytes.set(parameter, offset);
531
+ }
532
+ offset += length;
533
+ }
534
+ }
535
+ // One result format code, binary, for every column.
536
+ bytes[offset] = 0;
537
+ bytes[offset + 1] = 1;
538
+ bytes[offset + 2] = 0;
539
+ bytes[offset + 3] = 1;
540
+ writer.offset = offset + 4;
541
+ return end();
542
+ };
543
+ export const encodeBind = options => encodeResult(() => encodeBindUnsafe(options));
544
+ /**
545
+ * Builds a `Bind` encoder that writes parameters straight into the frame,
546
+ * skipping the array per parameter that `encodeBind` has to copy from.
547
+ *
548
+ * ```ts
549
+ * import { PgProtocol, PgTypes } from "@effect/sql-pg"
550
+ *
551
+ * const encodeBind = PgProtocol.makeBindEncoder(PgTypes.writeParameter)
552
+ * const frame = encodeBind({ portal: "", statement: "s1", parameters: [PgTypes.int4(1)] })
553
+ * ```
554
+ *
555
+ * @category encoding
556
+ * @since 4.0.0
557
+ */
558
+ const valueWriterUnsafe = /*#__PURE__*/Symbol.for("@effect/sql-pg/PgProtocol/ValueWriter/unsafe");
559
+ export const makeBindEncoder = writeParameter => options => {
560
+ try {
561
+ const writer = begin(0x42);
562
+ writer.cString(options.portal);
563
+ writer.cString(options.statement);
564
+ const parameters = options.parameters;
565
+ const count = requireInt16Count(parameters.length, "Bind parameter");
566
+ writer.reserve(6);
567
+ const header = writer.bytes;
568
+ const headerOffset = writer.offset;
569
+ header[headerOffset] = 0;
570
+ header[headerOffset + 1] = 1;
571
+ header[headerOffset + 2] = 0;
572
+ header[headerOffset + 3] = 1;
573
+ header[headerOffset + 4] = count >>> 8;
574
+ header[headerOffset + 5] = count;
575
+ writer.offset = headerOffset + 6;
576
+ const writeUnsafe = writeParameter[valueWriterUnsafe];
577
+ for (let index = 0; index < count; index++) {
578
+ const token = writer.beginLength();
579
+ writer.isNull = false;
580
+ if (writeUnsafe === undefined) {
581
+ const written = writeParameter(writer, parameters[index]);
582
+ if (Result.isFailure(written)) return Result.fail(written.failure);
583
+ } else {
584
+ writeUnsafe(writer, parameters[index]);
585
+ }
586
+ if (writer.isNull) {
587
+ writer.isNull = false;
588
+ writer.offset = writer.start + token;
589
+ writer.int32(-1);
590
+ } else {
591
+ writer.endLength(token);
592
+ }
593
+ }
594
+ writer.reserve(4);
595
+ const trailer = writer.bytes;
596
+ const trailerOffset = writer.offset;
597
+ trailer[trailerOffset] = 0;
598
+ trailer[trailerOffset + 1] = 1;
599
+ trailer[trailerOffset + 2] = 0;
600
+ trailer[trailerOffset + 3] = 1;
601
+ writer.offset = trailerOffset + 4;
602
+ return Result.succeed(end());
603
+ } catch (error) {
604
+ if (error instanceof EncodeError) return Result.fail(error);
605
+ throw error;
606
+ }
607
+ };
608
+ /**
609
+ * Encodes an `Execute` message.
610
+ *
611
+ * @category encoding
612
+ * @since 4.0.0
613
+ */
614
+ export const encodeExecute = options => {
615
+ const writer = begin(0x45);
616
+ writer.cString(options.portal);
617
+ writer.int32(options.maxRows);
618
+ return end();
619
+ };
620
+ /**
621
+ * Encodes a `Describe` message.
622
+ *
623
+ * @category encoding
624
+ * @since 4.0.0
625
+ */
626
+ export const encodeDescribe = options => {
627
+ const writer = begin(0x44);
628
+ writer.uint8(targetByte(options.target));
629
+ writer.cString(options.name);
630
+ return end();
631
+ };
632
+ /**
633
+ * Encodes a `Close` message.
634
+ *
635
+ * @category encoding
636
+ * @since 4.0.0
637
+ */
638
+ export const encodeClose = options => {
639
+ const writer = begin(0x43);
640
+ writer.uint8(targetByte(options.target));
641
+ writer.cString(options.name);
642
+ return end();
643
+ };
644
+ /**
645
+ * Encodes a `Sync` message.
646
+ *
647
+ * @category encoding
648
+ * @since 4.0.0
649
+ */
650
+ export const encodeSync = () => empty(0x53);
651
+ /**
652
+ * Encodes a `Flush` message.
653
+ *
654
+ * @category encoding
655
+ * @since 4.0.0
656
+ */
657
+ export const encodeFlush = () => empty(0x48);
658
+ /**
659
+ * Encodes a `Terminate` message.
660
+ *
661
+ * @category encoding
662
+ * @since 4.0.0
663
+ */
664
+ export const encodeTerminate = () => empty(0x58);
665
+ /**
666
+ * Encodes a `PasswordMessage`. The password is sent verbatim, so MD5 hashing
667
+ * belongs to the caller - see `PgAuth.md5Password`.
668
+ *
669
+ * @category encoding
670
+ * @since 4.0.0
671
+ */
672
+ export const encodePasswordMessage = options => {
673
+ const writer = begin(0x70);
674
+ writer.cString(options.password);
675
+ return end();
676
+ };
677
+ /**
678
+ * Encodes a `SASLInitialResponse` message.
679
+ *
680
+ * @category encoding
681
+ * @since 4.0.0
682
+ */
683
+ export const encodeSASLInitialResponse = options => {
684
+ const writer = begin(0x70);
685
+ writer.cString(options.mechanism);
686
+ if (options.initialResponse === null) {
687
+ writer.int32(-1);
688
+ } else {
689
+ writer.int32(options.initialResponse.length);
690
+ writer.raw(options.initialResponse);
691
+ }
692
+ return end();
693
+ };
694
+ /**
695
+ * Encodes a `SASLResponse` message.
696
+ *
697
+ * @category encoding
698
+ * @since 4.0.0
699
+ */
700
+ export const encodeSASLResponse = options => {
701
+ const writer = begin(0x70);
702
+ writer.raw(options.data);
703
+ return end();
704
+ };
705
+ /**
706
+ * Encodes any frontend message.
707
+ *
708
+ * @category encoding
709
+ * @since 4.0.0
710
+ */
711
+ export const encode = message => {
712
+ switch (message._tag) {
713
+ case "Parse":
714
+ return encodeParse(message);
715
+ case "Bind":
716
+ return encodeBind(message);
717
+ case "Execute":
718
+ return Result.succeed(encodeExecute(message));
719
+ case "Describe":
720
+ return Result.succeed(encodeDescribe(message));
721
+ case "Close":
722
+ return Result.succeed(encodeClose(message));
723
+ case "Sync":
724
+ return Result.succeed(encodeSync());
725
+ case "Flush":
726
+ return Result.succeed(encodeFlush());
727
+ case "Terminate":
728
+ return Result.succeed(encodeTerminate());
729
+ case "PasswordMessage":
730
+ return Result.succeed(encodePasswordMessage(message));
731
+ case "SASLInitialResponse":
732
+ return Result.succeed(encodeSASLInitialResponse(message));
733
+ case "SASLResponse":
734
+ return Result.succeed(encodeSASLResponse(message));
735
+ }
736
+ };
737
+ // -----------------------------------------------------------------------------
738
+ // special messages
739
+ // -----------------------------------------------------------------------------
740
+ const PROTOCOL_VERSION_3_0 = 196608;
741
+ const SSL_REQUEST_CODE = 80877103;
742
+ const CANCEL_REQUEST_CODE = 80877102;
743
+ /**
744
+ * Encodes an `SSLRequest`. It has no type byte and is only valid before
745
+ * startup.
746
+ *
747
+ * @category encoding
748
+ * @since 4.0.0
749
+ */
750
+ export const encodeSslRequest = () => {
751
+ sharedWriter.begin();
752
+ sharedWriter.int32(8);
753
+ sharedWriter.int32(SSL_REQUEST_CODE);
754
+ return sharedWriter.finish();
755
+ };
756
+ /**
757
+ * Decodes the single byte the server sends in reply to an `SSLRequest`. `"S"`
758
+ * means the server will speak TLS, `"N"` means it will not.
759
+ *
760
+ * @category decoding
761
+ * @since 4.0.0
762
+ */
763
+ export const decodeSslResponse = byte => byte === 0x53 ? Result.succeed("S") : byte === 0x4e ? Result.succeed("N") : Result.fail(new ParseError({
764
+ message: `Invalid SSLRequest response byte: ${byte}`
765
+ }));
766
+ /**
767
+ * Encodes a `StartupMessage` for protocol 3.0. It has no type byte.
768
+ * `client_encoding` defaults to `UTF8` because this codec always writes UTF-8.
769
+ *
770
+ * @category encoding
771
+ * @since 4.0.0
772
+ */
773
+ export const encodeStartupMessage = parameters => {
774
+ const writer = sharedWriter;
775
+ writer.begin();
776
+ writer.int32(0);
777
+ writer.int32(PROTOCOL_VERSION_3_0);
778
+ for (const [key, value] of Object.entries(parameters)) {
779
+ if (value === undefined) continue;
780
+ writer.cString(key);
781
+ writer.cString(value);
782
+ }
783
+ if (parameters.client_encoding === undefined) {
784
+ writer.cString("client_encoding");
785
+ writer.cString("UTF8");
786
+ }
787
+ writer.uint8(0);
788
+ writer.setInt32(writer.start, writer.offset - writer.start);
789
+ return writer.finish();
790
+ };
791
+ /**
792
+ * Encodes a `CancelRequest`. It has no type byte and is sent on a separate
793
+ * connection, using the `pid` and `secret` from `BackendKeyData`.
794
+ *
795
+ * @category encoding
796
+ * @since 4.0.0
797
+ */
798
+ export const encodeCancelRequest = options => {
799
+ const writer = sharedWriter;
800
+ writer.begin();
801
+ writer.int32(16);
802
+ writer.int32(CANCEL_REQUEST_CODE);
803
+ writer.int32(options.pid);
804
+ writer.int32(options.secret);
805
+ return writer.finish();
806
+ };
807
+ const BackendType = {
808
+ NotificationResponse: 0x41,
809
+ // A
810
+ CommandComplete: 0x43,
811
+ // C
812
+ DataRow: 0x44,
813
+ // D
814
+ ErrorResponse: 0x45,
815
+ // E
816
+ CopyInResponse: 0x47,
817
+ // G
818
+ CopyOutResponse: 0x48,
819
+ // H
820
+ EmptyQueryResponse: 0x49,
821
+ // I
822
+ BackendKeyData: 0x4b,
823
+ // K
824
+ NoticeResponse: 0x4e,
825
+ // N
826
+ Authentication: 0x52,
827
+ // R
828
+ ParameterStatus: 0x53,
829
+ // S
830
+ RowDescription: 0x54,
831
+ // T
832
+ CopyBothResponse: 0x57,
833
+ // W
834
+ ReadyForQuery: 0x5a,
835
+ // Z
836
+ CopyDone: 0x63,
837
+ // c
838
+ CopyData: 0x64,
839
+ // d
840
+ NoData: 0x6e,
841
+ // n
842
+ PortalSuspended: 0x73,
843
+ // s
844
+ ParameterDescription: 0x74,
845
+ // t
846
+ NegotiateProtocolVersion: 0x76,
847
+ // v
848
+ ParseComplete: 0x31,
849
+ // 1
850
+ BindComplete: 0x32,
851
+ // 2
852
+ CloseComplete: 0x33 // 3
853
+ };
854
+ const errorFieldNames = {
855
+ S: "severity",
856
+ V: "severityUnlocalized",
857
+ C: "code",
858
+ M: "message",
859
+ D: "detail",
860
+ H: "hint",
861
+ P: "position",
862
+ p: "internalPosition",
863
+ q: "internalQuery",
864
+ W: "where",
865
+ s: "schema",
866
+ t: "table",
867
+ c: "column",
868
+ d: "dataType",
869
+ n: "constraint",
870
+ F: "file",
871
+ L: "line",
872
+ R: "routine"
873
+ };
874
+ const decodeAuthentication = reader => {
875
+ const method = reader.int32();
876
+ switch (method) {
877
+ case 0:
878
+ return {
879
+ _tag: "AuthenticationOk"
880
+ };
881
+ case 3:
882
+ return {
883
+ _tag: "AuthenticationCleartextPassword"
884
+ };
885
+ case 5:
886
+ return {
887
+ _tag: "AuthenticationMD5Password",
888
+ salt: reader.raw(4)
889
+ };
890
+ case 10:
891
+ {
892
+ const mechanisms = [];
893
+ for (;;) {
894
+ const mechanism = reader.cString();
895
+ if (mechanism === "") break;
896
+ mechanisms.push(mechanism);
897
+ }
898
+ return {
899
+ _tag: "AuthenticationSASL",
900
+ mechanisms
901
+ };
902
+ }
903
+ case 11:
904
+ return {
905
+ _tag: "AuthenticationSASLContinue",
906
+ data: reader.rest()
907
+ };
908
+ case 12:
909
+ return {
910
+ _tag: "AuthenticationSASLFinal",
911
+ data: reader.rest()
912
+ };
913
+ default:
914
+ return {
915
+ _tag: "AuthenticationUnsupported",
916
+ method,
917
+ payload: reader.rest()
918
+ };
919
+ }
920
+ };
921
+ const decodeErrorFields = reader => {
922
+ const fields = {};
923
+ for (;;) {
924
+ const code = reader.uint8();
925
+ if (code === 0) break;
926
+ const key = String.fromCharCode(code);
927
+ fields[errorFieldNames[key] ?? key] = reader.cString();
928
+ }
929
+ return fields;
930
+ };
931
+ const requireNonNegativeCount = (count, name) => {
932
+ if (count < 0) {
933
+ throw new ParseError({
934
+ message: `Invalid ${name} count: ${count}`
935
+ });
936
+ }
937
+ return count;
938
+ };
939
+ const decodeCopyResponse = reader => {
940
+ const format = reader.uint8();
941
+ const count = requireNonNegativeCount(reader.int16(), "COPY column");
942
+ const columnFormats = new Array(count);
943
+ for (let i = 0; i < count; i++) {
944
+ columnFormats[i] = reader.int16();
945
+ }
946
+ return {
947
+ format,
948
+ columnFormats
949
+ };
950
+ };
951
+ // The one message that arrives per result row, so it reads the frame directly
952
+ // instead of going through `Reader`. Callers have already checked that the
953
+ // whole frame is buffered, which turns every field read into one bounds check
954
+ // against `limit`.
955
+ const decodeDataRow = (bytes, store, base, offset, limit, readField) => {
956
+ if (offset + 2 > limit) {
957
+ throw new ParseError({
958
+ message: "Truncated message: expected 2 more byte(s)"
959
+ });
960
+ }
961
+ const count = (bytes[offset] << 8 | bytes[offset + 1]) << 16 >> 16;
962
+ if (count < 0) {
963
+ throw new ParseError({
964
+ message: `Invalid DataRow field count: ${count}`
965
+ });
966
+ }
967
+ const values = new Array(count);
968
+ let position = offset + 2;
969
+ for (let i = 0; i < count; i++) {
970
+ if (position + 4 > limit) {
971
+ throw new ParseError({
972
+ message: "Truncated message: expected 4 more byte(s)"
973
+ });
974
+ }
975
+ const size = bytes[position] << 24 | bytes[position + 1] << 16 | bytes[position + 2] << 8 | bytes[position + 3];
976
+ position += 4;
977
+ if (size < 0) {
978
+ if (size < -1) {
979
+ throw new ParseError({
980
+ message: `Invalid DataRow field length: ${size}`
981
+ });
982
+ }
983
+ values[i] = readField === undefined ? null : readField(bytes, position, -1, i);
984
+ continue;
985
+ }
986
+ const next = position + size;
987
+ if (next > limit) {
988
+ throw new ParseError({
989
+ message: `Truncated message: expected ${size} more byte(s)`
990
+ });
991
+ }
992
+ values[i] = readField === undefined ? view(store, base + position, size) : readField(bytes, position, size, i);
993
+ position = next;
994
+ }
995
+ if (position !== limit) {
996
+ throw new ParseError({
997
+ message: `DataRow has ${limit - position} trailing byte(s)`
998
+ });
999
+ }
1000
+ return {
1001
+ _tag: "DataRow",
1002
+ values
1003
+ };
1004
+ };
1005
+ const decodeBackend = (type, reader) => {
1006
+ switch (type) {
1007
+ case BackendType.Authentication:
1008
+ return decodeAuthentication(reader);
1009
+ case BackendType.ParameterStatus:
1010
+ return {
1011
+ _tag: "ParameterStatus",
1012
+ name: reader.cString(),
1013
+ value: reader.cString()
1014
+ };
1015
+ case BackendType.BackendKeyData:
1016
+ return {
1017
+ _tag: "BackendKeyData",
1018
+ pid: reader.int32(),
1019
+ secret: reader.int32()
1020
+ };
1021
+ case BackendType.ReadyForQuery:
1022
+ {
1023
+ const status = String.fromCharCode(reader.uint8());
1024
+ if (status !== "I" && status !== "T" && status !== "E") {
1025
+ throw new ParseError({
1026
+ message: `Invalid ReadyForQuery status: ${status}`
1027
+ });
1028
+ }
1029
+ return {
1030
+ _tag: "ReadyForQuery",
1031
+ status
1032
+ };
1033
+ }
1034
+ case BackendType.RowDescription:
1035
+ {
1036
+ const count = requireNonNegativeCount(reader.int16(), "RowDescription field");
1037
+ const fields = new Array(count);
1038
+ for (let i = 0; i < count; i++) {
1039
+ fields[i] = {
1040
+ name: reader.cString(),
1041
+ tableOid: reader.uint32(),
1042
+ columnAttributeNumber: reader.int16(),
1043
+ dataTypeOid: reader.uint32(),
1044
+ dataTypeSize: reader.int16(),
1045
+ typeModifier: reader.int32(),
1046
+ format: reader.int16()
1047
+ };
1048
+ }
1049
+ return {
1050
+ _tag: "RowDescription",
1051
+ fields
1052
+ };
1053
+ }
1054
+ case BackendType.DataRow:
1055
+ return decodeDataRow(reader.bytes, reader.store, reader.base, reader.offset, reader.limit, undefined);
1056
+ case BackendType.CommandComplete:
1057
+ return {
1058
+ _tag: "CommandComplete",
1059
+ commandTag: reader.cString()
1060
+ };
1061
+ case BackendType.EmptyQueryResponse:
1062
+ return {
1063
+ _tag: "EmptyQueryResponse"
1064
+ };
1065
+ case BackendType.NoData:
1066
+ return {
1067
+ _tag: "NoData"
1068
+ };
1069
+ case BackendType.ParseComplete:
1070
+ return {
1071
+ _tag: "ParseComplete"
1072
+ };
1073
+ case BackendType.BindComplete:
1074
+ return {
1075
+ _tag: "BindComplete"
1076
+ };
1077
+ case BackendType.CloseComplete:
1078
+ return {
1079
+ _tag: "CloseComplete"
1080
+ };
1081
+ case BackendType.PortalSuspended:
1082
+ return {
1083
+ _tag: "PortalSuspended"
1084
+ };
1085
+ case BackendType.ParameterDescription:
1086
+ {
1087
+ const count = requireNonNegativeCount(reader.int16(), "ParameterDescription parameter");
1088
+ const parameterTypes = new Array(count);
1089
+ for (let i = 0; i < count; i++) {
1090
+ parameterTypes[i] = reader.uint32();
1091
+ }
1092
+ return {
1093
+ _tag: "ParameterDescription",
1094
+ parameterTypes
1095
+ };
1096
+ }
1097
+ case BackendType.ErrorResponse:
1098
+ return {
1099
+ _tag: "ErrorResponse",
1100
+ fields: decodeErrorFields(reader)
1101
+ };
1102
+ case BackendType.NoticeResponse:
1103
+ return {
1104
+ _tag: "NoticeResponse",
1105
+ fields: decodeErrorFields(reader)
1106
+ };
1107
+ case BackendType.NotificationResponse:
1108
+ return {
1109
+ _tag: "NotificationResponse",
1110
+ pid: reader.int32(),
1111
+ channel: reader.cString(),
1112
+ payload: reader.cString()
1113
+ };
1114
+ case BackendType.NegotiateProtocolVersion:
1115
+ {
1116
+ const minorVersion = reader.int32();
1117
+ const count = requireNonNegativeCount(reader.int32(), "NegotiateProtocolVersion option");
1118
+ const unrecognizedOptions = new Array(count);
1119
+ for (let i = 0; i < count; i++) {
1120
+ unrecognizedOptions[i] = reader.cString();
1121
+ }
1122
+ return {
1123
+ _tag: "NegotiateProtocolVersion",
1124
+ minorVersion,
1125
+ unrecognizedOptions
1126
+ };
1127
+ }
1128
+ case BackendType.CopyInResponse:
1129
+ return {
1130
+ _tag: "CopyInResponse",
1131
+ ...decodeCopyResponse(reader)
1132
+ };
1133
+ case BackendType.CopyOutResponse:
1134
+ return {
1135
+ _tag: "CopyOutResponse",
1136
+ ...decodeCopyResponse(reader)
1137
+ };
1138
+ case BackendType.CopyBothResponse:
1139
+ return {
1140
+ _tag: "CopyBothResponse",
1141
+ ...decodeCopyResponse(reader)
1142
+ };
1143
+ case BackendType.CopyData:
1144
+ return {
1145
+ _tag: "CopyData",
1146
+ data: reader.rest()
1147
+ };
1148
+ case BackendType.CopyDone:
1149
+ return {
1150
+ _tag: "CopyDone"
1151
+ };
1152
+ default:
1153
+ return {
1154
+ _tag: "Unknown",
1155
+ type,
1156
+ payload: reader.rest()
1157
+ };
1158
+ }
1159
+ };
1160
+ //# sourceMappingURL=PgProtocol.js.map