@amritk/nish-x86_64-linux 0.30.0 → 0.32.0

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.
@@ -59,8 +59,9 @@
59
59
  * does not need: a program prepares a statement and executes it in the same
60
60
  * flight.
61
61
  *
62
- * **Text results, no copies.** Every parameter is in text format, and so is
63
- * every result column unless the bind asks otherwise. A row is read where it
62
+ * **Text results, no copies.** Every parameter is in text format unless it
63
+ * is one of the binary integer arrays below, and every result column is text
64
+ * unless the bind asks otherwise. A row is read where it
64
65
  * lies in the input buffer: `next()` frames each message once, validates its
65
66
  * layout, and records each column's start and length in two reused arrays, so
66
67
  * a row costs no allocation. `int`, `bool`, `isNull` and `PgText.timestamp`
@@ -72,6 +73,21 @@
72
73
  * Bind, so a list needs no string built first. A program whose element was
73
74
  * refused for room can drop the whole bind, unsent, with `bindCancel`.
74
75
  *
76
+ * **Binary integer arrays.** `paramInt4Array` and `paramInt8Array` send an
77
+ * `i64[]` window as an `int4[]` or `int8[]` in PostgreSQL's binary layout:
78
+ * four or eight bytes an element and no digits to write or parse, which the
79
+ * benchmark (`bench/postgres/arrays.mjs`) measured at about a tenth of the
80
+ * text encoder's client CPU, and at 1,000 elements a 51% cut in the server's
81
+ * on `unnest` for `int8[]`, 30% for `int4[]`.
82
+ * `binaryArrayStart(oid)`, `binaryArrayInt` and `binaryArrayNull` build one
83
+ * element at a time, NULLs included, and `arrayEnd` closes it. The Bind's
84
+ * format codes stay a single zero word while every parameter is text; the
85
+ * first binary one makes `bindEnd` write a code for each, so an all-text bind
86
+ * costs nothing more. An `int4` element outside `int4` panics rather than
87
+ * wrapping. The element type must match the statement's parameter: the server
88
+ * answers a mismatch with `ErrorResponse` (42804) for that execution, and the
89
+ * connection goes on.
90
+ *
75
91
  * **Binary results.** A bind opened with `bindStartBinary` instead of
76
92
  * `bindStart` asks for every result column in binary format: an `int8` is
77
93
  * eight bytes, a `timestamptz` eight bytes of microseconds since 2000, a
@@ -133,7 +149,8 @@
133
149
  * **Batching.** `PgBatcher` turns many reads of one row shape by key — a user
134
150
  * by id, an article by slug — into one execution of a statement over
135
151
  * `unnest($1) with ordinality`. `addInt` or `addText` queues a request's key
136
- * and tag; `take(conn)` binds the oldest waiting, each distinct key once, up
152
+ * and tag; `take(conn)` binds the oldest waiting, each distinct key once (past
153
+ * `PG_BATCH_PROBES` slots of its table probe, a key is bound again), up
137
154
  * to `maxRequests` and `maxBytes`, and answers a run's handle; `row`, `end`
138
155
  * and `fail` take that execution's events (a handle already answered makes
139
156
  * them a no-op counted in `stray`, so a server's late event cannot panic),
@@ -159,7 +176,7 @@
159
176
  * Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
160
177
  * and RFC 5802 and RFC 7677; not ported from another implementation.
161
178
  */
162
- import { copyBytes, getF64BE, getI32BE, getI64BE, getU16BE, setI32BE, setU16BE } from "nish/bytes"
179
+ import { copyBytes, getF64BE, getI32BE, getI64BE, getU16BE, setI32BE, setI64BE, setU16BE } from "nish/bytes"
163
180
  import { base64StdPadDecode, base64StdPadEncode } from "nish/crypto/base64url"
164
181
  import { timingSafeEqual } from "nish/crypto/ct"
165
182
  import { hmacSha256, pbkdf2HmacSha256 } from "nish/crypto/hmac"
@@ -256,7 +273,8 @@ const PG_TAIL: i32 = 17
256
273
  const PG_BINARY_TAIL: i32 = 2
257
274
  // The result format code for binary.
258
275
  const PG_FORMAT_BINARY: i32 = 1
259
- // The element types `PgBinaryArray` reads, by OID: int8, int2 and int4.
276
+ // The element types `PgBinaryArray` reads, and the binary array parameters
277
+ // write (int4 and int8), by OID: int8, int2 and int4.
260
278
  const PG_OID_INT8: i32 = 20
261
279
  const PG_OID_INT2: i32 = 21
262
280
  const PG_OID_INT4: i32 = 23
@@ -436,6 +454,16 @@ const pgDecimalLength = (value: i64): i32 => {
436
454
  return n
437
455
  }
438
456
 
457
+ /** Whether `value` is an `int4`, -2^31..2^31-1. */
458
+ const pgFitsInt4 = (value: i64): boolean => value >= toI64(-2147483648) && value <= toI64(2147483647)
459
+
460
+ /** Panics unless `values[start, start + count)` is inside `values`. */
461
+ const pgIntWindow = (values: i64[], start: i32, count: i32): void => {
462
+ if (start < 0 || count < 0 || count > toI32(values.length) - start) {
463
+ panic("nish/net/postgres: an int array parameter outside its array")
464
+ }
465
+ }
466
+
439
467
  /** Whether `text` holds a NUL byte, which a protocol string cannot. */
440
468
  const pgHasNul = (text: string): boolean => {
441
469
  const n: i32 = toI32(text.length)
@@ -1211,9 +1239,19 @@ export class PgConnection {
1211
1239
  private bindAt: i32 = -1
1212
1240
  private paramCountAt: i32 = 0
1213
1241
  private paramCount: i32 = 0
1242
+ // What the open bind's end owes beyond `PG_TAIL`: its binary result format
1243
+ // code, and once it holds a binary parameter, a format code per parameter.
1244
+ private bindOwed: i32 = 0
1245
+ // The open bind's binary parameters, by index; `bindEnd` back-patches their format codes.
1246
+ private binaryParams: i32[]
1247
+ private binaryParamCount: i32 = 0
1214
1248
  // The open array parameter's length word, -1 when none is open, and its elements so far.
1215
1249
  private arrayAt: i32 = -1
1216
1250
  private arrayCount: i32 = 0
1251
+ // The open array's element OID when it is binary (`binaryArrayStart`), 0 when it is a text literal.
1252
+ private arrayOid: i32 = 0
1253
+ // Whether the open binary array holds a NULL element.
1254
+ private arrayNulls: boolean = false
1217
1255
  private state: i32 = 0
1218
1256
  private clientNonce: string = ""
1219
1257
  private serverSignature: u8[]
@@ -1239,6 +1277,7 @@ export class PgConnection {
1239
1277
  this.colStarts = new Array<i32>(16)
1240
1278
  this.colLengths = new Array<i32>(16)
1241
1279
  this.colTypes = new Array<i32>(16)
1280
+ this.binaryParams = []
1242
1281
  this.serverSignature = []
1243
1282
  const head: u8[] = []
1244
1283
  for (let i: i32 = 0; i < PG_NONCE_BYTES && i < toI32(nonce.length); i++) {
@@ -1915,14 +1954,13 @@ export class PgConnection {
1915
1954
 
1916
1955
  /**
1917
1956
  * Whether a builder call may queue `size` more bytes and still leave
1918
- * `PG_TAIL` under `maxOutput`. When they would not fit after the end of the
1957
+ * `PG_TAIL`, and what the open bind's end owes, under `maxOutput`. When they would not fit after the end of the
1919
1958
  * buffer, the unsent bytes move to its start first: a builder call asks
1920
1959
  * before it takes any offset into the buffer, so nothing it holds moves.
1921
1960
  */
1922
1961
  private room(size: i32): boolean {
1923
1962
  const queued: i32 = this.outEnd - this.outStart
1924
- const tail: i32 = this.bindAt >= 0 && this.bindBinary ? PG_TAIL + PG_BINARY_TAIL : PG_TAIL
1925
- if (size < 0 || size > this.config.maxOutput - tail - queued) {
1963
+ if (size < 0 || size > this.config.maxOutput - PG_TAIL - this.bindOwed - queued) {
1926
1964
  return false
1927
1965
  }
1928
1966
  if (this.outStart > 0 && this.outEnd + size > toI32(this.out.length)) {
@@ -2116,6 +2154,8 @@ export class PgConnection {
2116
2154
  }
2117
2155
  this.bindOpened = opens
2118
2156
  this.bindBinary = binary
2157
+ this.bindOwed = extra
2158
+ this.binaryParamCount = 0
2119
2159
  this.bindAt = this.startMessage(PG_FE_BIND)
2120
2160
  this.putByte(PG_ZERO)
2121
2161
  this.putCString(name)
@@ -2133,9 +2173,37 @@ export class PgConnection {
2133
2173
  */
2134
2174
  private startParam(size: i32): i32 {
2135
2175
  this.noArrayOpen()
2136
- if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size)) {
2176
+ // Once the bind holds a binary parameter, every parameter owes its format code.
2177
+ const owed: i32 = this.binaryParamCount > 0 ? 2 : 0
2178
+ if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size + owed)) {
2137
2179
  return -1
2138
2180
  }
2181
+ this.bindOwed += owed
2182
+ this.paramCount += 1
2183
+ const at: i32 = this.outEnd
2184
+ this.putI32(PG_ZERO)
2185
+ return at
2186
+ }
2187
+
2188
+ /**
2189
+ * `startParam` for a parameter in binary format: the first one in a bind
2190
+ * owes a format code for every parameter before it as well as its own,
2191
+ * which `bindEnd` writes once the count is known. A text-only bind owes none
2192
+ * and writes the zero count it always has.
2193
+ */
2194
+ private startBinaryParam(size: i32): i32 {
2195
+ this.noArrayOpen()
2196
+ const owed: i32 = this.binaryParamCount > 0 ? toI32(2) : toI32(2) * (this.paramCount + toI32(1))
2197
+ if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size + owed)) {
2198
+ return -1
2199
+ }
2200
+ if (this.binaryParamCount < toI32(this.binaryParams.length)) {
2201
+ this.binaryParams[this.binaryParamCount] = this.paramCount
2202
+ } else {
2203
+ this.binaryParams.push(this.paramCount)
2204
+ }
2205
+ this.binaryParamCount += 1
2206
+ this.bindOwed += owed
2139
2207
  this.paramCount += 1
2140
2208
  const at: i32 = this.outEnd
2141
2209
  this.putI32(PG_ZERO)
@@ -2248,7 +2316,7 @@ export class PgConnection {
2248
2316
  const end: i32 = start + count
2249
2317
  // Its exact size first, so that room is asked for once and a refusal writes nothing.
2250
2318
  let size: i64 = count > 0 ? toI64(count + 1) : toI64(2)
2251
- for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
2319
+ for (let i: i32 = start; i >= 0 && i < end && i < toI32(values.length); i++) {
2252
2320
  size += toI64(pgDecimalLength(values[i]))
2253
2321
  }
2254
2322
  const at: i32 = this.startParam(size > toI64(PG_MAX_CAP) ? PG_MAX_CAP + 1 : toI32(size))
@@ -2256,7 +2324,7 @@ export class PgConnection {
2256
2324
  return false
2257
2325
  }
2258
2326
  this.putByte(PG_LBRACE)
2259
- for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
2327
+ for (let i: i32 = start; i >= 0 && i < end && i < toI32(values.length); i++) {
2260
2328
  // Read before the comma's call, which could drop the length fact.
2261
2329
  const value: i64 = values[i]
2262
2330
  if (i > start) {
@@ -2310,6 +2378,80 @@ export class PgConnection {
2310
2378
  return true
2311
2379
  }
2312
2380
 
2381
+ // Binary `int4[]` and `int8[]` parameters, in PostgreSQL's binary array
2382
+ // layout (`array_send`'s): the Bind's only parameters not in text, and its
2383
+ // format codes say so. Their elements are fixed-size big-endian integers,
2384
+ // so there are no digits to write and none for the server to parse. The
2385
+ // element type is explicit and must be the statement's: an `int4[]` sent
2386
+ // where the server expects `int8[]`, or the reverse, is refused by the
2387
+ // server as that execution's `PG_ERROR`, and the connection goes on. An
2388
+ // `int4` element outside -2^31..2^31-1 panics as a program error: false
2389
+ // would read as "no room", and a batching loop would retry it forever.
2390
+
2391
+ /**
2392
+ * An `int4[]` parameter in binary from `values[start, start + count)`;
2393
+ * false as the `param...` calls are. Panics, writing nothing, when the
2394
+ * window is not inside `values` or an element is outside `int4`.
2395
+ */
2396
+ paramInt4Array(values: i64[], start: i32, count: i32): boolean {
2397
+ pgIntWindow(values, start, count)
2398
+ const end: i32 = start + count
2399
+ for (let i: i32 = start; i >= 0 && i < end && i < toI32(values.length); i++) {
2400
+ if (!pgFitsInt4(values[i])) {
2401
+ panic("nish/net/postgres: paramInt4Array() with an element outside int4")
2402
+ }
2403
+ }
2404
+ return this.paramBinaryInts(values, start, count, PG_OID_INT4)
2405
+ }
2406
+
2407
+ /**
2408
+ * An `int8[]` parameter in binary from `values[start, start + count)`;
2409
+ * false as the `param...` calls are. Panics when the window is not inside
2410
+ * `values`.
2411
+ */
2412
+ paramInt8Array(values: i64[], start: i32, count: i32): boolean {
2413
+ pgIntWindow(values, start, count)
2414
+ return this.paramBinaryInts(values, start, count, PG_OID_INT8)
2415
+ }
2416
+
2417
+ private paramBinaryInts(values: i64[], start: i32, count: i32, oid: i32): boolean {
2418
+ const width: i32 = oid === PG_OID_INT4 ? 4 : 8
2419
+ // The header, its one dimension when there are elements, then each element's length word and bytes.
2420
+ const size: i64 =
2421
+ count > 0
2422
+ ? toI64(PG_ARRAY_HEADER + PG_ARRAY_DIM) + toI64(count) * toI64(4 + width)
2423
+ : toI64(PG_ARRAY_HEADER)
2424
+ const at: i32 = this.startBinaryParam(size > toI64(PG_MAX_CAP) ? PG_MAX_CAP + 1 : toI32(size))
2425
+ if (at < 0) {
2426
+ return false
2427
+ }
2428
+ this.reserve(this.outEnd + toI32(size))
2429
+ this.putArrayHeader(oid, count)
2430
+ const end: i32 = start + count
2431
+ for (let i: i32 = start; i >= 0 && i < end && i < toI32(values.length); i++) {
2432
+ setI32BE(this.out, this.outEnd, width)
2433
+ if (width === 4) {
2434
+ setI32BE(this.out, this.outEnd + 4, toI32(values[i]))
2435
+ } else {
2436
+ setI64BE(this.out, this.outEnd + 4, values[i])
2437
+ }
2438
+ this.outEnd += 4 + width
2439
+ }
2440
+ this.endParam(at)
2441
+ return true
2442
+ }
2443
+
2444
+ /** A binary array's header with no NULLs: no dimension when it is empty, as `array_send` writes one, else one of `count` elements from 1. */
2445
+ private putArrayHeader(oid: i32, count: i32): void {
2446
+ this.putI32(count > 0 ? toI32(1) : PG_ZERO)
2447
+ this.putI32(PG_ZERO)
2448
+ this.putI32(oid)
2449
+ if (count > 0) {
2450
+ this.putI32(count)
2451
+ this.putI32(toI32(1))
2452
+ }
2453
+ }
2454
+
2313
2455
  // An array parameter built element by element: `arrayStart()`, then
2314
2456
  // `arrayElement`, `arrayInt` or `arrayNull` for each element, then `arrayEnd()`. An
2315
2457
  // element refused for room writes nothing and leaves the array open, so a
@@ -2339,6 +2481,9 @@ export class PgConnection {
2339
2481
  if (this.arrayAt < 0) {
2340
2482
  panic("nish/net/postgres: an array element with no arrayStart()")
2341
2483
  }
2484
+ if (this.arrayOid !== 0) {
2485
+ panic("nish/net/postgres: a text array element in a binary array")
2486
+ }
2342
2487
  }
2343
2488
 
2344
2489
  /** Whether the open array has room for an element of `size` bytes, its comma, and the `}` it owes. */
@@ -2417,13 +2562,98 @@ export class PgConnection {
2417
2562
  return true
2418
2563
  }
2419
2564
 
2420
- /** Ends the open array parameter; never refused for room, which its elements kept. Panics when no array is open. */
2565
+ // The binary twin: `binaryArrayStart(elementOid)` opens an `int4[]` (OID
2566
+ // 23) or `int8[]` (OID 20) parameter in binary, `binaryArrayInt` and
2567
+ // `binaryArrayNull` add its elements, and the same `arrayEnd()` closes it.
2568
+ // The text builder's rules hold, and a text element in a binary array, or a
2569
+ // binary element in a text one, panics.
2570
+
2571
+ /** Opens a binary array parameter of `elementOid`'s elements; false as the `param...` calls are. Panics on another OID, or while an array is open. */
2572
+ binaryArrayStart(elementOid: i32): boolean {
2573
+ if (this.arrayAt >= 0) {
2574
+ panic("nish/net/postgres: binaryArrayStart() while an array is open")
2575
+ }
2576
+ if (elementOid !== PG_OID_INT4 && elementOid !== PG_OID_INT8) {
2577
+ panic("nish/net/postgres: binaryArrayStart() with an element OID other than int4 (23) or int8 (20)")
2578
+ }
2579
+ // The header with its dimension; `arrayEnd()` fills in the length and the
2580
+ // NULL flag, or drops the dimension when there are no elements.
2581
+ const at: i32 = this.startBinaryParam(PG_ARRAY_HEADER + PG_ARRAY_DIM)
2582
+ if (at < 0) {
2583
+ return false
2584
+ }
2585
+ this.putArrayHeader(elementOid, toI32(1))
2586
+ this.arrayAt = at
2587
+ this.arrayCount = 0
2588
+ this.arrayOid = elementOid
2589
+ this.arrayNulls = false
2590
+ return true
2591
+ }
2592
+
2593
+ private binaryArrayOpen(): void {
2594
+ if (this.arrayAt < 0 || this.arrayOid === 0) {
2595
+ panic("nish/net/postgres: a binary array element with no binaryArrayStart()")
2596
+ }
2597
+ }
2598
+
2599
+ /**
2600
+ * An integer element of the open binary array; false, and nothing written,
2601
+ * when there is no room. Panics when no binary array is open, or when it is
2602
+ * an `int4[]` and `value` is outside `int4`.
2603
+ */
2604
+ binaryArrayInt(value: i64): boolean {
2605
+ this.binaryArrayOpen()
2606
+ const width: i32 = this.arrayOid === PG_OID_INT4 ? 4 : 8
2607
+ if (width === 4 && !pgFitsInt4(value)) {
2608
+ panic("nish/net/postgres: binaryArrayInt() with an element outside int4")
2609
+ }
2610
+ if (!this.room(4 + width)) {
2611
+ return false
2612
+ }
2613
+ this.putI32(width)
2614
+ if (width === 4) {
2615
+ this.putI32(toI32(value))
2616
+ } else {
2617
+ this.reserve(this.outEnd + 8)
2618
+ setI64BE(this.out, this.outEnd, value)
2619
+ this.outEnd += 8
2620
+ }
2621
+ this.arrayCount += 1
2622
+ return true
2623
+ }
2624
+
2625
+ /** A NULL element of the open binary array, length -1; false, and nothing written, when there is no room. Panics when no binary array is open. */
2626
+ binaryArrayNull(): boolean {
2627
+ this.binaryArrayOpen()
2628
+ if (!this.room(toI32(4))) {
2629
+ return false
2630
+ }
2631
+ this.putI32(toI32(-1))
2632
+ this.arrayNulls = true
2633
+ this.arrayCount += 1
2634
+ return true
2635
+ }
2636
+
2637
+ /** Ends the open array parameter, text or binary; never refused for room, which its elements kept. Panics when no array is open. */
2421
2638
  arrayEnd(): void {
2422
2639
  if (this.arrayAt < 0) {
2423
2640
  panic("nish/net/postgres: arrayEnd() with no arrayStart()")
2424
2641
  }
2425
- this.tailRoom(toI32(1))
2426
- this.putByte(PG_RBRACE)
2642
+ if (this.arrayOid !== 0) {
2643
+ // The header follows the parameter's length word: ndim, the NULL flag, the OID, then the dimension.
2644
+ const header: i32 = this.arrayAt + 4
2645
+ if (this.arrayCount === 0) {
2646
+ setI32BE(this.out, header, PG_ZERO)
2647
+ this.outEnd = header + PG_ARRAY_HEADER
2648
+ } else {
2649
+ setI32BE(this.out, header + 4, this.arrayNulls ? 1 : 0)
2650
+ setI32BE(this.out, header + PG_ARRAY_HEADER, this.arrayCount)
2651
+ }
2652
+ this.arrayOid = 0
2653
+ } else {
2654
+ this.tailRoom(toI32(1))
2655
+ this.putByte(PG_RBRACE)
2656
+ }
2427
2657
  this.endParam(this.arrayAt)
2428
2658
  this.arrayAt = -1
2429
2659
  }
@@ -2442,8 +2672,11 @@ export class PgConnection {
2442
2672
  panic("nish/net/postgres: bindEnd() while an array is open")
2443
2673
  }
2444
2674
  const binary: boolean = this.bindBinary
2445
- this.tailRoom(binary ? PG_TAIL + PG_BINARY_TAIL : PG_TAIL)
2675
+ this.tailRoom(PG_TAIL + this.bindOwed)
2446
2676
  const at: i32 = this.bindAt
2677
+ if (this.binaryParamCount > 0) {
2678
+ this.putFormatCodes()
2679
+ }
2447
2680
  setU16BE(this.out, this.paramCountAt, toU16(this.paramCount))
2448
2681
  if (binary) {
2449
2682
  // One format code, which the server applies to every column.
@@ -2454,6 +2687,7 @@ export class PgConnection {
2454
2687
  }
2455
2688
  this.endMessage(at)
2456
2689
  this.bindAt = -1
2690
+ this.bindOwed = 0
2457
2691
  const execute: i32 = this.startMessage(PG_FE_EXECUTE)
2458
2692
  this.putByte(PG_ZERO)
2459
2693
  this.putI32(PG_ZERO)
@@ -2482,7 +2716,30 @@ export class PgConnection {
2482
2716
  private dropBind(): void {
2483
2717
  this.outEnd = this.bindAt
2484
2718
  this.bindAt = -1
2719
+ this.bindOwed = 0
2485
2720
  this.arrayAt = -1
2721
+ this.arrayOid = 0
2722
+ }
2723
+
2724
+ /**
2725
+ * Writes the format codes a bind holding a binary parameter owes, in front
2726
+ * of its parameter count: every parameter moves up by a code each, and
2727
+ * every code is text's but the binary parameters'.
2728
+ */
2729
+ private putFormatCodes(): void {
2730
+ const from: i32 = this.paramCountAt
2731
+ const codes: i32 = 2 * this.paramCount
2732
+ this.reserve(this.outEnd + codes)
2733
+ copyBytes(this.out, from + codes, this.out, from, this.outEnd - from)
2734
+ this.outEnd += codes
2735
+ setU16BE(this.out, from - 2, toU16(this.paramCount))
2736
+ for (let i: i32 = 0; i < this.paramCount; i++) {
2737
+ setU16BE(this.out, from + 2 * i, toU16(0))
2738
+ }
2739
+ for (let k: i32 = 0; k < this.binaryParamCount && k < toI32(this.binaryParams.length); k++) {
2740
+ setU16BE(this.out, from + 2 * this.binaryParams[k], toU16(PG_FORMAT_BINARY))
2741
+ }
2742
+ this.paramCountAt = from + codes
2486
2743
  }
2487
2744
 
2488
2745
  /** Sync: ends the open group, which the server answers with `PG_GROUP_DONE`. False when no group is open or a bind is; never for room. */
@@ -3005,10 +3262,22 @@ export const PG_BATCH_MISSING: i32 = 3
3005
3262
  /** `PgBatcher.next(run)`: the request `tag`'s execution failed, and its rows so far may be incomplete. */
3006
3263
  export const PG_BATCH_FAILED: i32 = 4
3007
3264
 
3008
- // `take` probes at most maxRequests^2 / 2 slots, so the cap keeps that near 5 * 10^5;
3009
- // a probe whose hash matches in full compares the key's bytes, so the bytes
3010
- // compared are at most that times the longest key, which `maxBytes` bounds.
3011
- const PG_BATCH_MAX_REQUESTS: i32 = 1024
3265
+ // The most requests one execution answers. The cost of a take does not grow
3266
+ // with it: each request probes at most `PG_BATCH_PROBES` slots, and the
3267
+ // arrays sized by it grow with the batches taken, not with the cap. It is
3268
+ // 65536 so that one full run's arrays stay at 512 KiB, and one execution's
3269
+ // int8 key array, which the server reads into memory whole, at about 1.4 MiB
3270
+ // (65536 keys of at most 20 digits and a comma); a text key array is bounded
3271
+ // by `maxBytes` instead.
3272
+ const PG_BATCH_MAX_REQUESTS: i32 = 65536
3273
+ // The most occupied slots one request's probe visits in `take` before its key
3274
+ // is bound again rather than found. A take of n requests holds at most n - 1
3275
+ // keys when it probes, so up to 1024 requests (the cap this replaced) no probe
3276
+ // stops; past that, keys chosen to collide cost each request this many probes
3277
+ // and key comparisons, not one per key before it.
3278
+ const PG_BATCH_PROBES: i32 = 1024
3279
+ // The fewest entries the arrays that grow with the batches taken start at.
3280
+ const PG_BATCH_MIN_GROWTH: i32 = 16
3012
3281
  const PG_BATCH_MAX_QUEUE: i32 = 1048576
3013
3282
  const PG_BATCH_MAX_RUNS: i32 = 1024
3014
3283
  // The takes a run's handles count before they wrap: handles stay under 2^30.
@@ -3016,6 +3285,28 @@ const PG_BATCH_USES: i32 = 1048576
3016
3285
  // The array's `{}` and the longest int8 element, `-9223372036854775808`.
3017
3286
  const PG_BATCH_MIN_BYTES: i32 = 22
3018
3287
 
3288
+ // The low 32 bits of an int8 key.
3289
+ const PG_LOW32: i64 = 4294967295
3290
+
3291
+ /**
3292
+ * The batcher's table hash of an int8 key, 31 bits: the high half times an
3293
+ * odd constant xored into the low half, then MurmurHash3's 32-bit finalizer,
3294
+ * whose unsigned multiplies wrap by definition. Every step is a bijection on
3295
+ * 32 bits, so keys of one high half never collide, and the finalizer spreads
3296
+ * a dense run of ids over the whole table: runs that would land on one
3297
+ * another's slots under a near-identity hash do not merge into one cluster
3298
+ * longer than `PG_BATCH_PROBES`.
3299
+ */
3300
+ const pgIntHash = (key: i64): i32 => {
3301
+ const lo: u32 = toU32(key & PG_LOW32)
3302
+ const hi: u32 = toU32((key >> toI64(32)) & PG_LOW32)
3303
+ let h: u32 = lo ^ (hi * 2654435769)
3304
+ h = (h ^ (h >> 16)) * 2246822507
3305
+ h = (h ^ (h >> 13)) * 3266489909
3306
+ h = h ^ (h >> 16)
3307
+ return toI32(h >> 1)
3308
+ }
3309
+
3019
3310
  /** What a `PgBatcher` sends: one statement shape, its key, and its caps. */
3020
3311
  export class PgBatchConfig {
3021
3312
  /** The prepared statement's name, which `PgBatcher.sql` is prepared under. */
@@ -3042,9 +3333,9 @@ export class PgBatchConfig {
3042
3333
 
3043
3334
  /** One execution's requests, grouped by key, and how far its rows have been routed. */
3044
3335
  class PgBatchRun {
3045
- /** Key `k`'s requests' tags are `tags[first[k], first[k + 1])`, in the order they were added. */
3046
- readonly tags: i32[]
3047
- readonly first: i32[]
3336
+ /** Key `k`'s requests' tags are `tags[first[k], first[k + 1])`, in the order they were added; grown by `take` to the batch. */
3337
+ tags: i32[]
3338
+ first: i32[]
3048
3339
  keys: i32 = 0
3049
3340
  /** The key rows are being read for, whether it has had one, and the next of its requests to answer. */
3050
3341
  cur: i32 = 0
@@ -3059,9 +3350,9 @@ class PgBatchRun {
3059
3350
  ended: boolean = false
3060
3351
  failed: boolean = false
3061
3352
 
3062
- constructor(maxRequests: i32) {
3063
- this.tags = new Array<i32>(maxRequests)
3064
- this.first = new Array<i32>(maxRequests + 1)
3353
+ constructor() {
3354
+ this.tags = []
3355
+ this.first = []
3065
3356
  }
3066
3357
 
3067
3358
  /** Opens the run for `keys` keys, none of whose rows have been read. */
@@ -3082,7 +3373,9 @@ class PgBatchRun {
3082
3373
  * it reads no clock and makes no socket. Each request is a key and the
3083
3374
  * caller's `tag`, queued by `addInt` or `addText`. When a connection is free,
3084
3375
  * `take(conn)` binds up to `maxRequests` of them, and up to `maxBytes` of
3085
- * keys, as one execution of `sql`, each distinct key once, and answers the
3376
+ * keys, as one execution of `sql`, each distinct key once (a key whose
3377
+ * probe of the batcher's table passes 1024 slots, which takes keys chosen to
3378
+ * collide, is bound again and answered the same), and answers the
3086
3379
  * execution's run handle. Each `PG_ROW` of that execution goes to
3087
3380
  * `row(run, conn)`, its `PG_COMPLETE` to `end(run)`, and an error or a lost
3088
3381
  * connection to `fail(run)`; after each, `next(run)` answers events until
@@ -3122,12 +3415,13 @@ export class PgBatcher {
3122
3415
  private readonly qLengths: i32[]
3123
3416
  private readonly store: u8[]
3124
3417
  // `take`'s open-addressed table of the keys it has bound, each a key number
3125
- // plus one, 0 for an empty slot; and for each key, its slot and its first request.
3126
- private readonly table: i32[]
3127
- private readonly keySlots: i32[]
3128
- private readonly keyEntries: i32[]
3129
- private readonly keyCounts: i32[]
3130
- private readonly takenKeys: i32[]
3418
+ // plus one, 0 for an empty slot; and for each key, its slot (-1 when its
3419
+ // probe stopped) and its first request. Grown by `take` to the batch.
3420
+ private table: i32[]
3421
+ private keySlots: i32[]
3422
+ private keyEntries: i32[]
3423
+ private keyCounts: i32[]
3424
+ private takenKeys: i32[]
3131
3425
  private readonly runs: PgBatchRun[]
3132
3426
  /** The request the last `next(run)` answered about; -1 for none. */
3133
3427
  tag: i32 = -1
@@ -3194,19 +3488,14 @@ export class PgBatcher {
3194
3488
  this.qStarts = new Array<i32>(textSize)
3195
3489
  this.qLengths = new Array<i32>(textSize)
3196
3490
  this.store = new Array<u8>(storeSize)
3197
- // At least twice the keys a take can hold, a power of two for the mask.
3198
- let tableSize: i32 = 2
3199
- while (tableSize < 2 * maxRequests) {
3200
- tableSize *= 2
3201
- }
3202
- this.table = new Array<i32>(tableSize)
3203
- this.keySlots = new Array<i32>(maxRequests)
3204
- this.keyEntries = new Array<i32>(maxRequests)
3205
- this.keyCounts = new Array<i32>(maxRequests)
3206
- this.takenKeys = new Array<i32>(maxRequests)
3491
+ this.table = []
3492
+ this.keySlots = []
3493
+ this.keyEntries = []
3494
+ this.keyCounts = []
3495
+ this.takenKeys = []
3207
3496
  this.runs = []
3208
3497
  for (let r: i32 = 0; r < config.runs; r++) {
3209
- this.runs.push(new PgBatchRun(maxRequests))
3498
+ this.runs.push(new PgBatchRun())
3210
3499
  }
3211
3500
  }
3212
3501
 
@@ -3242,10 +3531,8 @@ export class PgBatcher {
3242
3531
  if (q < 0) {
3243
3532
  return false
3244
3533
  }
3245
- // Both halves folded to 31 bits, so that no step can overflow.
3246
- const h: i32 = toI32(key & toI64(0x7fffffff)) ^ toI32((key >> toI64(31)) & toI64(0x7fffffff))
3247
3534
  this.qInts[q] = key
3248
- this.added(q, tag, h ^ (h >> 15), pgDecimalLength(key))
3535
+ this.added(q, tag, pgIntHash(key), pgDecimalLength(key))
3249
3536
  return true
3250
3537
  }
3251
3538
 
@@ -3394,15 +3681,19 @@ export class PgBatcher {
3394
3681
  return -1
3395
3682
  }
3396
3683
  const ring: i32 = toI32(this.qTags.length)
3684
+ this.grow(this.count < this.maxRequests ? this.count : this.maxRequests)
3397
3685
  const mask: i32 = toI32(this.table.length) - 1
3398
3686
  let keys: i32 = 0
3399
3687
  let taken: i32 = 0
3400
3688
  let bytes: i32 = 2
3401
3689
  while (taken < this.count && taken < this.maxRequests && taken < toI32(this.takenKeys.length)) {
3402
3690
  const q: i32 = (this.head + taken) % ring
3403
- // Probe for the key: a slot holding it, or the empty slot it would take.
3691
+ // Probe for the key: a slot holding it, or the empty slot it would take;
3692
+ // -1 after `PG_BATCH_PROBES` occupied ones, when the key is bound again,
3693
+ // its own ordinal and rows, and left out of the table.
3404
3694
  let slot: i32 = this.qHashes[q] & mask
3405
3695
  let k: i32 = -1
3696
+ let probes: i32 = 0
3406
3697
  while (this.table[slot] > 0) {
3407
3698
  const seen: i32 = this.table[slot] - 1
3408
3699
  const e: i32 = this.keyEntries[seen]
@@ -3410,6 +3701,11 @@ export class PgBatcher {
3410
3701
  k = seen
3411
3702
  break
3412
3703
  }
3704
+ probes += 1
3705
+ if (probes === PG_BATCH_PROBES) {
3706
+ slot = -1
3707
+ break
3708
+ }
3413
3709
  slot = (slot + 1) & mask
3414
3710
  }
3415
3711
  if (k < 0) {
@@ -3420,7 +3716,9 @@ export class PgBatcher {
3420
3716
  bytes += cost
3421
3717
  k = keys
3422
3718
  keys += 1
3423
- this.table[slot] = keys
3719
+ if (slot >= 0) {
3720
+ this.table[slot] = keys
3721
+ }
3424
3722
  this.keySlots[k] = slot
3425
3723
  this.keyEntries[k] = q
3426
3724
  this.keyCounts[k] = 0
@@ -3430,7 +3728,10 @@ export class PgBatcher {
3430
3728
  taken += 1
3431
3729
  }
3432
3730
  for (let k: i32 = 0; k < keys && k < toI32(this.keySlots.length); k++) {
3433
- this.table[this.keySlots[k]] = 0
3731
+ const slot: i32 = this.keySlots[k]
3732
+ if (slot >= 0) {
3733
+ this.table[slot] = 0
3734
+ }
3434
3735
  }
3435
3736
  if (taken === 0) {
3436
3737
  conn.bindCancel()
@@ -3440,6 +3741,11 @@ export class PgBatcher {
3440
3741
  conn.bindEnd()
3441
3742
  // The requests grouped by key, each key's in the order they were added.
3442
3743
  const run: PgBatchRun = this.runs[r]
3744
+ if (toI32(run.tags.length) < taken) {
3745
+ // Grown together: `first` holds one more than `tags`, which bounds the keys too.
3746
+ run.tags = new Array<i32>(toI32(this.takenKeys.length))
3747
+ run.first = new Array<i32>(toI32(this.takenKeys.length) + 1)
3748
+ }
3443
3749
  run.first[0] = 0
3444
3750
  for (let k: i32 = 0; k < keys && k < toI32(this.keyCounts.length); k++) {
3445
3751
  run.first[k + 1] = run.first[k] + this.keyCounts[k]
@@ -3461,6 +3767,34 @@ export class PgBatcher {
3461
3767
  return run.handle
3462
3768
  }
3463
3769
 
3770
+ /**
3771
+ * Grows `take`'s arrays, when they are smaller, to hold `need` requests:
3772
+ * to a power of two, at least `PG_BATCH_MIN_GROWTH` and at most
3773
+ * `maxRequests`, and the table to twice that, so it is at most half full.
3774
+ * A batcher whose batches stay small never holds arrays the size of its cap.
3775
+ */
3776
+ private grow(need: i32): void {
3777
+ if (toI32(this.takenKeys.length) >= need) {
3778
+ return
3779
+ }
3780
+ let size: i32 = PG_BATCH_MIN_GROWTH
3781
+ while (size < need) {
3782
+ size *= 2
3783
+ }
3784
+ if (size > this.maxRequests) {
3785
+ size = this.maxRequests
3786
+ }
3787
+ let tableSize: i32 = 2
3788
+ while (tableSize < 2 * size) {
3789
+ tableSize *= 2
3790
+ }
3791
+ this.table = new Array<i32>(tableSize)
3792
+ this.keySlots = new Array<i32>(size)
3793
+ this.keyEntries = new Array<i32>(size)
3794
+ this.keyCounts = new Array<i32>(size)
3795
+ this.takenKeys = new Array<i32>(size)
3796
+ }
3797
+
3464
3798
  /** Writes the queued request `q`'s key as the open array's next element; false when `conn` has no room. */
3465
3799
  private bindKey(conn: PgConnection, q: i32): boolean {
3466
3800
  if (this.keyType === PG_BATCH_INT) {