@effect/sql-pg 4.0.0-rc.111 → 4.0.0-rc.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai-docs/package.json +1 -1
- package/dist/PgAuth.d.ts +69 -0
- package/dist/PgAuth.d.ts.map +1 -0
- package/dist/PgAuth.js +202 -0
- package/dist/PgAuth.js.map +1 -0
- package/dist/PgProtocol.d.ts +750 -0
- package/dist/PgProtocol.d.ts.map +1 -0
- package/dist/PgProtocol.js +1160 -0
- package/dist/PgProtocol.js.map +1 -0
- package/dist/PgTypes.d.ts +361 -0
- package/dist/PgTypes.d.ts.map +1 -0
- package/dist/PgTypes.js +1514 -0
- package/dist/PgTypes.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
- package/src/PgAuth.ts +287 -0
- package/src/PgProtocol.ts +1820 -0
- package/src/PgTypes.ts +1930 -0
- package/src/index.ts +15 -0
|
@@ -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
|
+
}
|