@effect/sql-pg 4.0.0-rc.110 → 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,1820 @@
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
+ /**
24
+ * Default `maxMessageSize` for `makeParser`: 16 MiB.
25
+ *
26
+ * @category constants
27
+ * @since 4.0.0
28
+ */
29
+ export const defaultMaxMessageSize = 16 * 1024 * 1024
30
+
31
+ /** Where a parser stops growing its buffer pool. */
32
+ const maxBufferSize = 64 * 1024
33
+
34
+ /**
35
+ * An incremental decoder for the post-startup backend message stream.
36
+ *
37
+ * @category models
38
+ * @since 4.0.0
39
+ */
40
+ export interface Parser<A = Uint8Array | null> {
41
+ /**
42
+ * Reads each `DataRow` field, for a parser built with one. A result's
43
+ * columns are only known from its `RowDescription`, which arrives on the
44
+ * same stream, so this is settable: replace it when the columns change.
45
+ */
46
+ readField: FieldReader<A> | undefined
47
+
48
+ /**
49
+ * Feeds the next chunk of socket bytes and returns every message that is now
50
+ * complete. A partial trailing message is retained until the bytes that
51
+ * finish it arrive. A thrown parse or field-reader error is terminal and the
52
+ * parser cannot be reused afterward; any messages decoded earlier in the
53
+ * failing push are discarded.
54
+ *
55
+ * Byte fields on the returned messages - `DataRow` values, `CopyData` and
56
+ * `Unknown` payloads - are views into an internal buffer that the parser
57
+ * never rewrites, so they stay valid for as long as they are held. They are
58
+ * meant to be consumed before the next `push`: holding one keeps its whole
59
+ * buffer alive, so copy it if it has to outlive the row.
60
+ */
61
+ readonly push: (chunk: Uint8Array) => ReadonlyArray<BackendMessage<A>>
62
+ }
63
+
64
+ /**
65
+ * Creates a `Parser`.
66
+ *
67
+ * Special pre-startup replies have no type byte and are not handled here; use
68
+ * `decodeSslResponse` for those.
69
+ *
70
+ * @category constructors
71
+ * @since 4.0.0
72
+ */
73
+ export const makeParser = <A = Uint8Array | null>(options?: {
74
+ readonly maxMessageSize?: number | undefined
75
+ /**
76
+ * Reads each `DataRow` field as it is parsed, so a client that decodes its
77
+ * columns never needs a view per column. Without one every field is handed
78
+ * out as a view, which is the default.
79
+ */
80
+ readonly readField?: FieldReader<A> | undefined
81
+ }): Parser<A> => {
82
+ const maxMessageSize = options?.maxMessageSize ?? defaultMaxMessageSize
83
+ const reader = new Reader()
84
+ let bufferSize = 8192
85
+ let buffer = new Uint8Array(bufferSize)
86
+ let store = buffer.buffer
87
+ let start = 0
88
+ let end = 0
89
+ let failed = false
90
+
91
+ // Bytes already handed to the caller are never overwritten, so a full buffer
92
+ // is replaced rather than compacted in place. That lets `DataRow` fields be
93
+ // views instead of copies, which is the difference between one allocation per
94
+ // buffer and one per column.
95
+ //
96
+ // Every refill therefore allocates, so the pool doubles up to
97
+ // `maxBufferSize`: a busy connection spreads the allocation over more
98
+ // messages while a low-volume one stays small. A single oversized message
99
+ // grows its buffer beyond the pool without raising the pool itself.
100
+ const append = (chunk: Uint8Array): void => {
101
+ if (end + chunk.length > buffer.length) {
102
+ const pending = end - start
103
+ if (bufferSize < maxBufferSize) bufferSize *= 2
104
+ let capacity = bufferSize
105
+ while (capacity < pending + chunk.length) capacity *= 2
106
+ const next = new Uint8Array(capacity)
107
+ next.set(buffer.subarray(start, end))
108
+ buffer = next
109
+ store = next.buffer
110
+ start = 0
111
+ end = pending
112
+ }
113
+ buffer.set(chunk, end)
114
+ end += chunk.length
115
+ }
116
+
117
+ return {
118
+ readField: options?.readField,
119
+ push(chunk) {
120
+ if (failed) {
121
+ throw new ParseError({ message: "Parser cannot be reused after a failure" })
122
+ }
123
+ try {
124
+ append(chunk)
125
+ const messages: Array<BackendMessage<A>> = []
126
+ while (end - start >= 5) {
127
+ const length = (buffer[start + 1] << 24) | (buffer[start + 2] << 16) | (buffer[start + 3] << 8) |
128
+ buffer[start + 4]
129
+ if (length < 4) {
130
+ throw new ParseError({ message: `Invalid message length: ${length}` })
131
+ }
132
+ if (length > maxMessageSize) {
133
+ throw new ParseError({
134
+ message: `Message length ${length} exceeds maxMessageSize ${maxMessageSize}`
135
+ })
136
+ }
137
+ if (end - start < length + 1) break
138
+ const type = buffer[start]
139
+ const body = start + 5
140
+ const limit = start + 1 + length
141
+ start = limit
142
+ if (type === BackendType.DataRow) {
143
+ // `buffer` always starts at byte 0 of `store`, so offsets index both.
144
+ messages.push(decodeDataRow<A>(buffer, store, 0, body, limit, this.readField))
145
+ } else {
146
+ reader.reset(buffer, body, limit)
147
+ const message = decodeBackend(type, reader)
148
+ if (reader.offset !== limit) {
149
+ throw new ParseError({ message: `Message has ${limit - reader.offset} trailing byte(s)` })
150
+ }
151
+ messages.push(message as BackendMessage<A>)
152
+ }
153
+ }
154
+ return messages
155
+ } catch (error) {
156
+ failed = true
157
+ throw error
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ // -----------------------------------------------------------------------------
164
+ // frontend messages
165
+ // -----------------------------------------------------------------------------
166
+
167
+ /**
168
+ * Prepares a named or unnamed statement.
169
+ *
170
+ * @category models
171
+ * @since 4.0.0
172
+ */
173
+ export interface Parse {
174
+ readonly _tag: "Parse"
175
+ readonly name: string
176
+ readonly query: string
177
+ readonly parameterTypes: ReadonlyArray<number>
178
+ }
179
+
180
+ /**
181
+ * Binds parameter values to a prepared statement, creating a portal.
182
+ *
183
+ * Parameters and results always use the binary format code.
184
+ *
185
+ * @category models
186
+ * @since 4.0.0
187
+ */
188
+ export interface Bind {
189
+ readonly _tag: "Bind"
190
+ readonly portal: string
191
+ readonly statement: string
192
+ readonly parameters: ReadonlyArray<Uint8Array | null>
193
+ }
194
+
195
+ /**
196
+ * Runs a portal, optionally limiting the number of rows returned.
197
+ *
198
+ * @category models
199
+ * @since 4.0.0
200
+ */
201
+ export interface Execute {
202
+ readonly _tag: "Execute"
203
+ readonly portal: string
204
+ readonly maxRows: number
205
+ }
206
+
207
+ /**
208
+ * Which kind of object a `Describe` or `Close` message names.
209
+ *
210
+ * @category models
211
+ * @since 4.0.0
212
+ */
213
+ export type DescribeTarget = "statement" | "portal"
214
+
215
+ /**
216
+ * Asks for the parameter and row shape of a statement or portal.
217
+ *
218
+ * @category models
219
+ * @since 4.0.0
220
+ */
221
+ export interface Describe {
222
+ readonly _tag: "Describe"
223
+ readonly target: DescribeTarget
224
+ readonly name: string
225
+ }
226
+
227
+ /**
228
+ * Drops a prepared statement or portal.
229
+ *
230
+ * @category models
231
+ * @since 4.0.0
232
+ */
233
+ export interface Close {
234
+ readonly _tag: "Close"
235
+ readonly target: DescribeTarget
236
+ readonly name: string
237
+ }
238
+
239
+ /**
240
+ * Closes the current transaction block and requests a `ReadyForQuery`.
241
+ *
242
+ * @category models
243
+ * @since 4.0.0
244
+ */
245
+ export interface Sync {
246
+ readonly _tag: "Sync"
247
+ }
248
+
249
+ /**
250
+ * Asks the backend to deliver buffered output without ending the transaction.
251
+ *
252
+ * @category models
253
+ * @since 4.0.0
254
+ */
255
+ export interface Flush {
256
+ readonly _tag: "Flush"
257
+ }
258
+
259
+ /**
260
+ * Ends the session.
261
+ *
262
+ * @category models
263
+ * @since 4.0.0
264
+ */
265
+ export interface Terminate {
266
+ readonly _tag: "Terminate"
267
+ }
268
+
269
+ /**
270
+ * Answers a cleartext or MD5 password request.
271
+ *
272
+ * @category models
273
+ * @since 4.0.0
274
+ */
275
+ export interface PasswordMessage {
276
+ readonly _tag: "PasswordMessage"
277
+ readonly password: string
278
+ }
279
+
280
+ /**
281
+ * Selects a SASL mechanism and carries its opaque initial response.
282
+ *
283
+ * @category models
284
+ * @since 4.0.0
285
+ */
286
+ export interface SASLInitialResponse {
287
+ readonly _tag: "SASLInitialResponse"
288
+ readonly mechanism: string
289
+ readonly initialResponse: Uint8Array | null
290
+ }
291
+
292
+ /**
293
+ * Carries an opaque SASL continuation payload.
294
+ *
295
+ * @category models
296
+ * @since 4.0.0
297
+ */
298
+ export interface SASLResponse {
299
+ readonly _tag: "SASLResponse"
300
+ readonly data: Uint8Array
301
+ }
302
+
303
+ /**
304
+ * Any message the client sends after startup.
305
+ *
306
+ * @category models
307
+ * @since 4.0.0
308
+ */
309
+ export type FrontendMessage =
310
+ | Parse
311
+ | Bind
312
+ | Execute
313
+ | Describe
314
+ | Close
315
+ | Sync
316
+ | Flush
317
+ | Terminate
318
+ | PasswordMessage
319
+ | SASLInitialResponse
320
+ | SASLResponse
321
+
322
+ /**
323
+ * Error produced when bytes cannot be interpreted as a protocol message.
324
+ *
325
+ * @category errors
326
+ * @since 4.0.0
327
+ */
328
+ export class ParseError extends Data.TaggedError("PgProtocolParseError")<{
329
+ readonly message: string
330
+ }> {}
331
+
332
+ /**
333
+ * Error returned when a frontend message cannot be encoded.
334
+ *
335
+ * @category errors
336
+ * @since 4.0.0
337
+ */
338
+ export class EncodeError extends Data.TaggedError("PgProtocolEncodeError")<{
339
+ readonly message: string
340
+ }> {}
341
+
342
+ const textEncoder = new TextEncoder()
343
+ const textDecoder = new TextDecoder("utf-8", { fatal: true })
344
+
345
+ /**
346
+ * Above this length `TextEncoder.encodeInto` beats a per-character loop, below
347
+ * it the call overhead dominates. Measured on V8: the loop runs at about a
348
+ * nanosecond per character and `encodeInto` costs about 50 ns whatever the
349
+ * length, so the crossover is around 50 characters.
350
+ */
351
+ const asciiEncodeLimit = 48
352
+
353
+ /**
354
+ * A view over part of a cached backing store. Slicing runs once per column of
355
+ * every row, and both obvious spellings are slower than this one: `subarray`
356
+ * consults the constructor's `Symbol.species` before it can allocate, and
357
+ * reading `.buffer` off a typed array is an accessor call rather than a field
358
+ * load. Callers hold the store and the array's byte offset instead.
359
+ */
360
+ const view = (store: ArrayBufferLike, offset: number, length: number): Uint8Array =>
361
+ new Uint8Array(store, offset, length)
362
+
363
+ /** Up to this many bytes a copy loop beats `Uint8Array.prototype.set`. */
364
+ const smallCopyLimit = 8
365
+
366
+ /**
367
+ * Writes messages back to back into a pooled buffer and hands out a view of
368
+ * each one, so encoding a message costs no allocation of its own. Bytes below
369
+ * `start` have already been handed out and are never rewritten; when the pool
370
+ * runs out it is replaced rather than reused.
371
+ */
372
+ class Writer {
373
+ readonly poolSize: number
374
+ bytes: Uint8Array
375
+ view: DataView
376
+ /** Where the message currently being written begins. */
377
+ start = 0
378
+ offset = 0
379
+ /** Set by `sqlNull`, read and cleared by the `makeBindEncoder` loop. */
380
+ isNull = false
381
+
382
+ constructor(poolSize: number) {
383
+ this.poolSize = poolSize
384
+ this.bytes = new Uint8Array(poolSize)
385
+ this.view = new DataView(this.bytes.buffer)
386
+ }
387
+
388
+ reserve(size: number): void {
389
+ if (this.offset + size <= this.bytes.length) return
390
+ const pending = this.offset - this.start
391
+ let capacity = this.poolSize
392
+ while (capacity < pending + size) capacity *= 2
393
+ const next = new Uint8Array(capacity)
394
+ next.set(this.bytes.subarray(this.start, this.offset))
395
+ this.bytes = next
396
+ this.view = new DataView(next.buffer)
397
+ this.start = 0
398
+ this.offset = pending
399
+ }
400
+
401
+ /** Starts a message, dropping anything a failed write left behind. */
402
+ begin(): void {
403
+ this.start = this.offset
404
+ }
405
+
406
+ uint8(value: number): void {
407
+ this.reserve(1)
408
+ this.bytes[this.offset++] = value
409
+ }
410
+
411
+ int16(value: number): void {
412
+ this.reserve(2)
413
+ const bytes = this.bytes
414
+ const offset = this.offset
415
+ bytes[offset] = value >>> 8
416
+ bytes[offset + 1] = value
417
+ this.offset = offset + 2
418
+ }
419
+
420
+ int32(value: number): void {
421
+ this.reserve(4)
422
+ this.setInt32(this.offset, value)
423
+ this.offset += 4
424
+ }
425
+
426
+ setInt32(offset: number, value: number): void {
427
+ const bytes = this.bytes
428
+ bytes[offset] = value >>> 24
429
+ bytes[offset + 1] = value >>> 16
430
+ bytes[offset + 2] = value >>> 8
431
+ bytes[offset + 3] = value
432
+ }
433
+
434
+ float32(value: number): void {
435
+ this.reserve(4)
436
+ this.view.setFloat32(this.offset, value)
437
+ this.offset += 4
438
+ }
439
+
440
+ float64(value: number): void {
441
+ this.reserve(8)
442
+ this.view.setFloat64(this.offset, value)
443
+ this.offset += 8
444
+ }
445
+
446
+ bigInt64(value: bigint): void {
447
+ this.reserve(8)
448
+ this.view.setBigInt64(this.offset, value)
449
+ this.offset += 8
450
+ }
451
+
452
+ raw(value: Uint8Array): void {
453
+ const length = value.length
454
+ this.reserve(length)
455
+ const bytes = this.bytes
456
+ const offset = this.offset
457
+ if (length <= smallCopyLimit) {
458
+ for (let index = 0; index < length; index++) bytes[offset + index] = value[index]
459
+ } else {
460
+ bytes.set(value, offset)
461
+ }
462
+ this.offset = offset + length
463
+ }
464
+
465
+ sqlNull(): void {
466
+ this.isNull = true
467
+ }
468
+
469
+ beginLength(): number {
470
+ // Relative to `start`, because a later write may move the message to a new
471
+ // pool buffer, which rebases `start` and `offset` but not the distance
472
+ // between them.
473
+ const token = this.offset - this.start
474
+ this.int32(0)
475
+ return token
476
+ }
477
+
478
+ endLength(token: number): void {
479
+ this.setInt32(this.start + token, this.offset - this.start - token - 4)
480
+ }
481
+
482
+ utf8(value: string, nul = false): void {
483
+ const length = value.length
484
+ if (length <= asciiEncodeLimit) {
485
+ this.reserve(length + (nul ? 1 : 0))
486
+ const bytes = this.bytes
487
+ const start = this.offset
488
+ let i = 0
489
+ for (; i < length; i++) {
490
+ const code = value.charCodeAt(i)
491
+ if (code > 0x7f) break
492
+ bytes[start + i] = code
493
+ }
494
+ if (i === length) {
495
+ const offset = start + length
496
+ if (nul) bytes[offset] = 0
497
+ this.offset = offset + (nul ? 1 : 0)
498
+ return
499
+ }
500
+ }
501
+ // UTF-8 takes at most three bytes per UTF-16 code unit, and four for the
502
+ // two units of a surrogate pair, so this covers any string.
503
+ this.reserve(length * 3 + (nul ? 1 : 0))
504
+ this.offset += textEncoder.encodeInto(value, this.bytes.subarray(this.offset)).written
505
+ if (nul) this.bytes[this.offset++] = 0
506
+ }
507
+
508
+ cString(value: string): void {
509
+ this.utf8(value, true)
510
+ }
511
+
512
+ finish(): Uint8Array {
513
+ const value = view(this.bytes.buffer, this.bytes.byteOffset + this.start, this.offset - this.start)
514
+ if (this.bytes.length > this.poolSize) {
515
+ // An oversized message grew the pool; do not keep the rest of it around.
516
+ this.bytes = new Uint8Array(this.poolSize)
517
+ this.view = new DataView(this.bytes.buffer)
518
+ this.start = 0
519
+ this.offset = 0
520
+ } else {
521
+ this.start = this.offset
522
+ }
523
+ return value
524
+ }
525
+ }
526
+
527
+ const emptyBytes = new Uint8Array(0)
528
+
529
+ /**
530
+ * A cursor over a message payload. The parser reuses one instance pointed at a
531
+ * window of its own buffer, so decoding a message allocates nothing beyond the
532
+ * message itself.
533
+ */
534
+ class Reader {
535
+ bytes: Uint8Array = emptyBytes
536
+ /** The backing store of `bytes` and its offset into it, resolved per message. */
537
+ store: ArrayBufferLike = emptyBytes.buffer
538
+ base = 0
539
+ offset = 0
540
+ limit = 0
541
+
542
+ reset(bytes: Uint8Array, offset: number, limit: number): void {
543
+ this.bytes = bytes
544
+ this.store = bytes.buffer
545
+ this.base = bytes.byteOffset
546
+ this.offset = offset
547
+ this.limit = limit
548
+ }
549
+
550
+ require(size: number): void {
551
+ if (size < 0) {
552
+ throw new ParseError({ message: `Invalid read of ${size} byte(s)` })
553
+ }
554
+ if (this.offset + size > this.limit) {
555
+ throw new ParseError({ message: `Truncated message: expected ${size} more byte(s)` })
556
+ }
557
+ }
558
+
559
+ uint8(): number {
560
+ this.require(1)
561
+ return this.bytes[this.offset++]
562
+ }
563
+
564
+ int16(): number {
565
+ this.require(2)
566
+ const bytes = this.bytes
567
+ const offset = this.offset
568
+ this.offset = offset + 2
569
+ return ((bytes[offset] << 8) | bytes[offset + 1]) << 16 >> 16
570
+ }
571
+
572
+ int32(): number {
573
+ this.require(4)
574
+ const bytes = this.bytes
575
+ const offset = this.offset
576
+ this.offset = offset + 4
577
+ return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]
578
+ }
579
+
580
+ uint32(): number {
581
+ return this.int32() >>> 0
582
+ }
583
+
584
+ raw(size: number): Uint8Array {
585
+ this.require(size)
586
+ const value = view(this.store, this.base + this.offset, size)
587
+ this.offset += size
588
+ return value
589
+ }
590
+
591
+ rest(): Uint8Array {
592
+ return this.raw(this.limit - this.offset)
593
+ }
594
+
595
+ cString(): string {
596
+ const end = this.bytes.indexOf(0, this.offset)
597
+ if (end === -1 || end >= this.limit) {
598
+ throw new ParseError({ message: "Unterminated string" })
599
+ }
600
+ const value = decodeUtf8(this.bytes, this.offset, end - this.offset)
601
+ this.offset = end + 1
602
+ return value
603
+ }
604
+ }
605
+
606
+ /** Below this length a per-character loop beats `TextDecoder.decode`. */
607
+ const asciiDecodeLimit = 10
608
+
609
+ /**
610
+ * Node's own UTF-8 decoder; see the note on its `PgTypes` counterpart. A
611
+ * result containing a replacement character goes to the strict decoder, so
612
+ * invalid bytes still fail exactly as they did.
613
+ */
614
+ const utf8Slice: ((this: Uint8Array, start: number, end: number) => string) | undefined = (globalThis as any).Buffer
615
+ ?.prototype?.utf8Slice
616
+
617
+ const decodeUtf8 = (bytes: Uint8Array, offset: number, size: number): string => {
618
+ if (size <= asciiDecodeLimit) {
619
+ let text = ""
620
+ let index = 0
621
+ for (; index < size; index++) {
622
+ const code = bytes[offset + index]
623
+ if (code > 0x7f) break
624
+ text += String.fromCharCode(code)
625
+ }
626
+ if (index === size) return text
627
+ }
628
+ if (utf8Slice !== undefined) {
629
+ const text = utf8Slice.call(bytes, offset, offset + size)
630
+ if (text.indexOf("\ufffd") === -1) return text
631
+ }
632
+ try {
633
+ return textDecoder.decode(view(bytes.buffer, bytes.byteOffset + offset, size))
634
+ } catch {
635
+ throw new ParseError({ message: "Invalid UTF-8 in message" })
636
+ }
637
+ }
638
+
639
+ const sharedWriter = new Writer(8192)
640
+
641
+ /**
642
+ * Opens a typed message and leaves room for its length. Paired with `end`,
643
+ * which backfills it. A pair rather than a `write` callback, because a
644
+ * callback allocates a closure over the caller's options on every message.
645
+ */
646
+ const begin = (type: number): Writer => {
647
+ const writer = sharedWriter
648
+ writer.begin()
649
+ writer.reserve(5)
650
+ const bytes = writer.bytes
651
+ const offset = writer.offset
652
+ bytes[offset] = type
653
+ writer.offset = offset + 5
654
+ return writer
655
+ }
656
+
657
+ const end = (): Uint8Array => {
658
+ // Relative to `start`, because writing may have moved the message to a new
659
+ // pool buffer. The length counts itself but not the type byte.
660
+ sharedWriter.setInt32(sharedWriter.start + 1, sharedWriter.offset - sharedWriter.start - 1)
661
+ return sharedWriter.finish()
662
+ }
663
+
664
+ const empty = (type: number): Uint8Array => {
665
+ const writer = begin(type)
666
+ writer.setInt32(writer.start + 1, 4)
667
+ return writer.finish()
668
+ }
669
+
670
+ const targetByte = (target: DescribeTarget): number => target === "statement" ? 0x53 : 0x50
671
+
672
+ const requireInt16Count = (count: number, name: string): number => {
673
+ if (count > 0x7fff) throw new EncodeError({ message: `${name} count exceeds 32767: ${count}` })
674
+ return count
675
+ }
676
+
677
+ const encodeResult = <A>(evaluate: () => A): Result.Result<A, EncodeError> => {
678
+ try {
679
+ return Result.succeed(evaluate())
680
+ } catch (error) {
681
+ if (error instanceof EncodeError) return Result.fail(error)
682
+ throw error
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Encodes a `Parse` message, returning `EncodeError` when its parameter count
688
+ * is outside the signed int16 wire range.
689
+ *
690
+ * @category encoding
691
+ * @since 4.0.0
692
+ */
693
+ const encodeParseUnsafe = (options: Omit<Parse, "_tag">): Uint8Array => {
694
+ const writer = begin(0x50)
695
+ writer.cString(options.name)
696
+ writer.cString(options.query)
697
+ const parameterTypes = options.parameterTypes
698
+ const count = requireInt16Count(parameterTypes.length, "Parse parameter type")
699
+ writer.reserve(2 + count * 4)
700
+ const bytes = writer.bytes
701
+ let offset = writer.offset
702
+ bytes[offset] = count >>> 8
703
+ bytes[offset + 1] = count
704
+ offset += 2
705
+ for (let index = 0; index < count; index++) {
706
+ const oid = parameterTypes[index]
707
+ bytes[offset] = oid >>> 24
708
+ bytes[offset + 1] = oid >>> 16
709
+ bytes[offset + 2] = oid >>> 8
710
+ bytes[offset + 3] = oid
711
+ offset += 4
712
+ }
713
+ writer.offset = offset
714
+ return end()
715
+ }
716
+
717
+ export const encodeParse = (options: Omit<Parse, "_tag">): Result.Result<Uint8Array, EncodeError> =>
718
+ encodeResult(() => encodeParseUnsafe(options))
719
+
720
+ /**
721
+ * Encodes a `Bind` message using the binary format code for parameters and
722
+ * results, returning `EncodeError` when its parameter count is outside the
723
+ * signed int16 wire range.
724
+ *
725
+ * @category encoding
726
+ * @since 4.0.0
727
+ */
728
+ const encodeBindUnsafe = (options: Omit<Bind, "_tag">): Uint8Array => {
729
+ const writer = begin(0x42)
730
+ writer.cString(options.portal)
731
+ writer.cString(options.statement)
732
+ const parameters = options.parameters
733
+ const count = requireInt16Count(parameters.length, "Bind parameter")
734
+ // Sizing the rest of the frame up front turns every remaining write into a
735
+ // plain store: one bounds check for the message instead of one per field.
736
+ let size = 10 + count * 4
737
+ for (let index = 0; index < count; index++) {
738
+ const parameter = parameters[index]
739
+ if (parameter !== null) size += parameter.length
740
+ }
741
+ writer.reserve(size)
742
+ const bytes = writer.bytes
743
+ let offset = writer.offset
744
+ // One parameter format code, binary, for every parameter.
745
+ bytes[offset] = 0
746
+ bytes[offset + 1] = 1
747
+ bytes[offset + 2] = 0
748
+ bytes[offset + 3] = 1
749
+ bytes[offset + 4] = count >>> 8
750
+ bytes[offset + 5] = count
751
+ offset += 6
752
+ for (let index = 0; index < count; index++) {
753
+ const parameter = parameters[index]
754
+ if (parameter === null) {
755
+ bytes[offset] = 0xff
756
+ bytes[offset + 1] = 0xff
757
+ bytes[offset + 2] = 0xff
758
+ bytes[offset + 3] = 0xff
759
+ offset += 4
760
+ } else {
761
+ const length = parameter.length
762
+ bytes[offset] = length >>> 24
763
+ bytes[offset + 1] = length >>> 16
764
+ bytes[offset + 2] = length >>> 8
765
+ bytes[offset + 3] = length
766
+ offset += 4
767
+ if (length <= smallCopyLimit) {
768
+ for (let byte = 0; byte < length; byte++) bytes[offset + byte] = parameter[byte]
769
+ } else {
770
+ bytes.set(parameter, offset)
771
+ }
772
+ offset += length
773
+ }
774
+ }
775
+ // One result format code, binary, for every column.
776
+ bytes[offset] = 0
777
+ bytes[offset + 1] = 1
778
+ bytes[offset + 2] = 0
779
+ bytes[offset + 3] = 1
780
+ writer.offset = offset + 4
781
+ return end()
782
+ }
783
+
784
+ export const encodeBind = (options: Omit<Bind, "_tag">): Result.Result<Uint8Array, EncodeError> =>
785
+ encodeResult(() => encodeBindUnsafe(options))
786
+
787
+ /**
788
+ * Where a value writes its wire bytes. `Bind` frames a parameter by leaving
789
+ * room for its length, letting the sink fill in the body, and backfilling the
790
+ * length from what was written, so a value never needs an array of its own.
791
+ *
792
+ * `PgTypes.writeParameter` is the implementation for OID-typed values.
793
+ *
794
+ * @category models
795
+ * @since 4.0.0
796
+ */
797
+ export interface ValueSink {
798
+ readonly uint8: (value: number) => void
799
+ readonly int16: (value: number) => void
800
+ readonly int32: (value: number) => void
801
+ readonly float32: (value: number) => void
802
+ readonly float64: (value: number) => void
803
+ readonly bigInt64: (value: bigint) => void
804
+ readonly raw: (value: Uint8Array) => void
805
+ readonly utf8: (value: string) => void
806
+ /** Writes SQL NULL. The value must write nothing else. */
807
+ readonly sqlNull: () => void
808
+ /**
809
+ * Leaves room for an int32 length and returns a token for `endLength`, so a
810
+ * value that contains other values can frame them without knowing their
811
+ * sizes up front. Tokens nest, and must be closed in the order they were
812
+ * opened.
813
+ */
814
+ readonly beginLength: () => number
815
+ /** Backfills the length of everything written since its `beginLength`. */
816
+ readonly endLength: (token: number) => void
817
+ }
818
+
819
+ /**
820
+ * Builds a `Bind` encoder that writes parameters straight into the frame,
821
+ * skipping the array per parameter that `encodeBind` has to copy from.
822
+ *
823
+ * ```ts
824
+ * import { PgProtocol, PgTypes } from "@effect/sql-pg"
825
+ *
826
+ * const encodeBind = PgProtocol.makeBindEncoder(PgTypes.writeParameter)
827
+ * const frame = encodeBind({ portal: "", statement: "s1", parameters: [PgTypes.int4(1)] })
828
+ * ```
829
+ *
830
+ * @category encoding
831
+ * @since 4.0.0
832
+ */
833
+ const valueWriterUnsafe = Symbol.for("@effect/sql-pg/PgProtocol/ValueWriter/unsafe")
834
+
835
+ export const makeBindEncoder = <A, E = never>(
836
+ writeParameter: (sink: ValueSink, value: A) => Result.Result<void, E>
837
+ ) =>
838
+ (options: {
839
+ readonly portal: string
840
+ readonly statement: string
841
+ readonly parameters: ReadonlyArray<A>
842
+ }): Result.Result<Uint8Array, EncodeError | E> => {
843
+ try {
844
+ const writer = begin(0x42)
845
+ writer.cString(options.portal)
846
+ writer.cString(options.statement)
847
+ const parameters = options.parameters
848
+ const count = requireInt16Count(parameters.length, "Bind parameter")
849
+ writer.reserve(6)
850
+ const header = writer.bytes
851
+ const headerOffset = writer.offset
852
+ header[headerOffset] = 0
853
+ header[headerOffset + 1] = 1
854
+ header[headerOffset + 2] = 0
855
+ header[headerOffset + 3] = 1
856
+ header[headerOffset + 4] = count >>> 8
857
+ header[headerOffset + 5] = count
858
+ writer.offset = headerOffset + 6
859
+ const writeUnsafe = (writeParameter as any)[valueWriterUnsafe] as
860
+ | ((sink: ValueSink, value: A) => void)
861
+ | undefined
862
+ for (let index = 0; index < count; index++) {
863
+ const token = writer.beginLength()
864
+ writer.isNull = false
865
+ if (writeUnsafe === undefined) {
866
+ const written = writeParameter(writer, parameters[index])
867
+ if (Result.isFailure(written)) return Result.fail(written.failure)
868
+ } else {
869
+ writeUnsafe(writer, parameters[index])
870
+ }
871
+ if (writer.isNull) {
872
+ writer.isNull = false
873
+ writer.offset = writer.start + token
874
+ writer.int32(-1)
875
+ } else {
876
+ writer.endLength(token)
877
+ }
878
+ }
879
+ writer.reserve(4)
880
+ const trailer = writer.bytes
881
+ const trailerOffset = writer.offset
882
+ trailer[trailerOffset] = 0
883
+ trailer[trailerOffset + 1] = 1
884
+ trailer[trailerOffset + 2] = 0
885
+ trailer[trailerOffset + 3] = 1
886
+ writer.offset = trailerOffset + 4
887
+ return Result.succeed(end())
888
+ } catch (error) {
889
+ if (error instanceof EncodeError) return Result.fail(error)
890
+ throw error
891
+ }
892
+ }
893
+
894
+ /**
895
+ * Encodes an `Execute` message.
896
+ *
897
+ * @category encoding
898
+ * @since 4.0.0
899
+ */
900
+ export const encodeExecute = (options: Omit<Execute, "_tag">): Uint8Array => {
901
+ const writer = begin(0x45)
902
+ writer.cString(options.portal)
903
+ writer.int32(options.maxRows)
904
+ return end()
905
+ }
906
+
907
+ /**
908
+ * Encodes a `Describe` message.
909
+ *
910
+ * @category encoding
911
+ * @since 4.0.0
912
+ */
913
+ export const encodeDescribe = (options: Omit<Describe, "_tag">): Uint8Array => {
914
+ const writer = begin(0x44)
915
+ writer.uint8(targetByte(options.target))
916
+ writer.cString(options.name)
917
+ return end()
918
+ }
919
+
920
+ /**
921
+ * Encodes a `Close` message.
922
+ *
923
+ * @category encoding
924
+ * @since 4.0.0
925
+ */
926
+ export const encodeClose = (options: Omit<Close, "_tag">): Uint8Array => {
927
+ const writer = begin(0x43)
928
+ writer.uint8(targetByte(options.target))
929
+ writer.cString(options.name)
930
+ return end()
931
+ }
932
+
933
+ /**
934
+ * Encodes a `Sync` message.
935
+ *
936
+ * @category encoding
937
+ * @since 4.0.0
938
+ */
939
+ export const encodeSync = (): Uint8Array => empty(0x53)
940
+
941
+ /**
942
+ * Encodes a `Flush` message.
943
+ *
944
+ * @category encoding
945
+ * @since 4.0.0
946
+ */
947
+ export const encodeFlush = (): Uint8Array => empty(0x48)
948
+
949
+ /**
950
+ * Encodes a `Terminate` message.
951
+ *
952
+ * @category encoding
953
+ * @since 4.0.0
954
+ */
955
+ export const encodeTerminate = (): Uint8Array => empty(0x58)
956
+
957
+ /**
958
+ * Encodes a `PasswordMessage`. The password is sent verbatim, so MD5 hashing
959
+ * belongs to the caller - see `PgAuth.md5Password`.
960
+ *
961
+ * @category encoding
962
+ * @since 4.0.0
963
+ */
964
+ export const encodePasswordMessage = (options: Omit<PasswordMessage, "_tag">): Uint8Array => {
965
+ const writer = begin(0x70)
966
+ writer.cString(options.password)
967
+ return end()
968
+ }
969
+
970
+ /**
971
+ * Encodes a `SASLInitialResponse` message.
972
+ *
973
+ * @category encoding
974
+ * @since 4.0.0
975
+ */
976
+ export const encodeSASLInitialResponse = (options: Omit<SASLInitialResponse, "_tag">): Uint8Array => {
977
+ const writer = begin(0x70)
978
+ writer.cString(options.mechanism)
979
+ if (options.initialResponse === null) {
980
+ writer.int32(-1)
981
+ } else {
982
+ writer.int32(options.initialResponse.length)
983
+ writer.raw(options.initialResponse)
984
+ }
985
+ return end()
986
+ }
987
+
988
+ /**
989
+ * Encodes a `SASLResponse` message.
990
+ *
991
+ * @category encoding
992
+ * @since 4.0.0
993
+ */
994
+ export const encodeSASLResponse = (options: Omit<SASLResponse, "_tag">): Uint8Array => {
995
+ const writer = begin(0x70)
996
+ writer.raw(options.data)
997
+ return end()
998
+ }
999
+
1000
+ /**
1001
+ * Encodes any frontend message.
1002
+ *
1003
+ * @category encoding
1004
+ * @since 4.0.0
1005
+ */
1006
+ export const encode = (message: FrontendMessage): Result.Result<Uint8Array, EncodeError> => {
1007
+ switch (message._tag) {
1008
+ case "Parse":
1009
+ return encodeParse(message)
1010
+ case "Bind":
1011
+ return encodeBind(message)
1012
+ case "Execute":
1013
+ return Result.succeed(encodeExecute(message))
1014
+ case "Describe":
1015
+ return Result.succeed(encodeDescribe(message))
1016
+ case "Close":
1017
+ return Result.succeed(encodeClose(message))
1018
+ case "Sync":
1019
+ return Result.succeed(encodeSync())
1020
+ case "Flush":
1021
+ return Result.succeed(encodeFlush())
1022
+ case "Terminate":
1023
+ return Result.succeed(encodeTerminate())
1024
+ case "PasswordMessage":
1025
+ return Result.succeed(encodePasswordMessage(message))
1026
+ case "SASLInitialResponse":
1027
+ return Result.succeed(encodeSASLInitialResponse(message))
1028
+ case "SASLResponse":
1029
+ return Result.succeed(encodeSASLResponse(message))
1030
+ }
1031
+ }
1032
+
1033
+ // -----------------------------------------------------------------------------
1034
+ // special messages
1035
+ // -----------------------------------------------------------------------------
1036
+
1037
+ const PROTOCOL_VERSION_3_0 = 196608
1038
+ const SSL_REQUEST_CODE = 80877103
1039
+ const CANCEL_REQUEST_CODE = 80877102
1040
+
1041
+ /**
1042
+ * Startup parameters. `user` is required; any other run-time parameter the
1043
+ * server accepts may be passed alongside it.
1044
+ *
1045
+ * @category models
1046
+ * @since 4.0.0
1047
+ */
1048
+ export interface StartupParameters {
1049
+ readonly [key: string]: string | undefined
1050
+ readonly user: string
1051
+ readonly database?: string | undefined
1052
+ readonly application_name?: string | undefined
1053
+ }
1054
+
1055
+ /**
1056
+ * Encodes an `SSLRequest`. It has no type byte and is only valid before
1057
+ * startup.
1058
+ *
1059
+ * @category encoding
1060
+ * @since 4.0.0
1061
+ */
1062
+ export const encodeSslRequest = (): Uint8Array => {
1063
+ sharedWriter.begin()
1064
+ sharedWriter.int32(8)
1065
+ sharedWriter.int32(SSL_REQUEST_CODE)
1066
+ return sharedWriter.finish()
1067
+ }
1068
+
1069
+ /**
1070
+ * Decodes the single byte the server sends in reply to an `SSLRequest`. `"S"`
1071
+ * means the server will speak TLS, `"N"` means it will not.
1072
+ *
1073
+ * @category decoding
1074
+ * @since 4.0.0
1075
+ */
1076
+ export const decodeSslResponse = (byte: number): Result.Result<"S" | "N", ParseError> =>
1077
+ byte === 0x53
1078
+ ? Result.succeed("S")
1079
+ : byte === 0x4e
1080
+ ? Result.succeed("N")
1081
+ : Result.fail(new ParseError({ message: `Invalid SSLRequest response byte: ${byte}` }))
1082
+
1083
+ /**
1084
+ * Encodes a `StartupMessage` for protocol 3.0. It has no type byte.
1085
+ * `client_encoding` defaults to `UTF8` because this codec always writes UTF-8.
1086
+ *
1087
+ * @category encoding
1088
+ * @since 4.0.0
1089
+ */
1090
+ export const encodeStartupMessage = (parameters: StartupParameters): Uint8Array => {
1091
+ const writer = sharedWriter
1092
+ writer.begin()
1093
+ writer.int32(0)
1094
+ writer.int32(PROTOCOL_VERSION_3_0)
1095
+ for (const [key, value] of Object.entries(parameters)) {
1096
+ if (value === undefined) continue
1097
+ writer.cString(key)
1098
+ writer.cString(value)
1099
+ }
1100
+ if (parameters.client_encoding === undefined) {
1101
+ writer.cString("client_encoding")
1102
+ writer.cString("UTF8")
1103
+ }
1104
+ writer.uint8(0)
1105
+ writer.setInt32(writer.start, writer.offset - writer.start)
1106
+ return writer.finish()
1107
+ }
1108
+
1109
+ /**
1110
+ * Encodes a `CancelRequest`. It has no type byte and is sent on a separate
1111
+ * connection, using the `pid` and `secret` from `BackendKeyData`.
1112
+ *
1113
+ * @category encoding
1114
+ * @since 4.0.0
1115
+ */
1116
+ export const encodeCancelRequest = (options: {
1117
+ readonly pid: number
1118
+ readonly secret: number
1119
+ }): Uint8Array => {
1120
+ const writer = sharedWriter
1121
+ writer.begin()
1122
+ writer.int32(16)
1123
+ writer.int32(CANCEL_REQUEST_CODE)
1124
+ writer.int32(options.pid)
1125
+ writer.int32(options.secret)
1126
+ return writer.finish()
1127
+ }
1128
+
1129
+ // -----------------------------------------------------------------------------
1130
+ // backend messages
1131
+ // -----------------------------------------------------------------------------
1132
+
1133
+ /**
1134
+ * Authentication succeeded.
1135
+ *
1136
+ * @category models
1137
+ * @since 4.0.0
1138
+ */
1139
+ export interface AuthenticationOk {
1140
+ readonly _tag: "AuthenticationOk"
1141
+ }
1142
+
1143
+ /**
1144
+ * The server wants the password in the clear.
1145
+ *
1146
+ * @category models
1147
+ * @since 4.0.0
1148
+ */
1149
+ export interface AuthenticationCleartextPassword {
1150
+ readonly _tag: "AuthenticationCleartextPassword"
1151
+ }
1152
+
1153
+ /**
1154
+ * The server wants an MD5-hashed password, salted with these four bytes.
1155
+ *
1156
+ * @category models
1157
+ * @since 4.0.0
1158
+ */
1159
+ export interface AuthenticationMD5Password {
1160
+ readonly _tag: "AuthenticationMD5Password"
1161
+ readonly salt: Uint8Array
1162
+ }
1163
+
1164
+ /**
1165
+ * The server offers these SASL mechanisms.
1166
+ *
1167
+ * @category models
1168
+ * @since 4.0.0
1169
+ */
1170
+ export interface AuthenticationSASL {
1171
+ readonly _tag: "AuthenticationSASL"
1172
+ readonly mechanisms: ReadonlyArray<string>
1173
+ }
1174
+
1175
+ /**
1176
+ * An opaque SASL challenge.
1177
+ *
1178
+ * @category models
1179
+ * @since 4.0.0
1180
+ */
1181
+ export interface AuthenticationSASLContinue {
1182
+ readonly _tag: "AuthenticationSASLContinue"
1183
+ readonly data: Uint8Array
1184
+ }
1185
+
1186
+ /**
1187
+ * The opaque final SASL payload, carrying the server signature.
1188
+ *
1189
+ * @category models
1190
+ * @since 4.0.0
1191
+ */
1192
+ export interface AuthenticationSASLFinal {
1193
+ readonly _tag: "AuthenticationSASLFinal"
1194
+ readonly data: Uint8Array
1195
+ }
1196
+
1197
+ /**
1198
+ * An authentication request this codec does not model, such as GSSAPI or
1199
+ * SSPI. The `method` is the raw sub-type integer.
1200
+ *
1201
+ * @category models
1202
+ * @since 4.0.0
1203
+ */
1204
+ export interface AuthenticationUnsupported {
1205
+ readonly _tag: "AuthenticationUnsupported"
1206
+ readonly method: number
1207
+ readonly payload: Uint8Array
1208
+ }
1209
+
1210
+ /**
1211
+ * Reports a run-time parameter value, at startup or whenever it changes.
1212
+ *
1213
+ * @category models
1214
+ * @since 4.0.0
1215
+ */
1216
+ export interface ParameterStatus {
1217
+ readonly _tag: "ParameterStatus"
1218
+ readonly name: string
1219
+ readonly value: string
1220
+ }
1221
+
1222
+ /**
1223
+ * The identity a `CancelRequest` needs.
1224
+ *
1225
+ * @category models
1226
+ * @since 4.0.0
1227
+ */
1228
+ export interface BackendKeyData {
1229
+ readonly _tag: "BackendKeyData"
1230
+ readonly pid: number
1231
+ readonly secret: number
1232
+ }
1233
+
1234
+ /**
1235
+ * Transaction status: idle, in a transaction block, or in a failed
1236
+ * transaction block.
1237
+ *
1238
+ * @category models
1239
+ * @since 4.0.0
1240
+ */
1241
+ export type TransactionStatus = "I" | "T" | "E"
1242
+
1243
+ /**
1244
+ * The backend is ready for a new query cycle.
1245
+ *
1246
+ * @category models
1247
+ * @since 4.0.0
1248
+ */
1249
+ export interface ReadyForQuery {
1250
+ readonly _tag: "ReadyForQuery"
1251
+ readonly status: TransactionStatus
1252
+ }
1253
+
1254
+ /**
1255
+ * One column of a `RowDescription`.
1256
+ *
1257
+ * @category models
1258
+ * @since 4.0.0
1259
+ */
1260
+ export interface FieldDescription {
1261
+ readonly name: string
1262
+ readonly tableOid: number
1263
+ readonly columnAttributeNumber: number
1264
+ readonly dataTypeOid: number
1265
+ readonly dataTypeSize: number
1266
+ readonly typeModifier: number
1267
+ readonly format: number
1268
+ }
1269
+
1270
+ /**
1271
+ * Describes the columns a portal will return.
1272
+ *
1273
+ * @category models
1274
+ * @since 4.0.0
1275
+ */
1276
+ export interface RowDescription {
1277
+ readonly _tag: "RowDescription"
1278
+ readonly fields: ReadonlyArray<FieldDescription>
1279
+ }
1280
+
1281
+ /**
1282
+ * One result row. Values stay raw bytes; `null` is SQL NULL. Decoding them
1283
+ * requires the OIDs from the matching `RowDescription`.
1284
+ *
1285
+ * @category models
1286
+ * @since 4.0.0
1287
+ */
1288
+ export interface DataRow<out A = Uint8Array | null> {
1289
+ readonly _tag: "DataRow"
1290
+ readonly values: ReadonlyArray<A>
1291
+ }
1292
+
1293
+ /**
1294
+ * Reads one `DataRow` field out of the parser's buffer, for a parser given a
1295
+ * `readField`. `size` is -1 for SQL NULL, and `column` is the field's position
1296
+ * in the row.
1297
+ *
1298
+ * The bytes are the parser's buffer rather than a view of the field, so they
1299
+ * are only the field's for `offset` to `offset + size`, and reading outside
1300
+ * that reads the rest of the stream. `PgTypes.makeFieldReader` is the
1301
+ * implementation for OID-typed columns.
1302
+ *
1303
+ * A reader runs inside the stateful parser. If it throws, that failure is
1304
+ * terminal just like a `ParseError`.
1305
+ *
1306
+ * @category models
1307
+ * @since 4.0.0
1308
+ */
1309
+ export type FieldReader<A> = (bytes: Uint8Array, offset: number, size: number, column: number) => A
1310
+
1311
+ /**
1312
+ * A command finished, reporting its tag such as `SELECT 3`.
1313
+ *
1314
+ * @category models
1315
+ * @since 4.0.0
1316
+ */
1317
+ export interface CommandComplete {
1318
+ readonly _tag: "CommandComplete"
1319
+ readonly commandTag: string
1320
+ }
1321
+
1322
+ /**
1323
+ * The query string was empty.
1324
+ *
1325
+ * @category models
1326
+ * @since 4.0.0
1327
+ */
1328
+ export interface EmptyQueryResponse {
1329
+ readonly _tag: "EmptyQueryResponse"
1330
+ }
1331
+
1332
+ /**
1333
+ * The statement or portal returns no rows.
1334
+ *
1335
+ * @category models
1336
+ * @since 4.0.0
1337
+ */
1338
+ export interface NoData {
1339
+ readonly _tag: "NoData"
1340
+ }
1341
+
1342
+ /**
1343
+ * A `Parse` succeeded.
1344
+ *
1345
+ * @category models
1346
+ * @since 4.0.0
1347
+ */
1348
+ export interface ParseComplete {
1349
+ readonly _tag: "ParseComplete"
1350
+ }
1351
+
1352
+ /**
1353
+ * A `Bind` succeeded.
1354
+ *
1355
+ * @category models
1356
+ * @since 4.0.0
1357
+ */
1358
+ export interface BindComplete {
1359
+ readonly _tag: "BindComplete"
1360
+ }
1361
+
1362
+ /**
1363
+ * A `Close` succeeded.
1364
+ *
1365
+ * @category models
1366
+ * @since 4.0.0
1367
+ */
1368
+ export interface CloseComplete {
1369
+ readonly _tag: "CloseComplete"
1370
+ }
1371
+
1372
+ /**
1373
+ * An `Execute` stopped at its row limit; the portal can be executed again.
1374
+ *
1375
+ * @category models
1376
+ * @since 4.0.0
1377
+ */
1378
+ export interface PortalSuspended {
1379
+ readonly _tag: "PortalSuspended"
1380
+ }
1381
+
1382
+ /**
1383
+ * The parameter OIDs of a described statement.
1384
+ *
1385
+ * @category models
1386
+ * @since 4.0.0
1387
+ */
1388
+ export interface ParameterDescription {
1389
+ readonly _tag: "ParameterDescription"
1390
+ readonly parameterTypes: ReadonlyArray<number>
1391
+ }
1392
+
1393
+ /**
1394
+ * The fields of an `ErrorResponse` or `NoticeResponse`. Unrecognised field
1395
+ * codes are kept under their raw single-character key.
1396
+ *
1397
+ * @category models
1398
+ * @since 4.0.0
1399
+ */
1400
+ export interface ErrorFields {
1401
+ readonly [key: string]: string | undefined
1402
+ readonly severity?: string | undefined
1403
+ readonly severityUnlocalized?: string | undefined
1404
+ readonly code?: string | undefined
1405
+ readonly message?: string | undefined
1406
+ readonly detail?: string | undefined
1407
+ readonly hint?: string | undefined
1408
+ readonly position?: string | undefined
1409
+ readonly internalPosition?: string | undefined
1410
+ readonly internalQuery?: string | undefined
1411
+ readonly where?: string | undefined
1412
+ readonly schema?: string | undefined
1413
+ readonly table?: string | undefined
1414
+ readonly column?: string | undefined
1415
+ readonly dataType?: string | undefined
1416
+ readonly constraint?: string | undefined
1417
+ readonly file?: string | undefined
1418
+ readonly line?: string | undefined
1419
+ readonly routine?: string | undefined
1420
+ }
1421
+
1422
+ /**
1423
+ * An error. `code` is the SQLSTATE.
1424
+ *
1425
+ * @category models
1426
+ * @since 4.0.0
1427
+ */
1428
+ export interface ErrorResponse {
1429
+ readonly _tag: "ErrorResponse"
1430
+ readonly fields: ErrorFields
1431
+ }
1432
+
1433
+ /**
1434
+ * A warning or notice. Same field set as `ErrorResponse`.
1435
+ *
1436
+ * @category models
1437
+ * @since 4.0.0
1438
+ */
1439
+ export interface NoticeResponse {
1440
+ readonly _tag: "NoticeResponse"
1441
+ readonly fields: ErrorFields
1442
+ }
1443
+
1444
+ /**
1445
+ * A `LISTEN`/`NOTIFY` message.
1446
+ *
1447
+ * @category models
1448
+ * @since 4.0.0
1449
+ */
1450
+ export interface NotificationResponse {
1451
+ readonly _tag: "NotificationResponse"
1452
+ readonly pid: number
1453
+ readonly channel: string
1454
+ readonly payload: string
1455
+ }
1456
+
1457
+ /**
1458
+ * The server speaks an older minor protocol version, or did not recognise
1459
+ * some startup options.
1460
+ *
1461
+ * @category models
1462
+ * @since 4.0.0
1463
+ */
1464
+ export interface NegotiateProtocolVersion {
1465
+ readonly _tag: "NegotiateProtocolVersion"
1466
+ readonly minorVersion: number
1467
+ readonly unrecognizedOptions: ReadonlyArray<string>
1468
+ }
1469
+
1470
+ /**
1471
+ * The server is ready to receive `COPY` data.
1472
+ *
1473
+ * @category models
1474
+ * @since 4.0.0
1475
+ */
1476
+ export interface CopyInResponse {
1477
+ readonly _tag: "CopyInResponse"
1478
+ readonly format: number
1479
+ readonly columnFormats: ReadonlyArray<number>
1480
+ }
1481
+
1482
+ /**
1483
+ * The server is about to send `COPY` data.
1484
+ *
1485
+ * @category models
1486
+ * @since 4.0.0
1487
+ */
1488
+ export interface CopyOutResponse {
1489
+ readonly _tag: "CopyOutResponse"
1490
+ readonly format: number
1491
+ readonly columnFormats: ReadonlyArray<number>
1492
+ }
1493
+
1494
+ /**
1495
+ * The connection entered bidirectional `COPY` mode, as used by replication.
1496
+ *
1497
+ * @category models
1498
+ * @since 4.0.0
1499
+ */
1500
+ export interface CopyBothResponse {
1501
+ readonly _tag: "CopyBothResponse"
1502
+ readonly format: number
1503
+ readonly columnFormats: ReadonlyArray<number>
1504
+ }
1505
+
1506
+ /**
1507
+ * A chunk of `COPY` data.
1508
+ *
1509
+ * @category models
1510
+ * @since 4.0.0
1511
+ */
1512
+ export interface CopyData {
1513
+ readonly _tag: "CopyData"
1514
+ readonly data: Uint8Array
1515
+ }
1516
+
1517
+ /**
1518
+ * The `COPY` stream ended.
1519
+ *
1520
+ * @category models
1521
+ * @since 4.0.0
1522
+ */
1523
+ export interface CopyDone {
1524
+ readonly _tag: "CopyDone"
1525
+ }
1526
+
1527
+ /**
1528
+ * A message whose type byte this codec does not know. The payload excludes
1529
+ * the type byte and the length prefix.
1530
+ *
1531
+ * @category models
1532
+ * @since 4.0.0
1533
+ */
1534
+ export interface Unknown {
1535
+ readonly _tag: "Unknown"
1536
+ readonly type: number
1537
+ readonly payload: Uint8Array
1538
+ }
1539
+
1540
+ /**
1541
+ * Any message the server sends after startup.
1542
+ *
1543
+ * @category models
1544
+ * @since 4.0.0
1545
+ */
1546
+ export type BackendMessage<A = Uint8Array | null> =
1547
+ | AuthenticationOk
1548
+ | AuthenticationCleartextPassword
1549
+ | AuthenticationMD5Password
1550
+ | AuthenticationSASL
1551
+ | AuthenticationSASLContinue
1552
+ | AuthenticationSASLFinal
1553
+ | AuthenticationUnsupported
1554
+ | ParameterStatus
1555
+ | BackendKeyData
1556
+ | ReadyForQuery
1557
+ | RowDescription
1558
+ | DataRow<A>
1559
+ | CommandComplete
1560
+ | EmptyQueryResponse
1561
+ | NoData
1562
+ | ParseComplete
1563
+ | BindComplete
1564
+ | CloseComplete
1565
+ | PortalSuspended
1566
+ | ParameterDescription
1567
+ | ErrorResponse
1568
+ | NoticeResponse
1569
+ | NotificationResponse
1570
+ | NegotiateProtocolVersion
1571
+ | CopyInResponse
1572
+ | CopyOutResponse
1573
+ | CopyBothResponse
1574
+ | CopyData
1575
+ | CopyDone
1576
+ | Unknown
1577
+
1578
+ const BackendType = {
1579
+ NotificationResponse: 0x41, // A
1580
+ CommandComplete: 0x43, // C
1581
+ DataRow: 0x44, // D
1582
+ ErrorResponse: 0x45, // E
1583
+ CopyInResponse: 0x47, // G
1584
+ CopyOutResponse: 0x48, // H
1585
+ EmptyQueryResponse: 0x49, // I
1586
+ BackendKeyData: 0x4b, // K
1587
+ NoticeResponse: 0x4e, // N
1588
+ Authentication: 0x52, // R
1589
+ ParameterStatus: 0x53, // S
1590
+ RowDescription: 0x54, // T
1591
+ CopyBothResponse: 0x57, // W
1592
+ ReadyForQuery: 0x5a, // Z
1593
+ CopyDone: 0x63, // c
1594
+ CopyData: 0x64, // d
1595
+ NoData: 0x6e, // n
1596
+ PortalSuspended: 0x73, // s
1597
+ ParameterDescription: 0x74, // t
1598
+ NegotiateProtocolVersion: 0x76, // v
1599
+ ParseComplete: 0x31, // 1
1600
+ BindComplete: 0x32, // 2
1601
+ CloseComplete: 0x33 // 3
1602
+ } as const
1603
+
1604
+ const errorFieldNames: Record<string, string> = {
1605
+ S: "severity",
1606
+ V: "severityUnlocalized",
1607
+ C: "code",
1608
+ M: "message",
1609
+ D: "detail",
1610
+ H: "hint",
1611
+ P: "position",
1612
+ p: "internalPosition",
1613
+ q: "internalQuery",
1614
+ W: "where",
1615
+ s: "schema",
1616
+ t: "table",
1617
+ c: "column",
1618
+ d: "dataType",
1619
+ n: "constraint",
1620
+ F: "file",
1621
+ L: "line",
1622
+ R: "routine"
1623
+ }
1624
+
1625
+ const decodeAuthentication = (reader: Reader): BackendMessage => {
1626
+ const method = reader.int32()
1627
+ switch (method) {
1628
+ case 0:
1629
+ return { _tag: "AuthenticationOk" }
1630
+ case 3:
1631
+ return { _tag: "AuthenticationCleartextPassword" }
1632
+ case 5:
1633
+ return { _tag: "AuthenticationMD5Password", salt: reader.raw(4) }
1634
+ case 10: {
1635
+ const mechanisms: Array<string> = []
1636
+ for (;;) {
1637
+ const mechanism = reader.cString()
1638
+ if (mechanism === "") break
1639
+ mechanisms.push(mechanism)
1640
+ }
1641
+ return { _tag: "AuthenticationSASL", mechanisms }
1642
+ }
1643
+ case 11:
1644
+ return { _tag: "AuthenticationSASLContinue", data: reader.rest() }
1645
+ case 12:
1646
+ return { _tag: "AuthenticationSASLFinal", data: reader.rest() }
1647
+ default:
1648
+ return { _tag: "AuthenticationUnsupported", method, payload: reader.rest() }
1649
+ }
1650
+ }
1651
+
1652
+ const decodeErrorFields = (reader: Reader): ErrorFields => {
1653
+ const fields: Record<string, string> = {}
1654
+ for (;;) {
1655
+ const code = reader.uint8()
1656
+ if (code === 0) break
1657
+ const key = String.fromCharCode(code)
1658
+ fields[errorFieldNames[key] ?? key] = reader.cString()
1659
+ }
1660
+ return fields
1661
+ }
1662
+
1663
+ const requireNonNegativeCount = (count: number, name: string): number => {
1664
+ if (count < 0) {
1665
+ throw new ParseError({ message: `Invalid ${name} count: ${count}` })
1666
+ }
1667
+ return count
1668
+ }
1669
+
1670
+ const decodeCopyResponse = (
1671
+ reader: Reader
1672
+ ): { readonly format: number; readonly columnFormats: ReadonlyArray<number> } => {
1673
+ const format = reader.uint8()
1674
+ const count = requireNonNegativeCount(reader.int16(), "COPY column")
1675
+ const columnFormats: Array<number> = new Array(count)
1676
+ for (let i = 0; i < count; i++) {
1677
+ columnFormats[i] = reader.int16()
1678
+ }
1679
+ return { format, columnFormats }
1680
+ }
1681
+
1682
+ // The one message that arrives per result row, so it reads the frame directly
1683
+ // instead of going through `Reader`. Callers have already checked that the
1684
+ // whole frame is buffered, which turns every field read into one bounds check
1685
+ // against `limit`.
1686
+ const decodeDataRow = <A>(
1687
+ bytes: Uint8Array,
1688
+ store: ArrayBufferLike,
1689
+ base: number,
1690
+ offset: number,
1691
+ limit: number,
1692
+ readField: FieldReader<A> | undefined
1693
+ ): DataRow<A> => {
1694
+ if (offset + 2 > limit) {
1695
+ throw new ParseError({ message: "Truncated message: expected 2 more byte(s)" })
1696
+ }
1697
+ const count = ((bytes[offset] << 8) | bytes[offset + 1]) << 16 >> 16
1698
+ if (count < 0) {
1699
+ throw new ParseError({ message: `Invalid DataRow field count: ${count}` })
1700
+ }
1701
+ const values: Array<any> = new Array(count)
1702
+ let position = offset + 2
1703
+ for (let i = 0; i < count; i++) {
1704
+ if (position + 4 > limit) {
1705
+ throw new ParseError({ message: "Truncated message: expected 4 more byte(s)" })
1706
+ }
1707
+ const size = (bytes[position] << 24) | (bytes[position + 1] << 16) | (bytes[position + 2] << 8) |
1708
+ bytes[position + 3]
1709
+ position += 4
1710
+ if (size < 0) {
1711
+ if (size < -1) {
1712
+ throw new ParseError({ message: `Invalid DataRow field length: ${size}` })
1713
+ }
1714
+ values[i] = readField === undefined ? null : readField(bytes, position, -1, i)
1715
+ continue
1716
+ }
1717
+ const next = position + size
1718
+ if (next > limit) {
1719
+ throw new ParseError({ message: `Truncated message: expected ${size} more byte(s)` })
1720
+ }
1721
+ values[i] = readField === undefined
1722
+ ? view(store, base + position, size)
1723
+ : readField(bytes, position, size, i)
1724
+ position = next
1725
+ }
1726
+ if (position !== limit) {
1727
+ throw new ParseError({ message: `DataRow has ${limit - position} trailing byte(s)` })
1728
+ }
1729
+ return { _tag: "DataRow", values }
1730
+ }
1731
+
1732
+ const decodeBackend = (type: number, reader: Reader): BackendMessage => {
1733
+ switch (type) {
1734
+ case BackendType.Authentication:
1735
+ return decodeAuthentication(reader)
1736
+ case BackendType.ParameterStatus:
1737
+ return { _tag: "ParameterStatus", name: reader.cString(), value: reader.cString() }
1738
+ case BackendType.BackendKeyData:
1739
+ return { _tag: "BackendKeyData", pid: reader.int32(), secret: reader.int32() }
1740
+ case BackendType.ReadyForQuery: {
1741
+ const status = String.fromCharCode(reader.uint8())
1742
+ if (status !== "I" && status !== "T" && status !== "E") {
1743
+ throw new ParseError({ message: `Invalid ReadyForQuery status: ${status}` })
1744
+ }
1745
+ return { _tag: "ReadyForQuery", status }
1746
+ }
1747
+ case BackendType.RowDescription: {
1748
+ const count = requireNonNegativeCount(reader.int16(), "RowDescription field")
1749
+ const fields: Array<FieldDescription> = new Array(count)
1750
+ for (let i = 0; i < count; i++) {
1751
+ fields[i] = {
1752
+ name: reader.cString(),
1753
+ tableOid: reader.uint32(),
1754
+ columnAttributeNumber: reader.int16(),
1755
+ dataTypeOid: reader.uint32(),
1756
+ dataTypeSize: reader.int16(),
1757
+ typeModifier: reader.int32(),
1758
+ format: reader.int16()
1759
+ }
1760
+ }
1761
+ return { _tag: "RowDescription", fields }
1762
+ }
1763
+ case BackendType.DataRow:
1764
+ return decodeDataRow(reader.bytes, reader.store, reader.base, reader.offset, reader.limit, undefined)
1765
+ case BackendType.CommandComplete:
1766
+ return { _tag: "CommandComplete", commandTag: reader.cString() }
1767
+ case BackendType.EmptyQueryResponse:
1768
+ return { _tag: "EmptyQueryResponse" }
1769
+ case BackendType.NoData:
1770
+ return { _tag: "NoData" }
1771
+ case BackendType.ParseComplete:
1772
+ return { _tag: "ParseComplete" }
1773
+ case BackendType.BindComplete:
1774
+ return { _tag: "BindComplete" }
1775
+ case BackendType.CloseComplete:
1776
+ return { _tag: "CloseComplete" }
1777
+ case BackendType.PortalSuspended:
1778
+ return { _tag: "PortalSuspended" }
1779
+ case BackendType.ParameterDescription: {
1780
+ const count = requireNonNegativeCount(reader.int16(), "ParameterDescription parameter")
1781
+ const parameterTypes: Array<number> = new Array(count)
1782
+ for (let i = 0; i < count; i++) {
1783
+ parameterTypes[i] = reader.uint32()
1784
+ }
1785
+ return { _tag: "ParameterDescription", parameterTypes }
1786
+ }
1787
+ case BackendType.ErrorResponse:
1788
+ return { _tag: "ErrorResponse", fields: decodeErrorFields(reader) }
1789
+ case BackendType.NoticeResponse:
1790
+ return { _tag: "NoticeResponse", fields: decodeErrorFields(reader) }
1791
+ case BackendType.NotificationResponse:
1792
+ return {
1793
+ _tag: "NotificationResponse",
1794
+ pid: reader.int32(),
1795
+ channel: reader.cString(),
1796
+ payload: reader.cString()
1797
+ }
1798
+ case BackendType.NegotiateProtocolVersion: {
1799
+ const minorVersion = reader.int32()
1800
+ const count = requireNonNegativeCount(reader.int32(), "NegotiateProtocolVersion option")
1801
+ const unrecognizedOptions: Array<string> = new Array(count)
1802
+ for (let i = 0; i < count; i++) {
1803
+ unrecognizedOptions[i] = reader.cString()
1804
+ }
1805
+ return { _tag: "NegotiateProtocolVersion", minorVersion, unrecognizedOptions }
1806
+ }
1807
+ case BackendType.CopyInResponse:
1808
+ return { _tag: "CopyInResponse", ...decodeCopyResponse(reader) }
1809
+ case BackendType.CopyOutResponse:
1810
+ return { _tag: "CopyOutResponse", ...decodeCopyResponse(reader) }
1811
+ case BackendType.CopyBothResponse:
1812
+ return { _tag: "CopyBothResponse", ...decodeCopyResponse(reader) }
1813
+ case BackendType.CopyData:
1814
+ return { _tag: "CopyData", data: reader.rest() }
1815
+ case BackendType.CopyDone:
1816
+ return { _tag: "CopyDone" }
1817
+ default:
1818
+ return { _tag: "Unknown", type, payload: reader.rest() }
1819
+ }
1820
+ }