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