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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/PgTypes.ts ADDED
@@ -0,0 +1,1930 @@
1
+ /**
2
+ * Binary codecs for PostgreSQL values, keyed by type OID.
3
+ *
4
+ * Version 1 implements the binary wire format (`format = 1`) only; passing
5
+ * `format = 0` to `decode` is an error. Layouts follow rust-postgres'
6
+ * `postgres-types`, including the infinity sentinels, and assume the server
7
+ * was built with `integer_datetimes` (the only supported configuration since
8
+ * PostgreSQL 10).
9
+ *
10
+ * There is no `typeof` inference: an OID is always supplied, either directly
11
+ * or through a constructor such as `int4` that carries it.
12
+ *
13
+ * `timestamp` has no time zone on the wire and is treated as UTC in both
14
+ * directions. Decoding drops sub-millisecond precision by truncating toward
15
+ * zero, including for timestamps before the PostgreSQL epoch.
16
+ *
17
+ * @since 4.0.0
18
+ */
19
+ import * as Data from "effect/Data"
20
+ import * as Result from "effect/Result"
21
+ import type * as PgProtocol from "./PgProtocol.ts"
22
+ import type { ValueSink } from "./PgProtocol.ts"
23
+
24
+ /**
25
+ * Failure returned when a value cannot be encoded or decoded for its OID.
26
+ *
27
+ * @category errors
28
+ * @since 4.0.0
29
+ */
30
+ export class CodecError extends Data.TaggedError("PgTypesCodecError")<{
31
+ readonly message: string
32
+ }> {}
33
+
34
+ const fail = (message: string): never => {
35
+ throw new CodecError({ message })
36
+ }
37
+
38
+ const result = <A>(evaluate: () => A): Result.Result<A, CodecError> => {
39
+ try {
40
+ return Result.succeed(evaluate())
41
+ } catch (error) {
42
+ if (error instanceof CodecError) return Result.fail(error)
43
+ throw error
44
+ }
45
+ }
46
+
47
+ const textEncoder = new TextEncoder()
48
+ const textDecoder = new TextDecoder("utf-8", { fatal: true })
49
+
50
+ const writeInt16 = (bytes: Uint8Array, offset: number, value: number): void => {
51
+ bytes[offset] = value >>> 8
52
+ bytes[offset + 1] = value
53
+ }
54
+
55
+ const readInt16 = (bytes: Uint8Array, offset: number): number => ((bytes[offset] << 8) | bytes[offset + 1]) << 16 >> 16
56
+
57
+ const readUint16 = (bytes: Uint8Array, offset: number): number => (bytes[offset] << 8) | bytes[offset + 1]
58
+
59
+ const writeInt32 = (bytes: Uint8Array, offset: number, value: number): void => {
60
+ bytes[offset] = value >>> 24
61
+ bytes[offset + 1] = value >>> 16
62
+ bytes[offset + 2] = value >>> 8
63
+ bytes[offset + 3] = value
64
+ }
65
+
66
+ const readInt32 = (bytes: Uint8Array, offset: number): number =>
67
+ (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]
68
+
69
+ /** Writes an int64, given a value the caller has checked fits a double exactly. */
70
+ const writeInt64 = (bytes: Uint8Array, offset: number, value: number): void => {
71
+ const high = Math.floor(value / 4294967296)
72
+ writeInt32(bytes, offset, high)
73
+ writeInt32(bytes, offset + 4, value - high * 4294967296)
74
+ }
75
+
76
+ const readUint32 = (bytes: Uint8Array, offset: number): number =>
77
+ ((bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]) >>> 0
78
+
79
+ /**
80
+ * Constructing a `DataView` costs more than the reads and writes it performs,
81
+ * so the fixed-width codecs stage values through these instead of wrapping
82
+ * every freshly allocated array.
83
+ */
84
+ const scratch4 = new ArrayBuffer(4)
85
+ const scratchView4 = new DataView(scratch4)
86
+ const scratchBytes4 = new Uint8Array(scratch4)
87
+ const scratch8 = new ArrayBuffer(8)
88
+ const scratchView8 = new DataView(scratch8)
89
+ const scratchBytes8 = new Uint8Array(scratch8)
90
+
91
+ /**
92
+ * Stages eight wire bytes into `scratchView8`. Two big-endian `setInt32` calls
93
+ * beat both `Uint8Array.prototype.set` and eight stores, and unlike `set` they
94
+ * read from anywhere in `bytes` without a view of their own.
95
+ */
96
+ const stage8 = (bytes: Uint8Array, offset: number): void => {
97
+ scratchView8.setInt32(0, readInt32(bytes, offset))
98
+ scratchView8.setInt32(4, readInt32(bytes, offset + 4))
99
+ }
100
+
101
+ /** Decimal text for every two-digit number, so formatting never calls `padStart`. */
102
+ const twoDigits = /* @__PURE__ */ Array.from({ length: 100 }, (_, value) => (value < 10 ? "0" : "") + value)
103
+
104
+ /**
105
+ * Above this length `TextEncoder.encodeInto` beats a per-character loop, below
106
+ * it the call overhead dominates. Measured on V8: the loop runs at about a
107
+ * nanosecond per character and `encodeInto` costs about 50 ns whatever the
108
+ * length, so the crossover is around 50 characters.
109
+ */
110
+ const asciiEncodeLimit = 48
111
+
112
+ /**
113
+ * `encodeInto` needs somewhere to write. Encoding through this and copying out
114
+ * still beats `TextEncoder.encode`, which allocates inside the call. Strings
115
+ * that would need a larger one are rare enough to encode the slow way rather
116
+ * than keep that much memory alive.
117
+ */
118
+ let scratchUtf8 = new Uint8Array(4096)
119
+
120
+ const scratchUtf8Limit = 64 * 1024
121
+
122
+ /** Encodes into `scratchUtf8`, returning how many bytes it holds, or -1. */
123
+ const encodeUtf8Scratch = (text: string): number => {
124
+ const capacity = text.length * 3
125
+ if (capacity > scratchUtf8Limit) return -1
126
+ if (capacity > scratchUtf8.length) scratchUtf8 = new Uint8Array(capacity)
127
+ return textEncoder.encodeInto(text, scratchUtf8).written
128
+ }
129
+
130
+ const encodeUtf8 = (text: string): Uint8Array => {
131
+ const length = text.length
132
+ if (length <= asciiEncodeLimit) {
133
+ const bytes = new Uint8Array(length)
134
+ let i = 0
135
+ for (; i < length; i++) {
136
+ const code = text.charCodeAt(i)
137
+ if (code > 0x7f) break
138
+ bytes[i] = code
139
+ }
140
+ if (i === length) return bytes
141
+ }
142
+ const written = encodeUtf8Scratch(text)
143
+ return written === -1 ? textEncoder.encode(text) : scratchUtf8.slice(0, written)
144
+ }
145
+
146
+ /** As `encodeUtf8`, but leaves `prefix` bytes free at the front. */
147
+ const encodeUtf8Prefixed = (text: string, prefix: number): Uint8Array => {
148
+ const length = text.length
149
+ if (length <= asciiEncodeLimit) {
150
+ const bytes = new Uint8Array(prefix + length)
151
+ let i = 0
152
+ for (; i < length; i++) {
153
+ const code = text.charCodeAt(i)
154
+ if (code > 0x7f) break
155
+ bytes[prefix + i] = code
156
+ }
157
+ if (i === length) return bytes
158
+ }
159
+ const written = encodeUtf8Scratch(text)
160
+ if (written === -1) {
161
+ const body = textEncoder.encode(text)
162
+ const bytes = new Uint8Array(prefix + body.length)
163
+ bytes.set(body, prefix)
164
+ return bytes
165
+ }
166
+ const bytes = new Uint8Array(prefix + written)
167
+ bytes.set(scratchUtf8.subarray(0, written), prefix)
168
+ return bytes
169
+ }
170
+
171
+ /**
172
+ * A view of `size` bytes at `offset`, or `bytes` itself when that is already
173
+ * the whole of it. Only the codecs that hand their bytes to something else
174
+ * need one; the rest read through `bytes` and `offset` directly.
175
+ */
176
+ const region = (bytes: Uint8Array, offset: number, size: number): Uint8Array =>
177
+ offset === 0 && size === bytes.length ? bytes : new Uint8Array(bytes.buffer, bytes.byteOffset + offset, size)
178
+
179
+ /**
180
+ * Below this length building the string a character at a time beats
181
+ * `TextDecoder.decode`, which costs about 50 ns before it looks at a byte.
182
+ * Above it the per-character cost of about 3 ns takes over.
183
+ */
184
+ const asciiDecodeLimit = 10
185
+
186
+ /**
187
+ * Node's own UTF-8 decoder, which reads straight out of the buffer with no
188
+ * view and about a quarter less overhead than `TextDecoder`. It swaps
189
+ * replacement characters in where `TextDecoder` would fail, so a result that
190
+ * contains one is handed to the strict decoder after all: either the
191
+ * character was really in the text and the same string comes back, or the
192
+ * bytes were invalid and the failure is the one `TextDecoder` always raised.
193
+ */
194
+ const utf8Slice: ((this: Uint8Array, start: number, end: number) => string) | undefined = (globalThis as any).Buffer
195
+ ?.prototype?.utf8Slice
196
+
197
+ const decodeUtf8 = (bytes: Uint8Array, offset: number, size: number): string => {
198
+ if (size <= asciiDecodeLimit) {
199
+ let text = ""
200
+ let index = 0
201
+ for (; index < size; index++) {
202
+ const code = bytes[offset + index]
203
+ if (code > 0x7f) break
204
+ text += String.fromCharCode(code)
205
+ }
206
+ if (index === size) return text
207
+ }
208
+ if (utf8Slice !== undefined) {
209
+ const text = utf8Slice.call(bytes, offset, offset + size)
210
+ if (text.indexOf("\ufffd") === -1) return text
211
+ }
212
+ try {
213
+ return textDecoder.decode(region(bytes, offset, size))
214
+ } catch {
215
+ return fail("Invalid UTF-8 in text value")
216
+ }
217
+ }
218
+
219
+ const requireSize = (size: number, expected: number, name: string): void => {
220
+ if (size !== expected) {
221
+ fail(`Expected ${expected} byte(s) for ${name}, received ${size}`)
222
+ }
223
+ }
224
+
225
+ const requireString = (value: unknown, name: string): string =>
226
+ typeof value === "string" ? value : fail(`Expected a string for ${name}`)
227
+
228
+ const requireBigInt = (value: unknown, name: string): bigint =>
229
+ typeof value === "bigint" ? value : fail(`Expected a bigint for ${name}`)
230
+
231
+ const requireNumber = (value: unknown, name: string): number =>
232
+ typeof value === "number" ? value : fail(`Expected a number for ${name}`)
233
+
234
+ const requireInteger = (value: unknown, name: string, min: number, max: number): number => {
235
+ const num = requireNumber(value, name)
236
+ if (!Number.isInteger(num) || num < min || num > max) {
237
+ fail(`Expected an integer in [${min}, ${max}] for ${name}, received ${num}`)
238
+ }
239
+ return num
240
+ }
241
+
242
+ const ZERO = BigInt(0)
243
+ const THOUSAND = BigInt(1000)
244
+ const INT64_MIN = BigInt("-9223372036854775808")
245
+ const INT64_MAX = BigInt("9223372036854775807")
246
+ const INT32_MIN = -2147483648
247
+ const INT32_MAX = 2147483647
248
+
249
+ /** Milliseconds between the Unix epoch and the PostgreSQL epoch (2000-01-01). */
250
+ const PG_EPOCH_MS = 946684800000
251
+ const PG_EPOCH_MICROS = 946684800000000
252
+ /** Days between the Unix epoch and the PostgreSQL epoch. */
253
+ const PG_EPOCH_DAYS = 10957
254
+
255
+ /**
256
+ * Beyond this an integer no longer has an exact `Number`, so the timestamp
257
+ * codecs fall back to `BigInt` rather than round. It is about 285 years of
258
+ * microseconds either side of 2000-01-01.
259
+ */
260
+ const MAX_EXACT = 9007199254740992
261
+ /** The `high` half of an int64 whose magnitude is below `MAX_EXACT`. */
262
+ const MAX_EXACT_HIGH = 0x200000
263
+
264
+ /** Midnight to midnight, the widest PostgreSQL `time` and `timetz` allow. */
265
+ const MAX_TIME_MICROS_NUMBER = 86_400_000_000
266
+ /** PostgreSQL accepts time zone displacements strictly inside +/-16 hours. */
267
+ const TZDISP_LIMIT_SECONDS = 57_600
268
+
269
+ // -----------------------------------------------------------------------------
270
+ // OIDs
271
+ // -----------------------------------------------------------------------------
272
+
273
+ /**
274
+ * Type OIDs implemented by version 1 of this codec.
275
+ *
276
+ * @category constants
277
+ * @since 4.0.0
278
+ */
279
+ export const OID = {
280
+ bool: 16,
281
+ bytea: 17,
282
+ name: 19,
283
+ int8: 20,
284
+ int2: 21,
285
+ int4: 23,
286
+ text: 25,
287
+ oid: 26,
288
+ json: 114,
289
+ cidr: 650,
290
+ float4: 700,
291
+ float8: 701,
292
+ inet: 869,
293
+ bpchar: 1042,
294
+ varchar: 1043,
295
+ date: 1082,
296
+ time: 1083,
297
+ timestamp: 1114,
298
+ timestamptz: 1184,
299
+ timetz: 1266,
300
+ numeric: 1700,
301
+ uuid: 2950,
302
+ jsonb: 3802,
303
+ boolArray: 1000,
304
+ byteaArray: 1001,
305
+ nameArray: 1003,
306
+ int8Array: 1016,
307
+ int2Array: 1005,
308
+ int4Array: 1007,
309
+ textArray: 1009,
310
+ oidArray: 1028,
311
+ jsonArray: 199,
312
+ cidrArray: 651,
313
+ float4Array: 1021,
314
+ float8Array: 1022,
315
+ inetArray: 1041,
316
+ bpcharArray: 1014,
317
+ varcharArray: 1015,
318
+ dateArray: 1182,
319
+ timeArray: 1183,
320
+ timestampArray: 1115,
321
+ timestamptzArray: 1185,
322
+ timetzArray: 1270,
323
+ numericArray: 1231,
324
+ uuidArray: 2951,
325
+ jsonbArray: 3807
326
+ } as const
327
+
328
+ const arrayToElement = new Map<number, number>([
329
+ [OID.boolArray, OID.bool],
330
+ [OID.byteaArray, OID.bytea],
331
+ [OID.nameArray, OID.name],
332
+ [OID.int8Array, OID.int8],
333
+ [OID.int2Array, OID.int2],
334
+ [OID.int4Array, OID.int4],
335
+ [OID.textArray, OID.text],
336
+ [OID.oidArray, OID.oid],
337
+ [OID.jsonArray, OID.json],
338
+ [OID.cidrArray, OID.cidr],
339
+ [OID.float4Array, OID.float4],
340
+ [OID.float8Array, OID.float8],
341
+ [OID.inetArray, OID.inet],
342
+ [OID.bpcharArray, OID.bpchar],
343
+ [OID.varcharArray, OID.varchar],
344
+ [OID.dateArray, OID.date],
345
+ [OID.timeArray, OID.time],
346
+ [OID.timestampArray, OID.timestamp],
347
+ [OID.timestamptzArray, OID.timestamptz],
348
+ [OID.timetzArray, OID.timetz],
349
+ [OID.numericArray, OID.numeric],
350
+ [OID.uuidArray, OID.uuid],
351
+ [OID.jsonbArray, OID.jsonb]
352
+ ])
353
+
354
+ const elementToArray = new Map<number, number>(
355
+ Array.from(arrayToElement, ([array, element]) => [element, array])
356
+ )
357
+
358
+ /**
359
+ * Returns the array OID whose elements have the given OID, or `undefined`
360
+ * when there is no array type registered for it.
361
+ *
362
+ * @category getters
363
+ * @since 4.0.0
364
+ */
365
+ export const arrayOidFor = (elementOid: number): number | undefined => elementToArray.get(elementOid)
366
+
367
+ // -----------------------------------------------------------------------------
368
+ // calendar helpers
369
+ // -----------------------------------------------------------------------------
370
+
371
+ const pad = (value: number, size: number): string => String(value).padStart(size, "0")
372
+
373
+ /** Civil date from days since 1970-01-01, after Howard Hinnant's algorithm. */
374
+ const civilFromDays = (days: number): { year: number; month: number; day: number } => {
375
+ const z = days + 719468
376
+ const era = Math.floor(z / 146097)
377
+ const doe = z - era * 146097
378
+ const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365)
379
+ const y = yoe + era * 400
380
+ const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100))
381
+ const mp = Math.floor((5 * doy + 2) / 153)
382
+ const day = doy - Math.floor((153 * mp + 2) / 5) + 1
383
+ const month = mp < 10 ? mp + 3 : mp - 9
384
+ return { year: month <= 2 ? y + 1 : y, month, day }
385
+ }
386
+
387
+ /** Days since 1970-01-01 from a civil date. */
388
+ const daysFromCivil = (year: number, month: number, day: number): number => {
389
+ const y = month <= 2 ? year - 1 : year
390
+ const era = Math.floor(y / 400)
391
+ const yoe = y - era * 400
392
+ const mp = month > 2 ? month - 3 : month + 9
393
+ const doy = Math.floor((153 * mp + 2) / 5) + day - 1
394
+ const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy
395
+ return era * 146097 + doe - 719468
396
+ }
397
+
398
+ const monthLengths = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
399
+
400
+ const isLeapYear = (year: number): boolean => (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
401
+
402
+ /** The two-digit number at `at`, or `-1` when those characters are not digits. */
403
+ const twoDigitsAt = (text: string, at: number): number => {
404
+ const tens = text.charCodeAt(at) - 48
405
+ const units = text.charCodeAt(at + 1) - 48
406
+ return tens >= 0 && tens <= 9 && units >= 0 && units <= 9 ? tens * 10 + units : -1
407
+ }
408
+
409
+ /** Parses `[-]YYYY-MM-DD`, with as many year digits as the caller likes. */
410
+ const parseDate = (text: string): number => {
411
+ const yearStart = text.charCodeAt(0) === 45 ? 1 : 0
412
+ const yearEnd = text.length - 6
413
+ if (yearEnd - yearStart < 4 || text.charCodeAt(yearEnd) !== 45 || text.charCodeAt(yearEnd + 3) !== 45) {
414
+ return fail(`Expected a YYYY-MM-DD date, received "${text}"`)
415
+ }
416
+ let year = 0
417
+ for (let index = yearStart; index < yearEnd; index++) {
418
+ const digit = text.charCodeAt(index) - 48
419
+ if (digit < 0 || digit > 9) {
420
+ return fail(`Expected a YYYY-MM-DD date, received "${text}"`)
421
+ }
422
+ year = year * 10 + digit
423
+ }
424
+ const month = twoDigitsAt(text, yearEnd + 1)
425
+ const day = twoDigitsAt(text, yearEnd + 4)
426
+ if (month === -1 || day === -1) {
427
+ return fail(`Expected a YYYY-MM-DD date, received "${text}"`)
428
+ }
429
+ if (yearStart === 1) year = -year
430
+ // Enough digits and the year is Infinity, which `daysFromCivil` turns into a
431
+ // NaN the range check below would let through as a zero.
432
+ if (!Number.isFinite(year)) {
433
+ return fail(`date out of range: "${text}"`)
434
+ }
435
+ const monthLength = month === 2 && isLeapYear(year) ? 29 : monthLengths[month]
436
+ if (month < 1 || month > 12 || day < 1 || day > monthLength) {
437
+ return fail(`Invalid date "${text}"`)
438
+ }
439
+ return daysFromCivil(year, month, day)
440
+ }
441
+
442
+ const formatDate = (days: number): string => {
443
+ const { day, month, year } = civilFromDays(days)
444
+ const yearText = year < 0
445
+ ? `-${pad(-year, 4)}`
446
+ : year < 10000
447
+ ? twoDigits[(year / 100) | 0] + twoDigits[year % 100]
448
+ : String(year)
449
+ return `${yearText}-${twoDigits[month]}-${twoDigits[day]}`
450
+ }
451
+
452
+ const timeRegex = /^(\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,6}))?)?$/
453
+
454
+ /** Microseconds since midnight. A day of them is well inside `Number`'s exact range. */
455
+ const parseTimeOfDay = (text: string): number => {
456
+ const match = timeRegex.exec(text)
457
+ if (match === null) {
458
+ return fail(`Expected a HH:MM[:SS[.ffffff]] time, received "${text}"`)
459
+ }
460
+ const hours = Number(match[1])
461
+ const minutes = Number(match[2])
462
+ const seconds = match[3] === undefined ? 0 : Number(match[3])
463
+ const fraction = match[4] === undefined ? 0 : Number(match[4].padEnd(6, "0"))
464
+ const micros = (hours * 3600 + minutes * 60 + seconds) * 1_000_000 + fraction
465
+ if (hours > 24 || minutes > 59 || seconds > 60 || micros > MAX_TIME_MICROS_NUMBER) {
466
+ return fail(`Invalid time "${text}"`)
467
+ }
468
+ return micros
469
+ }
470
+
471
+ const formatTimeOfDay = (micros: number): string => {
472
+ const seconds = (micros / 1_000_000) | 0
473
+ const fraction = micros - seconds * 1_000_000
474
+ const base = `${twoDigits[(seconds / 3600) | 0]}:${twoDigits[((seconds / 60) | 0) % 60]}:${twoDigits[seconds % 60]}`
475
+ if (fraction === 0) return base
476
+ return `${base}.${pad(fraction, 6).replace(/0+$/, "")}`;
477
+ }
478
+
479
+ const zoneRegex = /^(?:Z|([+-])(\d{2})(?::?(\d{2}))?(?::?(\d{2}))?)$/
480
+
481
+ /** Parses a trailing time zone into seconds east of UTC. */
482
+ const parseZone = (text: string): number => {
483
+ const match = zoneRegex.exec(text)
484
+ if (match === null) {
485
+ return fail(`Expected a time zone offset, received "${text}"`)
486
+ }
487
+ if (match[1] === undefined) return 0
488
+ const minutes = match[3] === undefined ? 0 : Number(match[3])
489
+ const trailingSeconds = match[4] === undefined ? 0 : Number(match[4])
490
+ const seconds = Number(match[2]) * 3600 + minutes * 60 + trailingSeconds
491
+ if (minutes > 59 || trailingSeconds > 59 || seconds >= TZDISP_LIMIT_SECONDS) {
492
+ return fail(`Time zone offset out of range, received "${text}"`)
493
+ }
494
+ return match[1] === "-" ? -seconds : seconds
495
+ }
496
+
497
+ const formatZone = (secondsEast: number): string => {
498
+ const sign = secondsEast < 0 ? "-" : "+"
499
+ const total = Math.abs(secondsEast)
500
+ return `${sign}${pad((total / 3600) | 0, 2)}:${twoDigits[((total / 60) | 0) % 60]}`
501
+ }
502
+
503
+ // -----------------------------------------------------------------------------
504
+ // numeric
505
+ // -----------------------------------------------------------------------------
506
+
507
+ const NUMERIC_POS = 0x0000
508
+ const NUMERIC_NEG = 0x4000
509
+ const NUMERIC_NAN = 0xc000
510
+ const NUMERIC_PINF = 0xd000
511
+ const NUMERIC_NINF = 0xf000
512
+
513
+ const numericRegex = /^([+-])?(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/
514
+
515
+ const encodeNumeric = (value: unknown): Uint8Array => {
516
+ const text = requireString(value, "numeric")
517
+ if (text === "NaN") return numericSpecial(NUMERIC_NAN)
518
+ if (text === "Infinity") return numericSpecial(NUMERIC_PINF)
519
+ if (text === "-Infinity") return numericSpecial(NUMERIC_NINF)
520
+
521
+ const match = numericRegex.exec(text)
522
+ if (match === null || (match[2] ?? "") + (match[3] ?? "") === "") {
523
+ return fail(`Expected a decimal numeric string, received "${text}"`)
524
+ }
525
+ const exponent = match[4] === undefined ? 0 : Number(match[4])
526
+ let digits = (match[2] ?? "") + (match[3] ?? "")
527
+ let scale = (match[3] ?? "").length - exponent
528
+ if (scale < 0) {
529
+ digits += "0".repeat(-scale)
530
+ scale = 0
531
+ }
532
+ if (scale > 0x3fff) {
533
+ return fail(`numeric scale ${scale} exceeds the PostgreSQL maximum`)
534
+ }
535
+
536
+ const rightPad = (4 - (scale % 4)) % 4
537
+ digits += "0".repeat(rightPad)
538
+ const fractionLength = scale + rightPad
539
+ const leftPad = (4 - ((digits.length - fractionLength) % 4)) % 4
540
+ digits = "0".repeat(leftPad) + digits
541
+
542
+ // Groups are read out of `digits` on demand rather than collected, so
543
+ // trimming the leading and trailing zero groups is two moving indices
544
+ // instead of an array and a `shift` per leading zero.
545
+ const groupAt = (index: number): number => {
546
+ const at = index * 4
547
+ return (digits.charCodeAt(at) - 48) * 1000 + (digits.charCodeAt(at + 1) - 48) * 100 +
548
+ (digits.charCodeAt(at + 2) - 48) * 10 + (digits.charCodeAt(at + 3) - 48)
549
+ }
550
+ const groupCount = digits.length / 4
551
+ let first = 0
552
+ while (first < groupCount && groupAt(first) === 0) first++
553
+ let last = groupCount
554
+ while (last > first && groupAt(last - 1) === 0) last--
555
+ const count = last - first
556
+ const weight = count === 0 ? 0 : (digits.length - fractionLength) / 4 - 1 - first
557
+
558
+ const sign = count > 0 && match[1] === "-" ? NUMERIC_NEG : NUMERIC_POS
559
+ const bytes = new Uint8Array(8 + count * 2)
560
+ writeInt16(bytes, 0, count)
561
+ writeInt16(bytes, 2, weight)
562
+ writeInt16(bytes, 4, sign)
563
+ writeInt16(bytes, 6, scale)
564
+ for (let i = 0; i < count; i++) {
565
+ writeInt16(bytes, 8 + i * 2, groupAt(first + i))
566
+ }
567
+ return bytes
568
+ }
569
+
570
+ const numericSpecial = (sign: number): Uint8Array => {
571
+ const bytes = new Uint8Array(8)
572
+ writeInt16(bytes, 4, sign)
573
+ return bytes
574
+ }
575
+
576
+ const decodeNumeric = (bytes: Uint8Array, offset: number, size: number): string => {
577
+ if (size < 8) return fail("Truncated numeric value")
578
+ const count = readInt16(bytes, offset)
579
+ const weight = readInt16(bytes, offset + 2)
580
+ const sign = readUint16(bytes, offset + 4)
581
+ const scale = readInt16(bytes, offset + 6)
582
+ if (sign === NUMERIC_NAN) return "NaN"
583
+ if (sign === NUMERIC_PINF) return "Infinity"
584
+ if (sign === NUMERIC_NINF) return "-Infinity"
585
+ if (sign !== NUMERIC_POS && sign !== NUMERIC_NEG) {
586
+ return fail(`Invalid numeric sign: ${sign}`)
587
+ }
588
+ requireSize(size, 8 + count * 2, "numeric")
589
+
590
+ const digits = offset + 8
591
+ // Groups outside 0-9999 have no four-digit text, and reading past the ones
592
+ // the value carries is how the fractional part is padded out.
593
+ const digitAt = (index: number): number => {
594
+ if (index < 0 || index >= count) return 0
595
+ const digit = readInt16(bytes, digits + index * 2)
596
+ return digit >= 0 && digit <= 9999 ? digit : fail(`Invalid numeric digit: ${digit}`)
597
+ }
598
+
599
+ let result = sign === NUMERIC_NEG ? "-" : ""
600
+ if (weight < 0) {
601
+ result += "0"
602
+ } else {
603
+ for (let i = 0; i <= weight; i++) {
604
+ const digit = digitAt(i)
605
+ if (i === 0) {
606
+ result += String(digit)
607
+ } else {
608
+ result += twoDigits[(digit / 100) | 0]
609
+ result += twoDigits[digit % 100]
610
+ }
611
+ }
612
+ }
613
+ if (scale > 0) {
614
+ let fraction = ""
615
+ for (let i = weight + 1; fraction.length < scale; i++) {
616
+ const digit = digitAt(i)
617
+ fraction += twoDigits[(digit / 100) | 0]
618
+ fraction += twoDigits[digit % 100]
619
+ }
620
+ result += `.${fraction.slice(0, scale)}`
621
+ }
622
+ return result
623
+ }
624
+
625
+ // -----------------------------------------------------------------------------
626
+ // network addresses
627
+ // -----------------------------------------------------------------------------
628
+
629
+ const PGSQL_AF_INET = 2
630
+ const PGSQL_AF_INET6 = 3
631
+
632
+ const parseIPv4 = (text: string): Uint8Array | undefined => {
633
+ const parts = text.split(".")
634
+ if (parts.length !== 4) return undefined
635
+ const bytes = new Uint8Array(4)
636
+ for (let i = 0; i < 4; i++) {
637
+ if (!/^\d{1,3}$/.test(parts[i])) return undefined
638
+ const value = Number(parts[i])
639
+ if (value > 255) return undefined
640
+ bytes[i] = value
641
+ }
642
+ return bytes
643
+ }
644
+
645
+ const parseIPv6 = (text: string): Uint8Array | undefined => {
646
+ const halves = text.split("::")
647
+ if (halves.length > 2) return undefined
648
+ const toGroups = (part: string): Array<string> => part === "" ? [] : part.split(":")
649
+ const head = toGroups(halves[0])
650
+ const tail = halves.length === 2 ? toGroups(halves[1]) : []
651
+ const bytes = new Uint8Array(16)
652
+
653
+ const trailing = tail.length > 0 ? tail[tail.length - 1] : head.length > 0 ? head[head.length - 1] : ""
654
+ const embedded = trailing.includes(".") ? parseIPv4(trailing) : undefined
655
+ if (trailing.includes(".") && embedded === undefined) return undefined
656
+ if (embedded !== undefined) {
657
+ if (tail.length > 0) tail.pop()
658
+ else head.pop()
659
+ }
660
+ const groupCount = embedded === undefined ? 8 : 6
661
+ if (head.length + tail.length > groupCount) return undefined
662
+ if (halves.length === 1 && head.length !== groupCount) return undefined
663
+
664
+ const writeGroup = (group: string, offset: number): boolean => {
665
+ if (!/^[0-9a-fA-F]{1,4}$/.test(group)) return false
666
+ const value = Number.parseInt(group, 16)
667
+ bytes[offset] = value >> 8
668
+ bytes[offset + 1] = value & 0xff
669
+ return true
670
+ }
671
+ for (let i = 0; i < head.length; i++) {
672
+ if (!writeGroup(head[i], i * 2)) return undefined
673
+ }
674
+ for (let i = 0; i < tail.length; i++) {
675
+ if (!writeGroup(tail[i], (groupCount - tail.length + i) * 2)) return undefined
676
+ }
677
+ if (embedded !== undefined) bytes.set(embedded, 12)
678
+ return bytes
679
+ }
680
+
681
+ const formatIPv4 = (bytes: Uint8Array, offset: number): string =>
682
+ `${bytes[offset]}.${bytes[offset + 1]}.${bytes[offset + 2]}.${bytes[offset + 3]}`
683
+
684
+ const formatIPv6 = (bytes: Uint8Array, offset: number): string => {
685
+ let isV4Mapped = bytes[offset + 10] === 0xff && bytes[offset + 11] === 0xff
686
+ for (let i = 0; isV4Mapped && i < 10; i++) isV4Mapped = bytes[offset + i] === 0
687
+ if (isV4Mapped) return `::ffff:${formatIPv4(bytes, offset + 12)}`
688
+
689
+ const groups: Array<number> = []
690
+ for (let i = 0; i < 8; i++) groups.push(readUint16(bytes, offset + i * 2))
691
+
692
+ let bestStart = -1
693
+ let bestLength = 0
694
+ let start = -1
695
+ for (let i = 0; i <= 8; i++) {
696
+ if (i < 8 && groups[i] === 0) {
697
+ if (start === -1) start = i
698
+ } else if (start !== -1) {
699
+ if (i - start > bestLength) {
700
+ bestStart = start
701
+ bestLength = i - start
702
+ }
703
+ start = -1
704
+ }
705
+ }
706
+ if (bestLength < 2) {
707
+ return groups.map((group) => group.toString(16)).join(":")
708
+ }
709
+ const head = groups.slice(0, bestStart).map((group) => group.toString(16)).join(":")
710
+ const tail = groups.slice(bestStart + bestLength).map((group) => group.toString(16)).join(":")
711
+ return `${head}::${tail}`
712
+ }
713
+
714
+ const hasHostBits = (bytes: Uint8Array, offset: number, length: number, bits: number): boolean => {
715
+ const wholeBytes = Math.floor(bits / 8)
716
+ const partialBits = bits % 8
717
+ if (partialBits !== 0 && (bytes[offset + wholeBytes] & ((1 << (8 - partialBits)) - 1)) !== 0) {
718
+ return true
719
+ }
720
+ for (let index = wholeBytes + (partialBits === 0 ? 0 : 1); index < length; index++) {
721
+ if (bytes[offset + index] !== 0) return true
722
+ }
723
+ return false
724
+ }
725
+
726
+ const encodeInet = (value: unknown, isCidr: boolean): Uint8Array => {
727
+ const text = requireString(value, isCidr ? "cidr" : "inet")
728
+ const slash = text.lastIndexOf("/")
729
+ const address = slash === -1 ? text : text.slice(0, slash)
730
+ const v4 = parseIPv4(address)
731
+ const bytes = v4 ?? parseIPv6(address)
732
+ if (bytes === undefined) {
733
+ return fail(`Expected an IP address, received "${text}"`)
734
+ }
735
+ const fullBits = bytes.length * 8
736
+ const bits = slash === -1 ? fullBits : Number(text.slice(slash + 1))
737
+ if (!Number.isInteger(bits) || bits < 0 || bits > fullBits) {
738
+ return fail(`Invalid netmask length in "${text}"`)
739
+ }
740
+ if (isCidr && hasHostBits(bytes, 0, bytes.length, bits)) {
741
+ return fail(`CIDR address has host bits set in "${text}"`)
742
+ }
743
+ const result = new Uint8Array(4 + bytes.length)
744
+ result[0] = v4 === undefined ? PGSQL_AF_INET6 : PGSQL_AF_INET
745
+ result[1] = bits
746
+ result[2] = isCidr ? 1 : 0
747
+ result[3] = bytes.length
748
+ result.set(bytes, 4)
749
+ return result
750
+ }
751
+
752
+ const decodeInet = (bytes: Uint8Array, offset: number, size: number): string => {
753
+ if (size < 4) return fail("Truncated inet value")
754
+ const family = bytes[offset]
755
+ const bits = bytes[offset + 1]
756
+ const cidrFlag = bytes[offset + 2]
757
+ if (cidrFlag > 1) return fail(`Invalid inet CIDR flag: ${cidrFlag}`)
758
+ const isCidr = cidrFlag === 1
759
+ const addressSize = bytes[offset + 3]
760
+ const expected = family === PGSQL_AF_INET ? 4 : family === PGSQL_AF_INET6 ? 16 : -1
761
+ if (expected === -1 || addressSize !== expected) {
762
+ return fail(`Invalid inet address family ${family} with ${addressSize} byte(s)`)
763
+ }
764
+ requireSize(size, 4 + addressSize, "inet")
765
+ if (bits > addressSize * 8) return fail(`Invalid inet netmask length: ${bits}`)
766
+ if (isCidr && hasHostBits(bytes, offset + 4, addressSize, bits)) {
767
+ return fail("CIDR address has host bits set")
768
+ }
769
+ const text = family === PGSQL_AF_INET ? formatIPv4(bytes, offset + 4) : formatIPv6(bytes, offset + 4)
770
+ return isCidr || bits !== addressSize * 8 ? `${text}/${bits}` : text
771
+ }
772
+
773
+ // -----------------------------------------------------------------------------
774
+ // uuid
775
+ // -----------------------------------------------------------------------------
776
+
777
+ const hexDigits = "0123456789abcdef"
778
+
779
+ /** Byte value of each hex character code, `-1` for everything else. */
780
+ const hexValues = new Int8Array(128).fill(-1)
781
+ for (let i = 0; i < 16; i++) {
782
+ hexValues[hexDigits.charCodeAt(i)] = i
783
+ hexValues["0123456789ABCDEF".charCodeAt(i)] = i
784
+ }
785
+
786
+ /** Character codes of each byte value's two hex digits. */
787
+ const hexHigh = /* @__PURE__ */ (() => {
788
+ const codes = new Uint8Array(256)
789
+ for (let i = 0; i < 256; i++) codes[i] = hexDigits.charCodeAt(i >> 4)
790
+ return codes
791
+ })()
792
+ const hexLow = /* @__PURE__ */ (() => {
793
+ const codes = new Uint8Array(256)
794
+ for (let i = 0; i < 256; i++) codes[i] = hexDigits.charCodeAt(i & 0xf)
795
+ return codes
796
+ })()
797
+
798
+ /** Offsets of the 16 uuid bytes within the 36-character hyphenated text. */
799
+ const uuidOffsets = [0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34]
800
+
801
+ /** The hex digit at `at`, or `-1` when that character is not one. */
802
+ const hexAt = (text: string, at: number): number => {
803
+ const code = text.charCodeAt(at)
804
+ return code < 128 ? hexValues[code] : -1
805
+ }
806
+
807
+ /**
808
+ * The hyphen positions do the validating that a regular expression used to:
809
+ * anything else in the 36 characters has to be a hex digit for the pairs to
810
+ * come out, and `hexAt` reports the ones that are not.
811
+ */
812
+ /** The four int32 words of the uuid `uuidWords` last parsed. */
813
+ const uuidWord = new Int32Array(4)
814
+
815
+ /**
816
+ * Parses the hyphenated text into `uuidWord`. Both encoding paths read the
817
+ * words from there rather than from a returned array, so neither allocates.
818
+ */
819
+ const uuidWords = (value: unknown): void => {
820
+ const text = requireString(value, "uuid")
821
+ if (
822
+ text.length !== 36 || text.charCodeAt(8) !== 45 || text.charCodeAt(13) !== 45 ||
823
+ text.charCodeAt(18) !== 45 || text.charCodeAt(23) !== 45
824
+ ) {
825
+ return fail(`Expected a UUID, received "${text}"`)
826
+ }
827
+ for (let word = 0; word < 4; word++) {
828
+ let bits = 0
829
+ let valid = 0
830
+ for (let i = word * 4; i < word * 4 + 4; i++) {
831
+ const at = uuidOffsets[i]
832
+ const high = hexAt(text, at)
833
+ const low = hexAt(text, at + 1)
834
+ valid |= high | low
835
+ bits = (bits << 8) | (high << 4) | low
836
+ }
837
+ if (valid < 0) {
838
+ return fail(`Expected a UUID, received "${text}"`)
839
+ }
840
+ uuidWord[word] = bits
841
+ }
842
+ }
843
+
844
+ const encodeUuid = (value: unknown): Uint8Array => {
845
+ uuidWords(value)
846
+ const bytes = new Uint8Array(16)
847
+ writeInt32(bytes, 0, uuidWord[0])
848
+ writeInt32(bytes, 4, uuidWord[1])
849
+ writeInt32(bytes, 8, uuidWord[2])
850
+ writeInt32(bytes, 12, uuidWord[3])
851
+ return bytes
852
+ }
853
+
854
+ // Four int32s are the sixteen bytes, so a uuid parameter needs no array of
855
+ // its own on the way into a frame.
856
+ const writeUuid = (sink: ValueSink, value: unknown): void => {
857
+ uuidWords(value)
858
+ sink.int32(uuidWord[0])
859
+ sink.int32(uuidWord[1])
860
+ sink.int32(uuidWord[2])
861
+ sink.int32(uuidWord[3])
862
+ }
863
+
864
+ // One `String.fromCharCode` call, so the result is a flat string rather than
865
+ // a rope of pair concatenations that its first reader has to flatten. 45 is
866
+ // the hyphen.
867
+ const decodeUuid = (bytes: Uint8Array, offset: number, size: number): string => {
868
+ requireSize(size, 16, "uuid")
869
+ const b0 = bytes[offset]
870
+ const b1 = bytes[offset + 1]
871
+ const b2 = bytes[offset + 2]
872
+ const b3 = bytes[offset + 3]
873
+ const b4 = bytes[offset + 4]
874
+ const b5 = bytes[offset + 5]
875
+ const b6 = bytes[offset + 6]
876
+ const b7 = bytes[offset + 7]
877
+ const b8 = bytes[offset + 8]
878
+ const b9 = bytes[offset + 9]
879
+ const b10 = bytes[offset + 10]
880
+ const b11 = bytes[offset + 11]
881
+ const b12 = bytes[offset + 12]
882
+ const b13 = bytes[offset + 13]
883
+ const b14 = bytes[offset + 14]
884
+ const b15 = bytes[offset + 15]
885
+ return String.fromCharCode(
886
+ hexHigh[b0],
887
+ hexLow[b0],
888
+ hexHigh[b1],
889
+ hexLow[b1],
890
+ hexHigh[b2],
891
+ hexLow[b2],
892
+ hexHigh[b3],
893
+ hexLow[b3],
894
+ 45,
895
+ hexHigh[b4],
896
+ hexLow[b4],
897
+ hexHigh[b5],
898
+ hexLow[b5],
899
+ 45,
900
+ hexHigh[b6],
901
+ hexLow[b6],
902
+ hexHigh[b7],
903
+ hexLow[b7],
904
+ 45,
905
+ hexHigh[b8],
906
+ hexLow[b8],
907
+ hexHigh[b9],
908
+ hexLow[b9],
909
+ 45,
910
+ hexHigh[b10],
911
+ hexLow[b10],
912
+ hexHigh[b11],
913
+ hexLow[b11],
914
+ hexHigh[b12],
915
+ hexLow[b12],
916
+ hexHigh[b13],
917
+ hexLow[b13],
918
+ hexHigh[b14],
919
+ hexLow[b14],
920
+ hexHigh[b15],
921
+ hexLow[b15]
922
+ )
923
+ }
924
+
925
+ // -----------------------------------------------------------------------------
926
+ // codecs
927
+ // -----------------------------------------------------------------------------
928
+
929
+ /**
930
+ * A binary codec for a single OID.
931
+ *
932
+ * @category models
933
+ * @since 4.0.0
934
+ */
935
+ export interface Codec<A> {
936
+ readonly encode: (value: A) => Result.Result<Uint8Array, CodecError>
937
+ readonly decode: (bytes: Uint8Array) => Result.Result<A, CodecError>
938
+ /**
939
+ * Writes the value straight into a `Bind` frame, with no array of its own.
940
+ * Optional: a codec without one falls back to `encode` and a copy.
941
+ */
942
+ readonly write?: (sink: ValueSink, value: A) => Result.Result<void, CodecError>
943
+ /**
944
+ * Reads the value out of the `size` bytes at `offset`, with no view of its
945
+ * own. Optional: a codec without one is handed a view. Array elements are
946
+ * read through this, so it is what keeps decoding an array from allocating
947
+ * a view per element.
948
+ */
949
+ readonly read?: (bytes: Uint8Array, offset: number, size: number) => Result.Result<A, CodecError>
950
+ }
951
+
952
+ interface UnsafeCodec<A> {
953
+ readonly encode: (value: A) => Uint8Array
954
+ readonly decode: (bytes: Uint8Array) => A
955
+ readonly write?: (sink: ValueSink, value: A) => void
956
+ readonly read?: (bytes: Uint8Array, offset: number, size: number) => A
957
+ }
958
+
959
+ /** A codec whose `decode` is its `read` over the whole of its bytes. */
960
+ const codecOf = <A>(
961
+ read: (bytes: Uint8Array, offset: number, size: number) => A,
962
+ encode: (value: A) => Uint8Array,
963
+ write?: (sink: ValueSink, value: A) => void
964
+ ): UnsafeCodec<A> => {
965
+ const decode = (bytes: Uint8Array): A => read(bytes, 0, bytes.length)
966
+ return write === undefined ? { encode, decode, read } : { encode, decode, read, write }
967
+ }
968
+
969
+ /** A fresh copy of the first four staged bytes; `set` costs more than the stores. */
970
+ const takeScratch4 = (): Uint8Array => {
971
+ const bytes = new Uint8Array(4)
972
+ bytes[0] = scratchBytes4[0]
973
+ bytes[1] = scratchBytes4[1]
974
+ bytes[2] = scratchBytes4[2]
975
+ bytes[3] = scratchBytes4[3]
976
+ return bytes
977
+ }
978
+
979
+ const takeScratch8 = (): Uint8Array => {
980
+ const bytes = new Uint8Array(8)
981
+ bytes[0] = scratchBytes8[0]
982
+ bytes[1] = scratchBytes8[1]
983
+ bytes[2] = scratchBytes8[2]
984
+ bytes[3] = scratchBytes8[3]
985
+ bytes[4] = scratchBytes8[4]
986
+ bytes[5] = scratchBytes8[5]
987
+ bytes[6] = scratchBytes8[6]
988
+ bytes[7] = scratchBytes8[7]
989
+ return bytes
990
+ }
991
+
992
+ const utf8Codec: UnsafeCodec<any> = codecOf(
993
+ decodeUtf8,
994
+ (value) => encodeUtf8(requireString(value, "text")),
995
+ (sink, value) => sink.utf8(requireString(value, "text"))
996
+ )
997
+
998
+ const int8Value = (value: unknown): bigint => {
999
+ const big = requireBigInt(value, "int8")
1000
+ if (big < INT64_MIN || big > INT64_MAX) fail(`int8 out of range: ${big}`)
1001
+ return big
1002
+ }
1003
+
1004
+ const MAX_TIME_MICROS = BigInt(MAX_TIME_MICROS_NUMBER)
1005
+
1006
+ const timeValue = (value: unknown): bigint => {
1007
+ const micros = requireBigInt(value, "time")
1008
+ if (micros < ZERO || micros > MAX_TIME_MICROS) fail(`time out of range: ${micros}`)
1009
+ return micros
1010
+ }
1011
+
1012
+ /**
1013
+ * Microseconds since midnight, which always fit a `Number` exactly. Formatting
1014
+ * one assumes that, so a value the type cannot hold is an error rather than a
1015
+ * nonsense time.
1016
+ */
1017
+ const readTimeMicros = (bytes: Uint8Array, offset: number): number => {
1018
+ const high = readInt32(bytes, offset)
1019
+ const micros = high * 4294967296 + readUint32(bytes, offset + 4)
1020
+ if (high < 0 || micros > MAX_TIME_MICROS_NUMBER) {
1021
+ return fail(`timetz out of range: ${micros}`)
1022
+ }
1023
+ return micros
1024
+ }
1025
+
1026
+ /** The two halves of the int64 `timestampInt64` last produced. */
1027
+ let timestampHigh = 0
1028
+ let timestampLow = 0
1029
+
1030
+ /**
1031
+ * Converts epoch milliseconds to the halves of the wire int64. Both encoding
1032
+ * paths read them from here rather than from a returned pair, so neither
1033
+ * allocates.
1034
+ */
1035
+ const timestampInt64 = (value: unknown): void => {
1036
+ const ms = requireNumber(value, "timestamp")
1037
+ if (Number.isNaN(ms)) {
1038
+ fail("timestamp cannot be NaN")
1039
+ } else if (ms === Number.POSITIVE_INFINITY) {
1040
+ timestampHigh = INT32_MAX
1041
+ timestampLow = -1
1042
+ } else if (ms === Number.NEGATIVE_INFINITY) {
1043
+ timestampHigh = INT32_MIN
1044
+ timestampLow = 0
1045
+ } else {
1046
+ const unixMicros = Math.trunc(ms * 1000)
1047
+ if (!Number.isFinite(unixMicros)) {
1048
+ fail(`timestamp out of range: ${ms}`)
1049
+ }
1050
+ const micros = unixMicros - PG_EPOCH_MICROS
1051
+ if (micros > -MAX_EXACT && micros < MAX_EXACT) {
1052
+ timestampHigh = Math.floor(micros / 4294967296)
1053
+ timestampLow = micros - timestampHigh * 4294967296
1054
+ } else {
1055
+ const exact = BigInt(unixMicros) - BigInt(PG_EPOCH_MICROS)
1056
+ if (exact < INT64_MIN || exact > INT64_MAX) {
1057
+ fail(`timestamp out of range: ${ms}`)
1058
+ }
1059
+ scratchView8.setBigInt64(0, exact)
1060
+ timestampHigh = scratchView8.getInt32(0)
1061
+ timestampLow = scratchView8.getInt32(4)
1062
+ }
1063
+ }
1064
+ }
1065
+
1066
+ const timestampCodec: UnsafeCodec<any> = codecOf(
1067
+ (bytes, offset, size) => {
1068
+ requireSize(size, 8, "timestamp")
1069
+ const high = readInt32(bytes, offset)
1070
+ if (high >= -MAX_EXACT_HIGH && high < MAX_EXACT_HIGH) {
1071
+ // Inside these bounds the whole conversion is float arithmetic, so it
1072
+ // allocates no BigInt. Everything outside them, the sentinels included,
1073
+ // needs the exact 64-bit value.
1074
+ const micros = high * 4294967296 + readUint32(bytes, offset + 4)
1075
+ return (micros - micros % 1000) / 1000 + PG_EPOCH_MS
1076
+ }
1077
+ stage8(bytes, offset)
1078
+ const micros = scratchView8.getBigInt64(0)
1079
+ if (micros === INT64_MAX) return Number.POSITIVE_INFINITY
1080
+ if (micros === INT64_MIN) return Number.NEGATIVE_INFINITY
1081
+ return Number(micros / THOUSAND) + PG_EPOCH_MS
1082
+ },
1083
+ (value) => {
1084
+ timestampInt64(value)
1085
+ const bytes = new Uint8Array(8)
1086
+ writeInt32(bytes, 0, timestampHigh)
1087
+ writeInt32(bytes, 4, timestampLow)
1088
+ return bytes
1089
+ },
1090
+ // Two int32s are the int64, so the sink needs nothing of its own for it.
1091
+ (sink, value) => {
1092
+ timestampInt64(value)
1093
+ sink.int32(timestampHigh)
1094
+ sink.int32(timestampLow)
1095
+ }
1096
+ )
1097
+
1098
+ /** Days since the PostgreSQL epoch, or an infinity sentinel. */
1099
+ const dateDays = (value: unknown): number => {
1100
+ const text = requireString(value, "date")
1101
+ if (text === "infinity") return INT32_MAX
1102
+ if (text === "-infinity") return INT32_MIN
1103
+ const days = parseDate(text) - PG_EPOCH_DAYS
1104
+ if (days <= INT32_MIN || days >= INT32_MAX) {
1105
+ return fail(`date out of range: "${text}"`)
1106
+ }
1107
+ return days
1108
+ }
1109
+
1110
+ const dateCodec: UnsafeCodec<any> = codecOf(
1111
+ (bytes, offset, size) => {
1112
+ requireSize(size, 4, "date")
1113
+ const days = readInt32(bytes, offset)
1114
+ if (days === INT32_MAX) return "infinity"
1115
+ if (days === INT32_MIN) return "-infinity"
1116
+ return formatDate(days + PG_EPOCH_DAYS)
1117
+ },
1118
+ (value) => {
1119
+ const bytes = new Uint8Array(4)
1120
+ writeInt32(bytes, 0, dateDays(value))
1121
+ return bytes
1122
+ },
1123
+ (sink, value) => sink.int32(dateDays(value))
1124
+ )
1125
+
1126
+ const timetzCodec: UnsafeCodec<any> = codecOf(
1127
+ (bytes, offset, size) => {
1128
+ requireSize(size, 12, "timetz")
1129
+ const zone = readInt32(bytes, offset + 8)
1130
+ if (zone <= -TZDISP_LIMIT_SECONDS || zone >= TZDISP_LIMIT_SECONDS) {
1131
+ return fail(`timetz time zone displacement out of range: ${zone}`)
1132
+ }
1133
+ return formatTimeOfDay(readTimeMicros(bytes, offset)) + formatZone(-zone)
1134
+ },
1135
+ (value) => {
1136
+ const text = requireString(value, "timetz")
1137
+ const split = Math.max(text.lastIndexOf("+"), text.lastIndexOf("-"), text.lastIndexOf("Z"))
1138
+ if (split <= 0) {
1139
+ return fail(`Expected a timetz with a zone offset, received "${text}"`)
1140
+ }
1141
+ const bytes = new Uint8Array(12)
1142
+ writeInt64(bytes, 0, parseTimeOfDay(text.slice(0, split)))
1143
+ writeInt32(bytes, 8, -parseZone(text.slice(split)))
1144
+ return bytes
1145
+ }
1146
+ )
1147
+
1148
+ const jsonCodec: UnsafeCodec<any> = codecOf(
1149
+ (bytes, offset, size) => JSON.parse(decodeUtf8(bytes, offset, size)),
1150
+ (value) => {
1151
+ const text = JSON.stringify(value)
1152
+ if (text === undefined) return fail("Value cannot be serialised as JSON")
1153
+ return encodeUtf8(text)
1154
+ },
1155
+ (sink, value) => {
1156
+ const text = JSON.stringify(value)
1157
+ if (text === undefined) return fail("Value cannot be serialised as JSON")
1158
+ sink.utf8(text)
1159
+ }
1160
+ )
1161
+
1162
+ const jsonbCodec: UnsafeCodec<any> = codecOf(
1163
+ (bytes, offset, size) => {
1164
+ if (size === 0 || bytes[offset] !== 1) {
1165
+ return fail("Unsupported jsonb version byte")
1166
+ }
1167
+ return JSON.parse(decodeUtf8(bytes, offset + 1, size - 1))
1168
+ },
1169
+ (value) => {
1170
+ const text = JSON.stringify(value)
1171
+ if (text === undefined) return fail("Value cannot be serialised as JSON")
1172
+ const bytes = encodeUtf8Prefixed(text, 1)
1173
+ bytes[0] = 1
1174
+ return bytes
1175
+ },
1176
+ (sink, value) => {
1177
+ const text = JSON.stringify(value)
1178
+ if (text === undefined) return fail("Value cannot be serialised as JSON")
1179
+ sink.uint8(1)
1180
+ sink.utf8(text)
1181
+ }
1182
+ )
1183
+
1184
+ const builtins = new Map<number, UnsafeCodec<any>>([
1185
+ [
1186
+ OID.bool,
1187
+ codecOf(
1188
+ (bytes, offset, size) => {
1189
+ requireSize(size, 1, "bool")
1190
+ return bytes[offset] !== 0
1191
+ },
1192
+ (value) => {
1193
+ if (typeof value !== "boolean") fail("Expected a boolean for bool")
1194
+ const bytes = new Uint8Array(1)
1195
+ bytes[0] = value ? 1 : 0
1196
+ return bytes
1197
+ },
1198
+ (sink, value) => {
1199
+ if (typeof value !== "boolean") fail("Expected a boolean for bool")
1200
+ sink.uint8(value ? 1 : 0)
1201
+ }
1202
+ )
1203
+ ],
1204
+ [
1205
+ OID.bytea,
1206
+ codecOf(
1207
+ region,
1208
+ (value) => value instanceof Uint8Array ? value.slice() : fail("Expected a Uint8Array for bytea"),
1209
+ (sink, value) => value instanceof Uint8Array ? sink.raw(value) : fail("Expected a Uint8Array for bytea")
1210
+ )
1211
+ ],
1212
+ [
1213
+ OID.int2,
1214
+ codecOf(
1215
+ (bytes, offset, size) => {
1216
+ requireSize(size, 2, "int2")
1217
+ return ((bytes[offset] << 8) | bytes[offset + 1]) << 16 >> 16
1218
+ },
1219
+ (value) => {
1220
+ const num = requireInteger(value, "int2", -32768, 32767)
1221
+ const bytes = new Uint8Array(2)
1222
+ bytes[0] = num >>> 8
1223
+ bytes[1] = num
1224
+ return bytes
1225
+ },
1226
+ (sink, value) => sink.int16(requireInteger(value, "int2", -32768, 32767))
1227
+ )
1228
+ ],
1229
+ [
1230
+ OID.int4,
1231
+ codecOf(
1232
+ (bytes, offset, size) => {
1233
+ requireSize(size, 4, "int4")
1234
+ return readInt32(bytes, offset)
1235
+ },
1236
+ (value) => {
1237
+ const num = requireInteger(value, "int4", INT32_MIN, INT32_MAX)
1238
+ const bytes = new Uint8Array(4)
1239
+ bytes[0] = num >>> 24
1240
+ bytes[1] = num >>> 16
1241
+ bytes[2] = num >>> 8
1242
+ bytes[3] = num
1243
+ return bytes
1244
+ },
1245
+ (sink, value) => sink.int32(requireInteger(value, "int4", INT32_MIN, INT32_MAX))
1246
+ )
1247
+ ],
1248
+ [
1249
+ OID.oid,
1250
+ codecOf(
1251
+ (bytes, offset, size) => {
1252
+ requireSize(size, 4, "oid")
1253
+ return readUint32(bytes, offset)
1254
+ },
1255
+ (value) => {
1256
+ const num = requireInteger(value, "oid", 0, 4294967295)
1257
+ const bytes = new Uint8Array(4)
1258
+ bytes[0] = num >>> 24
1259
+ bytes[1] = num >>> 16
1260
+ bytes[2] = num >>> 8
1261
+ bytes[3] = num
1262
+ return bytes
1263
+ },
1264
+ (sink, value) => sink.int32(requireInteger(value, "oid", 0, 4294967295))
1265
+ )
1266
+ ],
1267
+ [
1268
+ OID.int8,
1269
+ codecOf(
1270
+ (bytes, offset, size) => {
1271
+ requireSize(size, 8, "int8")
1272
+ stage8(bytes, offset)
1273
+ return scratchView8.getBigInt64(0)
1274
+ },
1275
+ (value) => {
1276
+ scratchView8.setBigInt64(0, int8Value(value))
1277
+ return takeScratch8()
1278
+ },
1279
+ (sink, value) => sink.bigInt64(int8Value(value))
1280
+ )
1281
+ ],
1282
+ [
1283
+ OID.float4,
1284
+ codecOf(
1285
+ (bytes, offset, size) => {
1286
+ requireSize(size, 4, "float4")
1287
+ scratchView4.setInt32(0, readInt32(bytes, offset))
1288
+ return scratchView4.getFloat32(0)
1289
+ },
1290
+ (value) => {
1291
+ scratchView4.setFloat32(0, requireNumber(value, "float4"))
1292
+ return takeScratch4()
1293
+ },
1294
+ (sink, value) => sink.float32(requireNumber(value, "float4"))
1295
+ )
1296
+ ],
1297
+ [
1298
+ OID.float8,
1299
+ codecOf(
1300
+ (bytes, offset, size) => {
1301
+ requireSize(size, 8, "float8")
1302
+ stage8(bytes, offset)
1303
+ return scratchView8.getFloat64(0)
1304
+ },
1305
+ (value) => {
1306
+ scratchView8.setFloat64(0, requireNumber(value, "float8"))
1307
+ return takeScratch8()
1308
+ },
1309
+ (sink, value) => sink.float64(requireNumber(value, "float8"))
1310
+ )
1311
+ ],
1312
+ [
1313
+ OID.time,
1314
+ codecOf(
1315
+ (bytes, offset, size) => {
1316
+ requireSize(size, 8, "time")
1317
+ stage8(bytes, offset)
1318
+ return scratchView8.getBigInt64(0)
1319
+ },
1320
+ (value) => {
1321
+ scratchView8.setBigInt64(0, timeValue(value))
1322
+ return takeScratch8()
1323
+ },
1324
+ (sink, value) => sink.bigInt64(timeValue(value))
1325
+ )
1326
+ ],
1327
+ [OID.numeric, codecOf(decodeNumeric, encodeNumeric)],
1328
+ [OID.text, utf8Codec],
1329
+ [OID.varchar, utf8Codec],
1330
+ [OID.bpchar, utf8Codec],
1331
+ [OID.name, utf8Codec],
1332
+ [OID.json, jsonCodec],
1333
+ [OID.jsonb, jsonbCodec],
1334
+ [OID.uuid, codecOf(decodeUuid, encodeUuid, writeUuid)],
1335
+ [OID.inet, codecOf(decodeInet, (value) => encodeInet(value, false))],
1336
+ [OID.cidr, codecOf(decodeInet, (value) => encodeInet(value, true))],
1337
+ [OID.date, dateCodec],
1338
+ [OID.timetz, timetzCodec],
1339
+ [OID.timestamp, timestampCodec],
1340
+ [OID.timestamptz, timestampCodec]
1341
+ ])
1342
+
1343
+ for (const [arrayOid, elementOid] of arrayToElement) {
1344
+ builtins.set(
1345
+ arrayOid,
1346
+ codecOf(
1347
+ (bytes, offset, size) => decodeArray(bytes, offset, size, elementOid),
1348
+ (value) => encodeArray(value, elementOid),
1349
+ (sink, value) => writeArray(sink, value, elementOid)
1350
+ )
1351
+ )
1352
+ }
1353
+
1354
+ /**
1355
+ * Built-ins with registered codecs layered over them, so `encode` and `decode`
1356
+ * resolve an OID with a single lookup.
1357
+ */
1358
+ const codecs = new Map<number, UnsafeCodec<any>>(builtins)
1359
+
1360
+ /**
1361
+ * Every built-in OID is below this, and PostgreSQL hands user-defined types
1362
+ * OIDs from 16384 up, so a direct table covers the common case and the map
1363
+ * covers registered ones.
1364
+ */
1365
+ const tableSize = 4096
1366
+
1367
+ const table = new Array<UnsafeCodec<any> | undefined>(tableSize)
1368
+ for (const [oid, codec] of codecs) table[oid] = codec
1369
+
1370
+ const lookup = (oid: number): UnsafeCodec<any> | undefined => oid >= 0 && oid < tableSize ? table[oid] : codecs.get(oid)
1371
+
1372
+ /**
1373
+ * Registers a binary codec for an OID the built-in catalogue does not cover,
1374
+ * or overrides a built-in one. Registered codecs take precedence.
1375
+ *
1376
+ * @category registry
1377
+ * @since 4.0.0
1378
+ */
1379
+ export const register = <A>(oid: number, codec: Codec<A>): void => {
1380
+ const unsafe: UnsafeCodec<A> = {
1381
+ encode(value) {
1382
+ const encoded = codec.encode(value)
1383
+ if (Result.isFailure(encoded)) throw encoded.failure
1384
+ return encoded.success
1385
+ },
1386
+ decode(bytes) {
1387
+ const decoded = codec.decode(bytes)
1388
+ if (Result.isFailure(decoded)) throw decoded.failure
1389
+ return decoded.success
1390
+ },
1391
+ ...(codec.write === undefined ? undefined : {
1392
+ write(sink: ValueSink, value: A) {
1393
+ const written = codec.write!(sink, value)
1394
+ if (Result.isFailure(written)) throw written.failure
1395
+ }
1396
+ }),
1397
+ ...(codec.read === undefined ? undefined : {
1398
+ read(bytes: Uint8Array, offset: number, size: number) {
1399
+ const decoded = codec.read!(bytes, offset, size)
1400
+ if (Result.isFailure(decoded)) throw decoded.failure
1401
+ return decoded.success
1402
+ }
1403
+ })
1404
+ }
1405
+ codecs.set(oid, unsafe)
1406
+ if (oid >= 0 && oid < tableSize) table[oid] = unsafe
1407
+ }
1408
+
1409
+ /**
1410
+ * Removes a previously registered codec.
1411
+ *
1412
+ * @category registry
1413
+ * @since 4.0.0
1414
+ */
1415
+ export const unregister = (oid: number): void => {
1416
+ const builtin = builtins.get(oid)
1417
+ if (builtin === undefined) codecs.delete(oid)
1418
+ else codecs.set(oid, builtin)
1419
+ if (oid >= 0 && oid < tableSize) table[oid] = builtin
1420
+ }
1421
+
1422
+ // -----------------------------------------------------------------------------
1423
+ // arrays
1424
+ // -----------------------------------------------------------------------------
1425
+
1426
+ const encodeArray = (value: unknown, elementOid: number): Uint8Array => {
1427
+ if (!Array.isArray(value)) {
1428
+ return fail("Expected an array")
1429
+ }
1430
+ const codec = lookup(elementOid)
1431
+ if (codec === undefined) return fail(`No codec registered for OID ${elementOid}`)
1432
+ const count = value.length
1433
+ const elements: Array<Uint8Array | null> = new Array(count)
1434
+ let hasNull = false
1435
+ let payloadSize = 0
1436
+ for (let i = 0; i < count; i++) {
1437
+ const element = value[i]
1438
+ if (element === null) {
1439
+ elements[i] = null
1440
+ hasNull = true
1441
+ payloadSize += 4
1442
+ } else {
1443
+ const encoded = codec.encode(element)
1444
+ elements[i] = encoded
1445
+ payloadSize += 4 + encoded.length
1446
+ }
1447
+ }
1448
+ const dimensions = count === 0 ? 0 : 1
1449
+ const bytes = new Uint8Array(12 + dimensions * 8 + payloadSize)
1450
+ writeInt32(bytes, 0, dimensions)
1451
+ writeInt32(bytes, 4, hasNull ? 1 : 0)
1452
+ writeInt32(bytes, 8, elementOid)
1453
+ let offset = 12
1454
+ if (dimensions === 1) {
1455
+ writeInt32(bytes, offset, count)
1456
+ writeInt32(bytes, offset + 4, 1)
1457
+ offset += 8
1458
+ }
1459
+ for (let i = 0; i < count; i++) {
1460
+ const element = elements[i]
1461
+ if (element === null) {
1462
+ writeInt32(bytes, offset, -1)
1463
+ offset += 4
1464
+ } else {
1465
+ writeInt32(bytes, offset, element.length)
1466
+ bytes.set(element, offset + 4)
1467
+ offset += 4 + element.length
1468
+ }
1469
+ }
1470
+ return bytes
1471
+ }
1472
+
1473
+ /**
1474
+ * As `encodeArray`, but into a sink: each element is framed with `beginLength`
1475
+ * and written in place, so neither the elements nor the array itself needs an
1476
+ * array of bytes of its own.
1477
+ */
1478
+ const writeArray = (sink: ValueSink, value: unknown, elementOid: number): void => {
1479
+ if (!Array.isArray(value)) {
1480
+ return fail("Expected an array")
1481
+ }
1482
+ const count = value.length
1483
+ // The null flag sits ahead of the elements, so it is the one thing that has
1484
+ // to be known before any of them are written.
1485
+ let hasNull = false
1486
+ for (let i = 0; i < count; i++) {
1487
+ if (value[i] === null) {
1488
+ hasNull = true
1489
+ break
1490
+ }
1491
+ }
1492
+ const dimensions = count === 0 ? 0 : 1
1493
+ sink.int32(dimensions)
1494
+ sink.int32(hasNull ? 1 : 0)
1495
+ sink.int32(elementOid)
1496
+ if (dimensions === 1) {
1497
+ sink.int32(count)
1498
+ sink.int32(1)
1499
+ }
1500
+ // One lookup for the whole array. A codec with no writer of its own is left
1501
+ // to `writeValue`, which allocates for the element anyway.
1502
+ const write = lookup(elementOid)?.write
1503
+ for (let i = 0; i < count; i++) {
1504
+ const element = value[i]
1505
+ if (element === null) {
1506
+ sink.int32(-1)
1507
+ } else {
1508
+ const token = sink.beginLength()
1509
+ if (write === undefined) writeValue(sink, element, elementOid)
1510
+ else write(sink, element)
1511
+ sink.endLength(token)
1512
+ }
1513
+ }
1514
+ }
1515
+
1516
+ /**
1517
+ * The element codec is resolved once for the whole array, and read in place
1518
+ * where it can be, so a thousand elements cost a thousand reads rather than a
1519
+ * thousand lookups and a thousand views.
1520
+ */
1521
+ const decodeArray = (
1522
+ bytes: Uint8Array,
1523
+ start: number,
1524
+ size: number,
1525
+ elementOid: number
1526
+ ): ReadonlyArray<unknown> => {
1527
+ if (size < 12) return fail("Truncated array value")
1528
+ const dimensions = readInt32(bytes, start)
1529
+ const wireElementOid = readUint32(bytes, start + 8)
1530
+ if (wireElementOid !== elementOid) {
1531
+ return fail(`Array element OID ${wireElementOid} does not match expected OID ${elementOid}`)
1532
+ }
1533
+ if (dimensions === 0) {
1534
+ if (size !== 12) return fail("Zero-dimensional array has trailing bytes")
1535
+ return []
1536
+ }
1537
+ if (dimensions !== 1) {
1538
+ return fail(`Only 1-dimensional arrays are supported, received ${dimensions} dimensions`)
1539
+ }
1540
+ if (size < 20) return fail("Truncated array value")
1541
+ const length = readInt32(bytes, start + 12)
1542
+ if (length < 0) return fail(`Invalid array length: ${length}`)
1543
+ const lowerBound = readInt32(bytes, start + 16)
1544
+ if (lowerBound !== 1) return fail(`Only arrays with a lower bound of 1 are supported, received ${lowerBound}`)
1545
+ const codec = lookup(elementOid)
1546
+ const read = codec?.read
1547
+ const values: Array<unknown> = new Array(length)
1548
+ const limit = start + size
1549
+ let offset = start + 20
1550
+ for (let i = 0; i < length; i++) {
1551
+ if (offset + 4 > limit) return fail("Truncated array element")
1552
+ const elementSize = readInt32(bytes, offset)
1553
+ offset += 4
1554
+ if (elementSize < -1) return fail(`Invalid array element length: ${elementSize}`)
1555
+ if (elementSize === -1) {
1556
+ values[i] = null
1557
+ } else {
1558
+ if (offset + elementSize > limit) return fail("Truncated array element")
1559
+ values[i] = read !== undefined
1560
+ ? read(bytes, offset, elementSize)
1561
+ : codec === undefined
1562
+ ? region(bytes, offset, elementSize)
1563
+ : codec.decode(region(bytes, offset, elementSize))
1564
+ offset += elementSize
1565
+ }
1566
+ }
1567
+ if (offset !== limit) return fail("Array value has trailing bytes")
1568
+ return values
1569
+ }
1570
+
1571
+ // -----------------------------------------------------------------------------
1572
+ // entry points
1573
+ // -----------------------------------------------------------------------------
1574
+
1575
+ /**
1576
+ * A result column, as `RowDescription` describes one.
1577
+ *
1578
+ * @category models
1579
+ * @since 4.0.0
1580
+ */
1581
+ export interface Column {
1582
+ readonly dataTypeOid: number
1583
+ readonly format: number
1584
+ }
1585
+
1586
+ /**
1587
+ * Builds a field reader for `PgProtocol.makeParser`, so a result's rows decode
1588
+ * as they are parsed rather than through a view per column.
1589
+ *
1590
+ * Every column is resolved once here rather than once per row, and a codec that
1591
+ * can read in place does; the rest are handed a view. SQL NULL reads as `null`,
1592
+ * and a column whose OID has no codec reads as a copy of its bytes.
1593
+ *
1594
+ * Only the binary format is supported, and a text column returns a
1595
+ * `CodecError` failure here rather than once per row. A successful result
1596
+ * contains the parser's internal throwing fast path; its failures are terminal
1597
+ * for that parser. The standalone `encode` and `decode` APIs remain typed
1598
+ * `Result` values.
1599
+ *
1600
+ * ```ts
1601
+ * import { PgProtocol, PgTypes } from "@effect/sql-pg"
1602
+ *
1603
+ * const parser = PgProtocol.makeParser({ readField: Result.getOrThrow(PgTypes.makeFieldReader([])) })
1604
+ * // on each RowDescription
1605
+ * declare const description: PgProtocol.RowDescription
1606
+ * parser.readField = Result.getOrThrow(PgTypes.makeFieldReader(description.fields))
1607
+ * ```
1608
+ *
1609
+ * @category decoding
1610
+ * @since 4.0.0
1611
+ */
1612
+ export const makeFieldReader = (
1613
+ columns: ReadonlyArray<Column>
1614
+ ): Result.Result<PgProtocol.FieldReader<unknown>, CodecError> =>
1615
+ result(() => {
1616
+ const codecs = columns.map((column, index) => {
1617
+ if (column.format !== 1) {
1618
+ return fail(`Only the binary format is supported, column ${index} has format ${column.format}`)
1619
+ }
1620
+ return lookup(column.dataTypeOid)
1621
+ })
1622
+ return (bytes: Uint8Array, offset: number, size: number, column: number): unknown => {
1623
+ if (size < 0) return null
1624
+ const codec = codecs[column]
1625
+ if (codec === undefined) return bytes.slice(offset, offset + size)
1626
+ const read = codec.read
1627
+ return read === undefined ? codec.decode(bytes.subarray(offset, offset + size)) : read(bytes, offset, size)
1628
+ }
1629
+ })
1630
+
1631
+ /**
1632
+ * Encodes a JavaScript value as the binary representation of the given OID.
1633
+ *
1634
+ * Returns a `CodecError` failure when the value has the wrong JavaScript type,
1635
+ * or when the OID is neither built in nor registered.
1636
+ *
1637
+ * @category encoding
1638
+ * @since 4.0.0
1639
+ */
1640
+ export const encode = (value: unknown, oid: number): Result.Result<Uint8Array, CodecError> =>
1641
+ result(() => {
1642
+ const codec = lookup(oid)
1643
+ if (codec === undefined) return fail(`No codec registered for OID ${oid}`)
1644
+ return codec.encode(value)
1645
+ })
1646
+
1647
+ /**
1648
+ * Decodes the binary representation of the given OID.
1649
+ *
1650
+ * `format` must be `1`; the text format is not implemented. An OID that is
1651
+ * neither built in nor registered decodes to the raw bytes.
1652
+ *
1653
+ * @category decoding
1654
+ * @since 4.0.0
1655
+ */
1656
+ export const decode = (
1657
+ bytes: Uint8Array,
1658
+ oid: number,
1659
+ format: number
1660
+ ): Result.Result<unknown, CodecError> =>
1661
+ result(() => {
1662
+ if (format !== 1) {
1663
+ return fail(`Only the binary format is supported, received format ${format}`)
1664
+ }
1665
+ const codec = lookup(oid)
1666
+ return codec === undefined ? bytes : codec.decode(bytes)
1667
+ })
1668
+
1669
+ // -----------------------------------------------------------------------------
1670
+ // parameter constructors
1671
+ // -----------------------------------------------------------------------------
1672
+
1673
+ /**
1674
+ * A value paired with the OID it should be encoded as.
1675
+ *
1676
+ * @category models
1677
+ * @since 4.0.0
1678
+ */
1679
+ export interface Parameter {
1680
+ readonly oid: number
1681
+ readonly value: unknown
1682
+ }
1683
+
1684
+ /**
1685
+ * Encodes a parameter for a `Bind` message. SQL NULL stays `null`.
1686
+ *
1687
+ * @category encoding
1688
+ * @since 4.0.0
1689
+ */
1690
+ export const encodeParameter = (parameter: Parameter): Result.Result<Uint8Array | null, CodecError> =>
1691
+ parameter.value === null ? Result.succeed(null) : encode(parameter.value, parameter.oid)
1692
+
1693
+ const writeValue = (sink: ValueSink, value: unknown, oid: number): void => {
1694
+ const codec = lookup(oid)
1695
+ if (codec === undefined) return fail(`No codec registered for OID ${oid}`)
1696
+ if (codec.write === undefined) sink.raw(codec.encode(value))
1697
+ else codec.write(sink, value)
1698
+ }
1699
+
1700
+ /**
1701
+ * Writes a parameter into a `Bind` frame, for `PgProtocol.makeBindEncoder`.
1702
+ * Codecs that can write their bytes in place do; the rest fall back to
1703
+ * `encode` and a copy.
1704
+ *
1705
+ * @category encoding
1706
+ * @since 4.0.0
1707
+ */
1708
+ const writeParameterUnsafe = (sink: ValueSink, parameter: Parameter): void =>
1709
+ parameter.value === null ? sink.sqlNull() : writeValue(sink, parameter.value, parameter.oid)
1710
+
1711
+ export const writeParameter = (
1712
+ sink: ValueSink,
1713
+ parameter: Parameter
1714
+ ): Result.Result<void, CodecError> => {
1715
+ try {
1716
+ writeParameterUnsafe(sink, parameter)
1717
+ return Result.void
1718
+ } catch (error) {
1719
+ if (error instanceof CodecError) return Result.fail(error)
1720
+ throw error
1721
+ }
1722
+ }
1723
+
1724
+ const valueWriterUnsafe = Symbol.for("@effect/sql-pg/PgProtocol/ValueWriter/unsafe")
1725
+ Object.defineProperty(writeParameter, valueWriterUnsafe, { value: writeParameterUnsafe })
1726
+
1727
+ const parameter = (oid: number) => (value: unknown): Parameter => ({ oid, value })
1728
+
1729
+ /**
1730
+ * A `bool` parameter.
1731
+ *
1732
+ * @category constructors
1733
+ * @since 4.0.0
1734
+ */
1735
+ export const bool: (value: boolean | null) => Parameter = parameter(OID.bool)
1736
+
1737
+ /**
1738
+ * An `int2` parameter.
1739
+ *
1740
+ * @category constructors
1741
+ * @since 4.0.0
1742
+ */
1743
+ export const int2: (value: number | null) => Parameter = parameter(OID.int2)
1744
+
1745
+ /**
1746
+ * An `int4` parameter.
1747
+ *
1748
+ * @category constructors
1749
+ * @since 4.0.0
1750
+ */
1751
+ export const int4: (value: number | null) => Parameter = parameter(OID.int4)
1752
+
1753
+ /**
1754
+ * An `int8` parameter.
1755
+ *
1756
+ * @category constructors
1757
+ * @since 4.0.0
1758
+ */
1759
+ export const int8: (value: bigint | null) => Parameter = parameter(OID.int8)
1760
+
1761
+ /**
1762
+ * An `oid` parameter.
1763
+ *
1764
+ * @category constructors
1765
+ * @since 4.0.0
1766
+ */
1767
+ export const oid: (value: number | null) => Parameter = parameter(OID.oid)
1768
+
1769
+ /**
1770
+ * A `float4` parameter.
1771
+ *
1772
+ * @category constructors
1773
+ * @since 4.0.0
1774
+ */
1775
+ export const float4: (value: number | null) => Parameter = parameter(OID.float4)
1776
+
1777
+ /**
1778
+ * A `float8` parameter.
1779
+ *
1780
+ * @category constructors
1781
+ * @since 4.0.0
1782
+ */
1783
+ export const float8: (value: number | null) => Parameter = parameter(OID.float8)
1784
+
1785
+ /**
1786
+ * A `numeric` parameter, given as a decimal string or `"NaN"`.
1787
+ *
1788
+ * @category constructors
1789
+ * @since 4.0.0
1790
+ */
1791
+ export const numeric: (value: string | null) => Parameter = parameter(OID.numeric)
1792
+
1793
+ /**
1794
+ * A `text` parameter.
1795
+ *
1796
+ * @category constructors
1797
+ * @since 4.0.0
1798
+ */
1799
+ export const text: (value: string | null) => Parameter = parameter(OID.text)
1800
+
1801
+ /**
1802
+ * A `varchar` parameter.
1803
+ *
1804
+ * @category constructors
1805
+ * @since 4.0.0
1806
+ */
1807
+ export const varchar: (value: string | null) => Parameter = parameter(OID.varchar)
1808
+
1809
+ /**
1810
+ * A `bpchar` parameter.
1811
+ *
1812
+ * @category constructors
1813
+ * @since 4.0.0
1814
+ */
1815
+ export const bpchar: (value: string | null) => Parameter = parameter(OID.bpchar)
1816
+
1817
+ /**
1818
+ * A `name` parameter.
1819
+ *
1820
+ * @category constructors
1821
+ * @since 4.0.0
1822
+ */
1823
+ export const name: (value: string | null) => Parameter = parameter(OID.name)
1824
+
1825
+ /**
1826
+ * A `bytea` parameter.
1827
+ *
1828
+ * @category constructors
1829
+ * @since 4.0.0
1830
+ */
1831
+ export const bytea: (value: Uint8Array | null) => Parameter = parameter(OID.bytea)
1832
+
1833
+ /**
1834
+ * A `json` parameter.
1835
+ *
1836
+ * @category constructors
1837
+ * @since 4.0.0
1838
+ */
1839
+ export const json: (value: unknown) => Parameter = parameter(OID.json)
1840
+
1841
+ /**
1842
+ * A `jsonb` parameter.
1843
+ *
1844
+ * @category constructors
1845
+ * @since 4.0.0
1846
+ */
1847
+ export const jsonb: (value: unknown) => Parameter = parameter(OID.jsonb)
1848
+
1849
+ /**
1850
+ * A `uuid` parameter.
1851
+ *
1852
+ * @category constructors
1853
+ * @since 4.0.0
1854
+ */
1855
+ export const uuid: (value: string | null) => Parameter = parameter(OID.uuid)
1856
+
1857
+ /**
1858
+ * An `inet` parameter, such as `"10.0.0.1"` or `"10.0.0.0/8"`.
1859
+ *
1860
+ * @category constructors
1861
+ * @since 4.0.0
1862
+ */
1863
+ export const inet: (value: string | null) => Parameter = parameter(OID.inet)
1864
+
1865
+ /**
1866
+ * A `cidr` parameter.
1867
+ *
1868
+ * @category constructors
1869
+ * @since 4.0.0
1870
+ */
1871
+ export const cidr: (value: string | null) => Parameter = parameter(OID.cidr)
1872
+
1873
+ /**
1874
+ * A `date` parameter, given as `YYYY-MM-DD`, `"infinity"`, or `"-infinity"`.
1875
+ *
1876
+ * @category constructors
1877
+ * @since 4.0.0
1878
+ */
1879
+ export const date: (value: string | null) => Parameter = parameter(OID.date)
1880
+
1881
+ /**
1882
+ * A `time` parameter, given as microseconds since midnight.
1883
+ *
1884
+ * @category constructors
1885
+ * @since 4.0.0
1886
+ */
1887
+ export const time: (value: bigint | null) => Parameter = parameter(OID.time)
1888
+
1889
+ /**
1890
+ * A `timetz` parameter, such as `"12:34:56+02:00"`.
1891
+ *
1892
+ * @category constructors
1893
+ * @since 4.0.0
1894
+ */
1895
+ export const timetz: (value: string | null) => Parameter = parameter(OID.timetz)
1896
+
1897
+ /**
1898
+ * A `timestamp` parameter, given as Unix epoch milliseconds and interpreted
1899
+ * as UTC.
1900
+ *
1901
+ * @category constructors
1902
+ * @since 4.0.0
1903
+ */
1904
+ export const timestamp: (value: number | null) => Parameter = parameter(OID.timestamp)
1905
+
1906
+ /**
1907
+ * A `timestamptz` parameter, given as Unix epoch milliseconds.
1908
+ *
1909
+ * @category constructors
1910
+ * @since 4.0.0
1911
+ */
1912
+ export const timestamptz: (value: number | null) => Parameter = parameter(OID.timestamptz)
1913
+
1914
+ /**
1915
+ * A one-dimensional array parameter whose elements have the given OID.
1916
+ *
1917
+ * @category constructors
1918
+ * @since 4.0.0
1919
+ */
1920
+ export const array = (
1921
+ values: ReadonlyArray<unknown> | null,
1922
+ elementOid: number
1923
+ ): Result.Result<Parameter, CodecError> =>
1924
+ result(() => {
1925
+ const arrayOid = elementToArray.get(elementOid)
1926
+ if (arrayOid === undefined) {
1927
+ return fail(`No array type known for element OID ${elementOid}`)
1928
+ }
1929
+ return { oid: arrayOid, value: values }
1930
+ })