@amritk/nish-x86_64-linux 0.21.0 → 0.23.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.
@@ -63,7 +63,12 @@
63
63
  * length in two reused arrays, so a row costs no allocation. `int`, `bool`,
64
64
  * `isNull` and `PgText.timestamp` decode a column in place; `PgArrayWalker`
65
65
  * walks a one-dimensional `text[]` in place. `columnText(c)` is the one that
66
- * allocates, a string.
66
+ * allocates, a string. An array parameter is a text array literal too:
67
+ * `paramIntArray` writes an `int8[]` from an `i64[]` window, and `arrayStart`,
68
+ * `arrayElement`, `arrayInt`, `arrayNull` and `arrayEnd` build an array one
69
+ * element at a time into the Bind, so a list needs no string built first. A
70
+ * program whose element was refused for room can drop the whole bind,
71
+ * unsent, with `bindCancel`.
67
72
  *
68
73
  * **Authentication.** SCRAM-SHA-256 (RFC 5802, RFC 7677) or none (`trust`).
69
74
  * The password arrives as a `Secret<u8[]>`; the connection keeps a copy in a
@@ -90,6 +95,35 @@
90
95
  * connection with their own `failure` code. The constructor panics on a cap
91
96
  * out of range, as on a missing user or a short nonce.
92
97
  *
98
+ * **Pools.** `PgPool` holds up to `size` connections to one server and a FIFO
99
+ * queue of up to `queueSize` requests, still sans-IO: `wanted()` names a slot
100
+ * that needs a socket, `connect(slot, nonce)` starts its connection, and the
101
+ * program moves its bytes as above through `connection(slot)`. A request is a
102
+ * ticket from `request()`; `dispatch()` gives the oldest to the ready
103
+ * connection with the fewest groups in flight, the program builds its group and
104
+ * ends it with `send()`, and `next(slot)` reads that connection's events with
105
+ * `ticket` naming the request. Statements given to `register` are prepared on
106
+ * each connection ahead of its first request. A connection that fails reports
107
+ * `PG_LOST` for each request it took, then `PG_FAILED`; the program closes the
108
+ * socket and the slot is built again on its next use. `tests/postgres/carrier.ts`
109
+ * is a whole carrier: one `pollWait` loop holding the pool's sockets beside an
110
+ * HTTP listener.
111
+ *
112
+ * **Batching.** `PgBatcher` turns many reads of one row shape by key — a user
113
+ * by id, an article by slug — into one execution of a statement over
114
+ * `unnest($1) with ordinality`. `addInt` or `addText` queues a request's key
115
+ * and tag; `take(conn)` binds the oldest waiting, each distinct key once, up
116
+ * to `maxRequests` and `maxBytes`, and answers a run's handle; `row`, `end`
117
+ * and `fail` take that execution's events (a handle already answered makes
118
+ * them a no-op counted in `stray`, so a server's late event cannot panic),
119
+ * routing each row by its ordinal (checked first: a server's ordinal is
120
+ * refused before it routes anything), and `next(run)` answers
121
+ * `PG_BATCH_ROW`, then `PG_BATCH_DONE`, `PG_BATCH_MISSING` or
122
+ * `PG_BATCH_FAILED`, for each request. It reads no clock, and the batcher
123
+ * itself allocates only in its constructor (the connection's output buffer a
124
+ * take writes into may still grow, up to its `maxOutput`); when to take is
125
+ * the program's.
126
+ *
93
127
  * Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
94
128
  * and RFC 5802 and RFC 7677; not ported from another implementation.
95
129
  */
@@ -296,6 +330,35 @@ class PgPassword {
296
330
  }
297
331
  }
298
332
 
333
+ /** Panics on a `PgConfig` that is a mistake in the program: no user, a cap out of range, a NUL in a startup parameter. */
334
+ const pgCheckConfig = (config: PgConfig): void => {
335
+ if (config.user.length === 0) {
336
+ panic("nish/net/postgres: PgConfig.user is required")
337
+ }
338
+ if (
339
+ config.maxMessage < 1024 ||
340
+ config.maxMessage > PG_MAX_CAP ||
341
+ config.maxOutput < 1024 ||
342
+ config.maxOutput > PG_MAX_CAP ||
343
+ config.maxInFlight < 1 ||
344
+ config.maxColumns < 1 ||
345
+ config.maxColumns > 65535 ||
346
+ config.minIterations < 1 ||
347
+ config.maxIterations < config.minIterations ||
348
+ config.maxIterations > PG_MAX_ITERATIONS
349
+ ) {
350
+ panic("nish/net/postgres: a PgConfig cap out of range")
351
+ }
352
+ if (
353
+ pgHasNul(config.user) ||
354
+ pgHasNul(config.database) ||
355
+ pgHasNul(config.applicationName) ||
356
+ pgHasNul(config.options)
357
+ ) {
358
+ panic("nish/net/postgres: a startup parameter holds a NUL byte")
359
+ }
360
+ }
361
+
299
362
  /** The bytes of `text`. */
300
363
  const pgBytesOf = (text: string): u8[] => {
301
364
  const out: u8[] = []
@@ -306,6 +369,17 @@ const pgBytesOf = (text: string): u8[] => {
306
369
  return out
307
370
  }
308
371
 
372
+ /** How many bytes `value` takes in decimal, its sign included. */
373
+ const pgDecimalLength = (value: i64): i32 => {
374
+ let n: i32 = value < 0 ? 2 : 1
375
+ let v: i64 = value < 0 ? value : -value
376
+ while (v <= -10) {
377
+ v = v / 10
378
+ n += 1
379
+ }
380
+ return n
381
+ }
382
+
309
383
  /** Whether `text` holds a NUL byte, which a protocol string cannot. */
310
384
  const pgHasNul = (text: string): boolean => {
311
385
  const n: i32 = toI32(text.length)
@@ -317,6 +391,61 @@ const pgHasNul = (text: string): boolean => {
317
391
  return false
318
392
  }
319
393
 
394
+ /**
395
+ * Whether `b[from, end)` is UTF-8 (RFC 3629: shortest forms, no surrogates,
396
+ * nothing past U+10FFFF) with no NUL: what a `text` value can hold, so that
397
+ * the server accepts it.
398
+ */
399
+ const pgIsText = (b: u8[], from: i32, end: i32): boolean => {
400
+ let i: i32 = from
401
+ while (i >= 0 && i < end && i < toI32(b.length)) {
402
+ const c: i32 = toI32(b[i])
403
+ if (c < 0x80) {
404
+ if (c === 0) {
405
+ return false
406
+ }
407
+ i += 1
408
+ continue
409
+ }
410
+ // The bytes after the lead, and the range of the first: the rest are 0x80-0xBF.
411
+ let more: i32 = 0
412
+ let low: i32 = 0x80
413
+ let high: i32 = 0xbf
414
+ if (c >= 0xc2 && c <= 0xdf) {
415
+ more = 1
416
+ } else if (c >= 0xe0 && c <= 0xef) {
417
+ more = 2
418
+ if (c === 0xe0) {
419
+ low = 0xa0
420
+ } else if (c === 0xed) {
421
+ high = 0x9f
422
+ }
423
+ } else if (c >= 0xf0 && c <= 0xf4) {
424
+ more = 3
425
+ if (c === 0xf0) {
426
+ low = 0x90
427
+ } else if (c === 0xf4) {
428
+ high = 0x8f
429
+ }
430
+ } else {
431
+ return false
432
+ }
433
+ if (more > end - i - 1) {
434
+ return false
435
+ }
436
+ for (let j: i32 = 1; j <= more && i + j < toI32(b.length); j++) {
437
+ const d: i32 = toI32(b[i + j])
438
+ if (d < low || d > high) {
439
+ return false
440
+ }
441
+ low = 0x80
442
+ high = 0xbf
443
+ }
444
+ i += more + 1
445
+ }
446
+ return true
447
+ }
448
+
320
449
  /** The first `byte` in `b[from, end)`, or `end`. */
321
450
  const pgFind = (b: u8[], from: i32, end: i32, byte: i32): i32 => {
322
451
  if (from < 0 || end > toI32(b.length)) {
@@ -481,28 +610,33 @@ export class PgText {
481
610
  this.failed = true
482
611
  return -1
483
612
  }
484
- // The fixed part: YYYY-MM-DD HH:MM:SS, its separators where they belong.
485
- for (let i: i32 = 0; i < 19; i++) {
486
- const c: i32 = toI32(b[start + i])
487
- let ok: boolean = c >= 48 && c <= 57
488
- if (i === 4 || i === 7) {
489
- ok = c === 45
490
- } else if (i === 10) {
491
- ok = c === 32
492
- } else if (i === 13 || i === 16) {
493
- ok = c === 58
494
- }
495
- if (!ok) {
496
- this.failed = true
497
- return -1
498
- }
499
- }
613
+ // The fixed part: YYYY-MM-DD HH:MM:SS. Its five separators are where they
614
+ // belong and are not digits, so the other fourteen bytes are digits
615
+ // exactly when the part holds five non-digits.
500
616
  let p: i32 = start + 19
501
617
  const end: i32 = start + length
502
618
  if (p < 0 || end > toI32(b.length)) {
503
619
  this.failed = true
504
620
  return -1
505
621
  }
622
+ let others: i32 = 0
623
+ for (let i: i32 = start; i < p && i < toI32(b.length); i++) {
624
+ const c: i32 = toI32(b[i])
625
+ if (c < 48 || c > 57) {
626
+ others++
627
+ }
628
+ }
629
+ if (
630
+ others !== 5 ||
631
+ toI32(b[start + 4]) !== 45 ||
632
+ toI32(b[start + 7]) !== 45 ||
633
+ toI32(b[start + 10]) !== 32 ||
634
+ toI32(b[start + 13]) !== 58 ||
635
+ toI32(b[start + 16]) !== 58
636
+ ) {
637
+ this.failed = true
638
+ return -1
639
+ }
506
640
  let millis: i32 = 0
507
641
  if (p < end && toI32(b[p]) === 46) {
508
642
  p++
@@ -526,13 +660,13 @@ export class PgText {
526
660
  if (p + 3 === end && toI32(b[p]) === 43 && toI32(b[p + 1]) === 48 && toI32(b[p + 2]) === 48) {
527
661
  p = end
528
662
  }
529
- if (p !== end || pgBytesAre(b, start, 4, "0000")) {
663
+ // The year's four bytes are digits, so their OR is '0' exactly when the year is 0000.
664
+ const year: i32 = toI32(b[start]) | toI32(b[start + 1]) | toI32(b[start + 2]) | toI32(b[start + 3])
665
+ if (p !== end || year === 48) {
530
666
  this.failed = true
531
667
  return -1
532
668
  }
533
- for (let i: i32 = 0; i < 19; i++) {
534
- out[at + i] = b[start + i]
535
- }
669
+ copyBytes(out, at, b, start, 19)
536
670
  out[at + 10] = toU8(84)
537
671
  out[at + 19] = toU8(46)
538
672
  out[at + 20] = toU8(millis / 100 + 48)
@@ -727,9 +861,14 @@ export class PgConnection {
727
861
  private groupOpen: boolean = false
728
862
  /** Whether a statement in the group `PG_GROUP_DONE` just finished failed. */
729
863
  groupFailed: boolean = false
864
+ // Whether the open bind opened its group, which `bindCancel` closes again.
865
+ private bindOpened: boolean = false
730
866
  private bindAt: i32 = -1
731
867
  private paramCountAt: i32 = 0
732
868
  private paramCount: i32 = 0
869
+ // The open array parameter's length word, -1 when none is open, and its elements so far.
870
+ private arrayAt: i32 = -1
871
+ private arrayCount: i32 = 0
733
872
  private clientNonce: string = ""
734
873
  private serverSignature: u8[]
735
874
 
@@ -743,31 +882,7 @@ export class PgConnection {
743
882
  if (toI32(nonce.length) < PG_NONCE_BYTES) {
744
883
  panic("nish/net/postgres: the SCRAM nonce needs at least 18 random bytes")
745
884
  }
746
- if (config.user.length === 0) {
747
- panic("nish/net/postgres: PgConfig.user is required")
748
- }
749
- if (
750
- config.maxMessage < 1024 ||
751
- config.maxMessage > PG_MAX_CAP ||
752
- config.maxOutput < 1024 ||
753
- config.maxOutput > PG_MAX_CAP ||
754
- config.maxInFlight < 1 ||
755
- config.maxColumns < 1 ||
756
- config.maxColumns > 65535 ||
757
- config.minIterations < 1 ||
758
- config.maxIterations < config.minIterations ||
759
- config.maxIterations > PG_MAX_ITERATIONS
760
- ) {
761
- panic("nish/net/postgres: a PgConfig cap out of range")
762
- }
763
- if (
764
- pgHasNul(config.user) ||
765
- pgHasNul(config.database) ||
766
- pgHasNul(config.applicationName) ||
767
- pgHasNul(config.options)
768
- ) {
769
- panic("nish/net/postgres: a startup parameter holds a NUL byte")
770
- }
885
+ pgCheckConfig(config)
771
886
  this.config = config
772
887
  this.text = new PgText()
773
888
  this.password = new PgPassword(password)
@@ -909,8 +1024,15 @@ export class PgConnection {
909
1024
  return PG_NEED_MORE
910
1025
  }
911
1026
  const at: i32 = this.inStart
912
- const type: i32 = toI32(this.input[at])
913
- const length: i32 = getI32BE(this.input, at + 1)
1027
+ const b: u8[] = this.input
1028
+ // inStart and inEnd stay within the buffer, so this test never fails; it proves the indices.
1029
+ if (at < 0 || at + 4 >= toI32(b.length)) {
1030
+ return this.fail(PG_ERR_FRAME)
1031
+ }
1032
+ const type: i32 = toI32(b[at])
1033
+ // getI32BE written out, as in readRow: this runs once per message.
1034
+ const length: i32 =
1035
+ (toI32(b[at + 1]) << 24) | (toI32(b[at + 2]) << 16) | (toI32(b[at + 3]) << 8) | toI32(b[at + 4])
914
1036
  if (length < 4) {
915
1037
  return this.fail(PG_ERR_FRAME)
916
1038
  }
@@ -1133,18 +1255,24 @@ export class PgConnection {
1133
1255
  return this.fail(PG_ERR_TOO_BIG)
1134
1256
  }
1135
1257
  this.reserveColumns(n)
1258
+ const starts: i32[] = this.colStarts
1259
+ const lengths: i32[] = this.colLengths
1136
1260
  let p: i32 = body + 2
1137
- for (let c: i32 = 0; c < n; c++) {
1138
- if (end - p < 4) {
1261
+ // reserveColumns made both arrays hold n; the length tests prove the indices.
1262
+ for (let c: i32 = 0; c < n && c < toI32(starts.length) && c < toI32(lengths.length); c++) {
1263
+ // The buffer tests never fail on a frame next() cut from it; they prove the indices.
1264
+ if (end - p < 4 || p < 0 || p + 3 >= toI32(b.length)) {
1139
1265
  return this.fail(PG_ERR_FRAME)
1140
1266
  }
1141
- const length: i32 = getI32BE(b, p)
1267
+ // getI32BE written out: a call per column was a fifth of the row's cost.
1268
+ const length: i32 =
1269
+ (toI32(b[p]) << 24) | (toI32(b[p + 1]) << 16) | (toI32(b[p + 2]) << 8) | toI32(b[p + 3])
1142
1270
  p += 4
1143
1271
  if (length < -1 || length > end - p) {
1144
1272
  return this.fail(PG_ERR_FRAME)
1145
1273
  }
1146
- this.colStarts[c] = p
1147
- this.colLengths[c] = length
1274
+ starts[c] = p
1275
+ lengths[c] = length
1148
1276
  if (length > 0) {
1149
1277
  p += length
1150
1278
  }
@@ -1479,6 +1607,9 @@ export class PgConnection {
1479
1607
  this.bindAt -= shift
1480
1608
  this.paramCountAt -= shift
1481
1609
  }
1610
+ if (this.arrayAt >= 0) {
1611
+ this.arrayAt -= shift
1612
+ }
1482
1613
  }
1483
1614
 
1484
1615
  /** Makes the output buffer at least `want` bytes long. */
@@ -1609,9 +1740,11 @@ export class PgConnection {
1609
1740
  * order, then `bindEnd()`. False as `prepare` is.
1610
1741
  */
1611
1742
  bindStart(name: string): boolean {
1743
+ const opens: boolean = !this.groupOpen
1612
1744
  if (pgHasNul(name) || toI32(name.length) > PG_MAX_CAP || !this.canOpen(11 + toI32(name.length))) {
1613
1745
  return false
1614
1746
  }
1747
+ this.bindOpened = opens
1615
1748
  this.bindAt = this.startMessage(PG_FE_BIND)
1616
1749
  this.putByte(PG_ZERO)
1617
1750
  this.putCString(name)
@@ -1628,6 +1761,7 @@ export class PgConnection {
1628
1761
  * bind has 65535 parameters already, or there is no room under `maxOutput`.
1629
1762
  */
1630
1763
  private startParam(size: i32): i32 {
1764
+ this.noArrayOpen()
1631
1765
  if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size)) {
1632
1766
  return -1
1633
1767
  }
@@ -1641,6 +1775,12 @@ export class PgConnection {
1641
1775
  setI32BE(this.out, at, this.outEnd - at - 4)
1642
1776
  }
1643
1777
 
1778
+ private noArrayOpen(): void {
1779
+ if (this.arrayAt >= 0) {
1780
+ panic("nish/net/postgres: a parameter while an array is open")
1781
+ }
1782
+ }
1783
+
1644
1784
  // Every `param...` call answers false, and writes nothing, when no bind is
1645
1785
  // open, the bind has 65535 parameters already, or the parameter would pass
1646
1786
  // `maxOutput`; a program that checked `bindStart()` meets only the last.
@@ -1658,6 +1798,7 @@ export class PgConnection {
1658
1798
 
1659
1799
  /** A text parameter from `bytes[start, start + length)`, UTF-8; false too when the window is not inside `bytes`. */
1660
1800
  paramBytes(bytes: u8[], start: i32, length: i32): boolean {
1801
+ this.noArrayOpen()
1661
1802
  if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
1662
1803
  return false
1663
1804
  }
@@ -1687,6 +1828,13 @@ export class PgConnection {
1687
1828
  if (at < 0) {
1688
1829
  return false
1689
1830
  }
1831
+ this.putDecimal(value)
1832
+ this.endParam(at)
1833
+ return true
1834
+ }
1835
+
1836
+ /** `value` in decimal, `pgDecimalLength(value)` bytes. */
1837
+ private putDecimal(value: i64): void {
1690
1838
  this.reserve(this.outEnd + 20)
1691
1839
  // The digits are written from the least significant, then reversed; the
1692
1840
  // value is made negative first so that the most negative i64 has its digits.
@@ -1713,6 +1861,39 @@ export class PgConnection {
1713
1861
  lo++
1714
1862
  hi--
1715
1863
  }
1864
+ }
1865
+
1866
+ /**
1867
+ * An `int8[]` parameter from `values[start, start + count)`, in decimal;
1868
+ * panics when the range is not inside `values`.
1869
+ */
1870
+ paramIntArray(values: i64[], start: i32, count: i32): boolean {
1871
+ if (start < 0 || count < 0 || count > toI32(values.length) - start) {
1872
+ panic("nish/net/postgres: paramIntArray() outside its array")
1873
+ }
1874
+ // Each loop is bounded by `values.length` as well as by the window's end,
1875
+ // which the guard above keeps inside it: that names the bound the compiler
1876
+ // proves `values[i]` against, where the guard alone does not reach it.
1877
+ const end: i32 = start + count
1878
+ // Its exact size first, so that room is asked for once and a refusal writes nothing.
1879
+ let size: i64 = count > 0 ? toI64(count + 1) : toI64(2)
1880
+ for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
1881
+ size += toI64(pgDecimalLength(values[i]))
1882
+ }
1883
+ const at: i32 = this.startParam(size > toI64(PG_MAX_CAP) ? PG_MAX_CAP + 1 : toI32(size))
1884
+ if (at < 0) {
1885
+ return false
1886
+ }
1887
+ this.putByte(PG_LBRACE)
1888
+ for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
1889
+ // Read before the comma's call, which could drop the length fact.
1890
+ const value: i64 = values[i]
1891
+ if (i > start) {
1892
+ this.putByte(PG_COMMA)
1893
+ }
1894
+ this.putDecimal(value)
1895
+ }
1896
+ this.putByte(PG_RBRACE)
1716
1897
  this.endParam(at)
1717
1898
  return true
1718
1899
  }
@@ -1758,6 +1939,124 @@ export class PgConnection {
1758
1939
  return true
1759
1940
  }
1760
1941
 
1942
+ // An array parameter built element by element: `arrayStart()`, then
1943
+ // `arrayElement`, `arrayInt` or `arrayNull` for each element, then `arrayEnd()`. An
1944
+ // element refused for room writes nothing and leaves the array open, so a
1945
+ // program that batches rows ends the array with the elements it has and
1946
+ // sends the rest in the next statement. While an array is open, every other
1947
+ // `param...` call and `bindEnd()` panic, as do an element or `arrayEnd()`
1948
+ // with none open and a second `arrayStart()`; `sync()`, `prepare`,
1949
+ // `closeStatement` and `bindStart` answer false, the bind being open.
1950
+
1951
+ /** Opens an array parameter, `{}` so far; false as the `param...` calls are. */
1952
+ arrayStart(): boolean {
1953
+ if (this.arrayAt >= 0) {
1954
+ panic("nish/net/postgres: arrayStart() while an array is open")
1955
+ }
1956
+ // Room for `{}`: the `}` is owed, and every element keeps room for it.
1957
+ const at: i32 = this.startParam(toI32(2))
1958
+ if (at < 0) {
1959
+ return false
1960
+ }
1961
+ this.putByte(PG_LBRACE)
1962
+ this.arrayAt = at
1963
+ this.arrayCount = 0
1964
+ return true
1965
+ }
1966
+
1967
+ private arrayOpen(): void {
1968
+ if (this.arrayAt < 0) {
1969
+ panic("nish/net/postgres: an array element with no arrayStart()")
1970
+ }
1971
+ }
1972
+
1973
+ /** Whether the open array has room for an element of `size` bytes, its comma, and the `}` it owes. */
1974
+ private elementRoom(size: i64): boolean {
1975
+ const total: i64 = size + toI64(this.arrayCount > 0 ? 2 : 1)
1976
+ return total <= toI64(PG_MAX_CAP) && this.room(toI32(total))
1977
+ }
1978
+
1979
+ /**
1980
+ * A text element from `bytes[start, start + length)`, UTF-8, quoted with
1981
+ * `"` and `\` escaped; false, and nothing written, when there is no room.
1982
+ * Panics when no array is open or the window is not inside `bytes`.
1983
+ */
1984
+ arrayElement(bytes: u8[], start: i32, length: i32): boolean {
1985
+ this.arrayOpen()
1986
+ if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
1987
+ panic("nish/net/postgres: arrayElement() outside its source")
1988
+ }
1989
+ // Both loops are bounded by `bytes.length` too, as `paramIntArray`'s are.
1990
+ const end: i32 = start + length
1991
+ let escapes: i32 = 0
1992
+ for (let i: i32 = start; i < end && i < toI32(bytes.length); i++) {
1993
+ const c: i32 = toI32(bytes[i])
1994
+ if (c === PG_QUOTE || c === PG_BACKSLASH) {
1995
+ escapes += 1
1996
+ }
1997
+ }
1998
+ if (!this.elementRoom(toI64(length) + toI64(escapes) + 2)) {
1999
+ return false
2000
+ }
2001
+ this.elementComma()
2002
+ this.putByte(PG_QUOTE)
2003
+ this.reserve(this.outEnd + length + escapes + 1)
2004
+ if (escapes === 0) {
2005
+ copyBytes(this.out, this.outEnd, bytes, start, length)
2006
+ this.outEnd += length
2007
+ } else {
2008
+ for (let i: i32 = start; i < end && i < toI32(bytes.length); i++) {
2009
+ const c: i32 = toI32(bytes[i])
2010
+ if (c === PG_QUOTE || c === PG_BACKSLASH) {
2011
+ this.putByte(PG_BACKSLASH)
2012
+ }
2013
+ this.putByte(c)
2014
+ }
2015
+ }
2016
+ this.putByte(PG_QUOTE)
2017
+ return true
2018
+ }
2019
+
2020
+ /** A NULL element; false, and nothing written, when there is no room. Panics when no array is open. */
2021
+ arrayNull(): boolean {
2022
+ this.arrayOpen()
2023
+ if (!this.elementRoom(toI64(4))) {
2024
+ return false
2025
+ }
2026
+ this.elementComma()
2027
+ this.putString("NULL")
2028
+ return true
2029
+ }
2030
+
2031
+ private elementComma(): void {
2032
+ if (this.arrayCount > 0) {
2033
+ this.putByte(PG_COMMA)
2034
+ }
2035
+ this.arrayCount += 1
2036
+ }
2037
+
2038
+ /** An integer element, in decimal; false, and nothing written, when there is no room. Panics when no array is open. */
2039
+ arrayInt(value: i64): boolean {
2040
+ this.arrayOpen()
2041
+ if (!this.elementRoom(toI64(pgDecimalLength(value)))) {
2042
+ return false
2043
+ }
2044
+ this.elementComma()
2045
+ this.putDecimal(value)
2046
+ return true
2047
+ }
2048
+
2049
+ /** Ends the open array parameter; never refused for room, which its elements kept. Panics when no array is open. */
2050
+ arrayEnd(): void {
2051
+ if (this.arrayAt < 0) {
2052
+ panic("nish/net/postgres: arrayEnd() with no arrayStart()")
2053
+ }
2054
+ this.tailRoom(toI32(1))
2055
+ this.putByte(PG_RBRACE)
2056
+ this.endParam(this.arrayAt)
2057
+ this.arrayAt = -1
2058
+ }
2059
+
1761
2060
  /**
1762
2061
  * Ends the Bind (every result column in text) and queues its Execute, for
1763
2062
  * every row. False, and nothing written, when no bind is open; never for
@@ -1767,6 +2066,9 @@ export class PgConnection {
1767
2066
  if (this.bindAt < 0) {
1768
2067
  return false
1769
2068
  }
2069
+ if (this.arrayAt >= 0) {
2070
+ panic("nish/net/postgres: bindEnd() while an array is open")
2071
+ }
1770
2072
  this.tailRoom(PG_TAIL)
1771
2073
  const at: i32 = this.bindAt
1772
2074
  setU16BE(this.out, this.paramCountAt, toU16(this.paramCount))
@@ -1780,6 +2082,30 @@ export class PgConnection {
1780
2082
  return true
1781
2083
  }
1782
2084
 
2085
+ /**
2086
+ * Drops the open bind, and the array open in it, unsent, so that a program
2087
+ * whose parameter was refused for room can send the group without it; a
2088
+ * group the bind opened is closed again. False when no bind is open.
2089
+ */
2090
+ bindCancel(): boolean {
2091
+ if (this.bindAt < 0) {
2092
+ return false
2093
+ }
2094
+ this.dropBind()
2095
+ if (this.bindOpened) {
2096
+ this.groupOpen = false
2097
+ this.inFlight -= 1
2098
+ }
2099
+ return true
2100
+ }
2101
+
2102
+ // Drops the open bind's bytes, and the array open in it; the group is left as it is.
2103
+ private dropBind(): void {
2104
+ this.outEnd = this.bindAt
2105
+ this.bindAt = -1
2106
+ this.arrayAt = -1
2107
+ }
2108
+
1783
2109
  /** 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. */
1784
2110
  sync(): boolean {
1785
2111
  if (!this.groupOpen || this.bindAt >= 0) {
@@ -1795,8 +2121,7 @@ export class PgConnection {
1795
2121
  /** Terminate: tells the server the connection is ending; close the socket once it is sent. A bind still open is dropped unsent. */
1796
2122
  terminate(): void {
1797
2123
  if (this.bindAt >= 0) {
1798
- this.outEnd = this.bindAt
1799
- this.bindAt = -1
2124
+ this.dropBind()
1800
2125
  }
1801
2126
  this.tailRoom(toI32(5))
1802
2127
  const at: i32 = this.startMessage(PG_FE_TERMINATE)
@@ -1873,3 +2198,962 @@ export class PgConnection {
1873
2198
  return overflowed ? -1 : rows
1874
2199
  }
1875
2200
  }
2201
+
2202
+ // --- The pool ------------------------------------------------------------------
2203
+
2204
+ /** `PgPool.next(slot)`: the request `ticket` names will get no answer, because its connection failed first. */
2205
+ export const PG_LOST: i32 = 9
2206
+ /** `PgPool.dispatch()` and `PgPool.wanted()`: no slot. */
2207
+ export const PG_POOL_NONE: i32 = -1
2208
+ /** `PgPool.request()`: the queue is full, and the request was not taken. */
2209
+ export const PG_POOL_FULL: i32 = -2
2210
+
2211
+ /** The most slots a pool may have. */
2212
+ const PG_POOL_MAX_SLOTS: i32 = 1024
2213
+ /** The longest queue a pool may have. */
2214
+ const PG_POOL_MAX_QUEUE: i32 = 1048576
2215
+ /** The most groups a pooled connection may have in flight, which sizes each slot's ring. */
2216
+ const PG_POOL_MAX_IN_FLIGHT: i32 = 1024
2217
+ /** The most statements a pool may register. */
2218
+ const PG_POOL_MAX_STATEMENTS: i32 = 256
2219
+
2220
+ /** No connection: one is built the next time a request needs it. */
2221
+ const PG_SLOT_IDLE: i32 = 0
2222
+ /** A request needs a connection here: the carrier is to make a socket. */
2223
+ const PG_SLOT_WANTED: i32 = 1
2224
+ /** The connection is made and authenticating. */
2225
+ const PG_SLOT_STARTING: i32 = 2
2226
+ const PG_SLOT_READY: i32 = 3
2227
+ /** The connection failed: `next` is reporting the requests it took with it. */
2228
+ const PG_SLOT_DRAINING: i32 = 4
2229
+
2230
+ /** One of a pool's connections, and the groups it has in flight. */
2231
+ class PgPoolSlot {
2232
+ state: i32 = 0
2233
+ conn: PgConnection | null = null
2234
+ /** How many registered statements this connection has queued, and how many the server has described. */
2235
+ queued: i32 = 0
2236
+ described: i32 = 0
2237
+ /** The groups in flight, oldest first, in a ring: each one's ticket, and how many of the pool's own prepares lead it. */
2238
+ readonly tickets: i32[]
2239
+ readonly leads: i32[]
2240
+ head: i32 = 0
2241
+ count: i32 = 0
2242
+
2243
+ constructor(ring: i32) {
2244
+ this.tickets = new Array<i32>(ring)
2245
+ this.leads = new Array<i32>(ring)
2246
+ }
2247
+
2248
+ push(ticket: i32, leads: i32): void {
2249
+ const at: i32 = (this.head + this.count) % toI32(this.tickets.length)
2250
+ this.tickets[at] = ticket
2251
+ this.leads[at] = leads
2252
+ this.count += 1
2253
+ }
2254
+
2255
+ pop(): void {
2256
+ this.head = (this.head + 1) % toI32(this.tickets.length)
2257
+ this.count -= 1
2258
+ }
2259
+ }
2260
+
2261
+ /**
2262
+ * A pool of connections to one server, sans-IO like `PgConnection`: it makes
2263
+ * no socket and reads no clock, and says which slot needs a socket and when.
2264
+ * Its size is a cap, and no connection is made until a request needs one and
2265
+ * no ready connection has room.
2266
+ *
2267
+ * A request is a ticket from `request()`, queued in FIFO order. `dispatch()`
2268
+ * hands the queue's head to the ready connection with the fewest groups in
2269
+ * flight, never past `maxInFlight` on one; the program builds the request's
2270
+ * group on `connection(slot)` and ends it with `send()`, not `sync()`.
2271
+ * `next(slot)` then reads that connection's events, `ticket` naming the
2272
+ * request each is about. A connection that fails reports `PG_LOST` for each
2273
+ * request it took with it, then `PG_FAILED`, and is built again on its next
2274
+ * use. Statements given to `register` are prepared on each connection in the
2275
+ * same group as its first request, so a request may bind them at once.
2276
+ */
2277
+ export class PgPool {
2278
+ /** The request the last `dispatch()`, or event from `next(slot)`, is about; -1 for none. */
2279
+ ticket: i32 = -1
2280
+ /** The number of slots, the most connections the pool makes. */
2281
+ readonly size: i32
2282
+
2283
+ private readonly config: PgConfig
2284
+ private readonly password: PgPassword
2285
+ private readonly slots: PgPoolSlot[]
2286
+ private readonly queue: i32[]
2287
+ private queueHead: i32 = 0
2288
+ private queueCount: i32 = 0
2289
+ private nextTicket: i32 = 0
2290
+ /** The slot whose request is being built, between `dispatch()` and `send()`. */
2291
+ private building: i32 = -1
2292
+ private started: boolean = false
2293
+ private readonly names: string[]
2294
+ private readonly sqls: string[]
2295
+ private readonly types: i32[][]
2296
+
2297
+ /**
2298
+ * A pool of up to `size` connections (1 to 1024) and up to `queueSize`
2299
+ * waiting requests (1 to 2^20). `password` is copied and kept until
2300
+ * `dispose()`, since a connection is made again after a failure. Panics as
2301
+ * `PgConnection` does on `config`, and on a `maxInFlight` over 1024.
2302
+ */
2303
+ constructor(config: PgConfig, password: Secret<u8[]>, size: i32, queueSize: i32) {
2304
+ pgCheckConfig(config)
2305
+ if (size < 1 || size > PG_POOL_MAX_SLOTS) {
2306
+ panic("nish/net/postgres: a PgPool size out of range")
2307
+ }
2308
+ if (queueSize < 1 || queueSize > PG_POOL_MAX_QUEUE) {
2309
+ panic("nish/net/postgres: a PgPool queueSize out of range")
2310
+ }
2311
+ if (config.maxInFlight > PG_POOL_MAX_IN_FLIGHT) {
2312
+ panic("nish/net/postgres: a PgPool connection's maxInFlight is over 1024")
2313
+ }
2314
+ this.config = config
2315
+ this.password = new PgPassword(password)
2316
+ this.size = size
2317
+ this.slots = []
2318
+ for (let i: i32 = 0; i < size; i++) {
2319
+ this.slots.push(new PgPoolSlot(config.maxInFlight))
2320
+ }
2321
+ this.queue = new Array<i32>(queueSize)
2322
+ this.names = []
2323
+ this.sqls = []
2324
+ this.types = []
2325
+ }
2326
+
2327
+ /**
2328
+ * Registers a statement for every connection to prepare before its first
2329
+ * request. False when a connection has been made already, `name` or `sql`
2330
+ * holds a NUL, or 256 are registered.
2331
+ */
2332
+ register(name: string, sql: string, paramTypes: i32[]): boolean {
2333
+ if (
2334
+ this.started ||
2335
+ pgHasNul(name) ||
2336
+ pgHasNul(sql) ||
2337
+ toI32(this.names.length) === PG_POOL_MAX_STATEMENTS
2338
+ ) {
2339
+ return false
2340
+ }
2341
+ const types: i32[] = []
2342
+ for (const oid of paramTypes) {
2343
+ types.push(oid)
2344
+ }
2345
+ this.names.push(name)
2346
+ this.sqls.push(sql)
2347
+ this.types.push(types)
2348
+ return true
2349
+ }
2350
+
2351
+ /** Queues a request and answers its ticket, or `PG_POOL_FULL` when `queueSize` are waiting. */
2352
+ request(): i32 {
2353
+ const room: i32 = toI32(this.queue.length)
2354
+ if (this.queueCount === room) {
2355
+ return PG_POOL_FULL
2356
+ }
2357
+ const ticket: i32 = this.nextTicket
2358
+ this.nextTicket = ticket === 2147483647 ? 0 : ticket + 1
2359
+ this.queue[(this.queueHead + this.queueCount) % room] = ticket
2360
+ this.queueCount += 1
2361
+ return ticket
2362
+ }
2363
+
2364
+ /** The number of requests waiting for a connection. */
2365
+ waiting(): i32 {
2366
+ return this.queueCount
2367
+ }
2368
+
2369
+ /**
2370
+ * Hands the oldest waiting request to a ready connection with room, and
2371
+ * answers its slot, `ticket` set to the request: build its group on
2372
+ * `connection(slot)`, then `send()`. `PG_POOL_NONE` when nothing waits, a
2373
+ * request is still being built, or no ready connection has room; then a
2374
+ * connection may be wanted (`wanted()`): one more for every `maxInFlight`
2375
+ * waiting requests that the connections already starting cannot take. A
2376
+ * request handed out is the program's to end with `send()`: until then no
2377
+ * other is handed out.
2378
+ */
2379
+ dispatch(): i32 {
2380
+ if (this.building >= 0 || this.queueCount === 0) {
2381
+ return PG_POOL_NONE
2382
+ }
2383
+ const cap: i32 = this.config.maxInFlight
2384
+ let best: i32 = -1
2385
+ let load: i32 = 0
2386
+ let starting: i32 = 0
2387
+ let idle: i32 = -1
2388
+ for (let s: i32 = 0; s < this.size; s++) {
2389
+ const slot: PgPoolSlot = this.slots[s]
2390
+ const conn: PgConnection | null = slot.conn
2391
+ if (slot.state === PG_SLOT_READY && conn !== null) {
2392
+ if (conn.inFlight < cap && (best < 0 || conn.inFlight < load)) {
2393
+ best = s
2394
+ load = conn.inFlight
2395
+ }
2396
+ } else if (slot.state === PG_SLOT_WANTED || slot.state === PG_SLOT_STARTING) {
2397
+ starting += 1
2398
+ } else if (slot.state === PG_SLOT_IDLE && idle < 0) {
2399
+ idle = s
2400
+ }
2401
+ }
2402
+ if (best < 0) {
2403
+ if (idle >= 0 && toI64(this.queueCount) > toI64(starting) * toI64(cap)) {
2404
+ this.slots[idle].state = PG_SLOT_WANTED
2405
+ }
2406
+ return PG_POOL_NONE
2407
+ }
2408
+ const ticket: i32 = this.queue[this.queueHead]
2409
+ this.queueHead = (this.queueHead + 1) % toI32(this.queue.length)
2410
+ this.queueCount -= 1
2411
+ const slot: PgPoolSlot = this.slots[best]
2412
+ const conn: PgConnection = this.connection(best)
2413
+ let leads: i32 = 0
2414
+ while (slot.queued < toI32(this.names.length)) {
2415
+ const k: i32 = slot.queued
2416
+ if (!conn.prepare(this.names[k], this.sqls[k], this.types[k])) {
2417
+ break
2418
+ }
2419
+ slot.queued += 1
2420
+ leads += 1
2421
+ }
2422
+ slot.push(ticket, leads)
2423
+ this.building = best
2424
+ this.ticket = ticket
2425
+ return best
2426
+ }
2427
+
2428
+ /**
2429
+ * Ends the group of the request `dispatch()` handed out. False when there
2430
+ * is none; when a bind is still open, so finish it and call again; and
2431
+ * when no message of the group was written, so the request is dropped and
2432
+ * gets no events (`PG_LOST` if its connection has failed).
2433
+ */
2434
+ send(): boolean {
2435
+ const s: i32 = this.building
2436
+ if (s < 0) {
2437
+ return false
2438
+ }
2439
+ const slot: PgPoolSlot = this.slots[s]
2440
+ const conn: PgConnection = this.connection(s)
2441
+ if (conn.sync()) {
2442
+ this.building = -1
2443
+ return true
2444
+ }
2445
+ if (conn.inFlight < slot.count) {
2446
+ // No message opened the group. A failed connection reports the request
2447
+ // lost with the rest; a live one would never answer it, so it is dropped.
2448
+ if (conn.failure === PG_ERR_NONE) {
2449
+ slot.count -= 1
2450
+ }
2451
+ this.building = -1
2452
+ }
2453
+ return false
2454
+ }
2455
+
2456
+ /** A slot that needs a socket, or `PG_POOL_NONE`: connect it, then call `connect(slot, nonce)`. */
2457
+ wanted(): i32 {
2458
+ for (let s: i32 = 0; s < this.size; s++) {
2459
+ if (this.slots[s].state === PG_SLOT_WANTED) {
2460
+ return s
2461
+ }
2462
+ }
2463
+ return PG_POOL_NONE
2464
+ }
2465
+
2466
+ /**
2467
+ * Makes the wanted `slot`'s connection, `nonce` as `PgConnection` takes it:
2468
+ * send its output once the socket is connected, and if the socket fails,
2469
+ * call `connection(slot).closed()`. Panics when `slot` is not wanted or the
2470
+ * pool is disposed.
2471
+ */
2472
+ connect(slot: i32, nonce: u8[]): void {
2473
+ if (slot < 0 || slot >= this.size || this.slots[slot].state !== PG_SLOT_WANTED) {
2474
+ panic("nish/net/postgres: PgPool.connect on a slot that wants no connection")
2475
+ }
2476
+ if (this.password.gone) {
2477
+ panic("nish/net/postgres: PgPool.connect after dispose")
2478
+ }
2479
+ const s: PgPoolSlot = this.slots[slot]
2480
+ s.conn = new PgConnection(this.config, this.password.bytes, nonce)
2481
+ s.state = PG_SLOT_STARTING
2482
+ s.queued = 0
2483
+ s.described = 0
2484
+ s.head = 0
2485
+ s.count = 0
2486
+ this.started = true
2487
+ }
2488
+
2489
+ /** The connection in `slot`, its buffers the carrier's to fill and drain; the last one made after it fails. Panics when none was made. */
2490
+ connection(slot: i32): PgConnection {
2491
+ const conn: PgConnection | null = slot < 0 || slot >= this.size ? null : this.slots[slot].conn
2492
+ if (conn === null) {
2493
+ panic("nish/net/postgres: PgPool.connection on a slot with no connection")
2494
+ }
2495
+ return conn
2496
+ }
2497
+
2498
+ /**
2499
+ * The next event from `slot`'s input, as `PgConnection.next()` answers it,
2500
+ * `ticket` naming the request it is about (-1 for `PG_READY`, `PG_FAILED`,
2501
+ * and a notice while no request is in flight). The `PG_DESCRIBED` of a registered
2502
+ * statement is the pool's and is not answered. When the connection fails,
2503
+ * `PG_LOST` comes for each request in flight on it, oldest first, then
2504
+ * `PG_FAILED`: close its socket. A connection that fails before it is ready,
2505
+ * while no other is, takes the oldest waiting request with it, so that a
2506
+ * server that cannot be reached empties the queue rather than holds it.
2507
+ * `PG_NEED_MORE` once `PG_FAILED` was answered, until `connect` builds the
2508
+ * slot again. Panics as `connection(slot)` does.
2509
+ */
2510
+ next(slot: i32): i32 {
2511
+ const conn: PgConnection = this.connection(slot)
2512
+ const s: PgPoolSlot = this.slots[slot]
2513
+ if (s.state === PG_SLOT_IDLE || s.state === PG_SLOT_WANTED) {
2514
+ this.ticket = -1
2515
+ return PG_NEED_MORE
2516
+ }
2517
+ while (s.state === PG_SLOT_STARTING || s.state === PG_SLOT_READY) {
2518
+ this.ticket = s.count > 0 ? s.tickets[s.head] : -1
2519
+ const event: i32 = conn.next()
2520
+ if (event === PG_READY) {
2521
+ s.state = PG_SLOT_READY
2522
+ } else if (event === PG_DESCRIBED && s.count > 0 && s.leads[s.head] > 0) {
2523
+ s.leads[s.head] -= 1
2524
+ s.described += 1
2525
+ continue
2526
+ } else if (event === PG_ERROR && s.count > 0 && s.leads[s.head] > 0) {
2527
+ // The rest of the group's prepares were skipped: the next request prepares them again.
2528
+ s.leads[s.head] = 0
2529
+ s.queued = s.described
2530
+ } else if (event === PG_GROUP_DONE) {
2531
+ s.pop()
2532
+ } else if (event === PG_FAILED) {
2533
+ if (s.state === PG_SLOT_STARTING && this.queueCount > 0 && !this.anyReady()) {
2534
+ s.push(this.queue[this.queueHead], toI32(0))
2535
+ this.queueHead = (this.queueHead + 1) % toI32(this.queue.length)
2536
+ this.queueCount -= 1
2537
+ }
2538
+ if (this.building === slot) {
2539
+ this.building = -1
2540
+ }
2541
+ s.state = PG_SLOT_DRAINING
2542
+ break
2543
+ }
2544
+ return event
2545
+ }
2546
+ if (s.state === PG_SLOT_DRAINING && s.count > 0) {
2547
+ this.ticket = s.tickets[s.head]
2548
+ s.pop()
2549
+ return PG_LOST
2550
+ }
2551
+ s.state = PG_SLOT_IDLE
2552
+ this.ticket = -1
2553
+ return PG_FAILED
2554
+ }
2555
+
2556
+ private anyReady(): boolean {
2557
+ for (const slot of this.slots) {
2558
+ if (slot.state === PG_SLOT_READY) {
2559
+ return true
2560
+ }
2561
+ }
2562
+ return false
2563
+ }
2564
+
2565
+ /** Whether every byte of the pool's copy of the password is zero, as `PgConnection.passwordWiped()` reads its own. */
2566
+ passwordWiped(): boolean {
2567
+ return this.password.wiped()
2568
+ }
2569
+
2570
+ /** Wipes the pool's password and every connection's; `connect` panics after it. */
2571
+ dispose(): void {
2572
+ if (!this.password.gone) {
2573
+ this.password.wipe()
2574
+ }
2575
+ for (const slot of this.slots) {
2576
+ const conn: PgConnection | null = slot.conn
2577
+ if (conn !== null) {
2578
+ conn.dispose()
2579
+ }
2580
+ }
2581
+ }
2582
+ }
2583
+
2584
+ // --- Batching ------------------------------------------------------------------
2585
+
2586
+ /** `PgBatchConfig.keyType`: the keys are `int8`, given to `PgBatcher.addInt`. */
2587
+ export const PG_BATCH_INT: i32 = 0
2588
+ /** `PgBatchConfig.keyType`: the keys are `text`, given to `PgBatcher.addText`. */
2589
+ export const PG_BATCH_TEXT: i32 = 1
2590
+
2591
+ /** `PgBatcher.next(run)`: no event until the next row, `end` or `fail`. */
2592
+ export const PG_BATCH_NONE: i32 = 0
2593
+ /** `PgBatcher.next(run)`: the connection's current row answers the request `tag`. */
2594
+ export const PG_BATCH_ROW: i32 = 1
2595
+ /** `PgBatcher.next(run)`: the request `tag` has had all its rows, at least one. */
2596
+ export const PG_BATCH_DONE: i32 = 2
2597
+ /** `PgBatcher.next(run)`: no row answered the request `tag`'s key. */
2598
+ export const PG_BATCH_MISSING: i32 = 3
2599
+ /** `PgBatcher.next(run)`: the request `tag`'s execution failed, and its rows so far may be incomplete. */
2600
+ export const PG_BATCH_FAILED: i32 = 4
2601
+
2602
+ // `take` probes at most maxRequests^2 / 2 slots, so the cap keeps that near 5 * 10^5;
2603
+ // a probe whose hash matches in full compares the key's bytes, so the bytes
2604
+ // compared are at most that times the longest key, which `maxBytes` bounds.
2605
+ const PG_BATCH_MAX_REQUESTS: i32 = 1024
2606
+ const PG_BATCH_MAX_QUEUE: i32 = 1048576
2607
+ const PG_BATCH_MAX_RUNS: i32 = 1024
2608
+ // The takes a run's handles count before they wrap: handles stay under 2^30.
2609
+ const PG_BATCH_USES: i32 = 1048576
2610
+ // The array's `{}` and the longest int8 element, `-9223372036854775808`.
2611
+ const PG_BATCH_MIN_BYTES: i32 = 22
2612
+
2613
+ /** What a `PgBatcher` sends: one statement shape, its key, and its caps. */
2614
+ export class PgBatchConfig {
2615
+ /** The prepared statement's name, which `PgBatcher.sql` is prepared under. */
2616
+ name: string = ""
2617
+ /** The query for one key, `q.key`: `select id, title from articles where slug = q.key`. */
2618
+ select: string = ""
2619
+ /** Columns of `t`, the query's rows, ordering the rows of one key: `t.created_at desc`; "" for none. */
2620
+ orderBy: string = ""
2621
+ /** `PG_BATCH_INT` or `PG_BATCH_TEXT`. */
2622
+ keyType: i32 = 0
2623
+ /** The most requests one execution answers. */
2624
+ maxRequests: i32 = 32
2625
+ /** The most bytes the key array parameter takes, `{}` included. */
2626
+ maxBytes: i32 = 32768
2627
+ /** The most requests waiting. */
2628
+ queueSize: i32 = 1024
2629
+ /** The most bytes of text keys waiting. */
2630
+ queueBytes: i32 = 65536
2631
+ /** The most executions in flight at once. */
2632
+ runs: i32 = 16
2633
+ }
2634
+
2635
+ /** One execution's requests, grouped by key, and how far its rows have been routed. */
2636
+ class PgBatchRun {
2637
+ /** Key `k`'s requests' tags are `tags[first[k], first[k + 1])`, in the order they were added. */
2638
+ readonly tags: i32[]
2639
+ readonly first: i32[]
2640
+ keys: i32 = 0
2641
+ /** The key rows are being read for, whether it has had one, and the next of its requests to answer. */
2642
+ cur: i32 = 0
2643
+ curRows: boolean = false
2644
+ at: i32 = 0
2645
+ /** The key whose `PG_BATCH_ROW` events are owed for the connection's current row; -1 for none. */
2646
+ target: i32 = -1
2647
+ /** The handle `take` gave for the run's execution, and how many takes it has had, mod `PG_BATCH_USES`. */
2648
+ handle: i32 = -1
2649
+ uses: i32 = 0
2650
+ open: boolean = false
2651
+ ended: boolean = false
2652
+ failed: boolean = false
2653
+
2654
+ constructor(maxRequests: i32) {
2655
+ this.tags = new Array<i32>(maxRequests)
2656
+ this.first = new Array<i32>(maxRequests + 1)
2657
+ }
2658
+
2659
+ /** Opens the run for `keys` keys, none of whose rows have been read. */
2660
+ reset(keys: i32): void {
2661
+ this.open = true
2662
+ this.keys = keys
2663
+ this.cur = 0
2664
+ this.curRows = false
2665
+ this.at = 0
2666
+ this.target = -1
2667
+ this.ended = false
2668
+ this.failed = false
2669
+ }
2670
+ }
2671
+
2672
+ /**
2673
+ * Batches keyed reads of one statement shape, sans-IO like `PgConnection`:
2674
+ * it reads no clock and makes no socket. Each request is a key and the
2675
+ * caller's `tag`, queued by `addInt` or `addText`. When a connection is free,
2676
+ * `take(conn)` binds up to `maxRequests` of them, and up to `maxBytes` of
2677
+ * keys, as one execution of `sql`, each distinct key once, and answers the
2678
+ * execution's run handle. Each `PG_ROW` of that execution goes to
2679
+ * `row(run, conn)`, its `PG_COMPLETE` to `end(run)`, and an error or a lost
2680
+ * connection to `fail(run)`; after each, `next(run)` answers events until
2681
+ * `PG_BATCH_NONE`, `tag` naming the request each is about. Every row goes to
2682
+ * every request for its key, and every request gets one last event:
2683
+ * `PG_BATCH_DONE`, `PG_BATCH_MISSING` or `PG_BATCH_FAILED`.
2684
+ *
2685
+ * Nothing outlives its execution: a run is free again once the events
2686
+ * `end` or `fail` owe are answered, and its handle is then spent. What the
2687
+ * server or the network can send after that — a second `PG_COMPLETE`, a row
2688
+ * or an error after one, a lost connection — goes to a spent handle, and
2689
+ * `row`, `end` and `fail` count it in `stray` and do nothing, even once
2690
+ * `take` has given the run out again under a new handle. The ordinal each
2691
+ * row carries comes from the server and is checked before it routes
2692
+ * anything. What panics is only the program's own misuse: a handle out of
2693
+ * the range `take` can give (-1 or any negative, or `runs` * 2^20 or more),
2694
+ * or `row`, `end` or `fail` before `next(run)` has answered `PG_BATCH_NONE`
2695
+ * for the event before. A handle in that range that `take` never gave is
2696
+ * spent like any other: counted in `stray`, not a panic.
2697
+ */
2698
+ export class PgBatcher {
2699
+ /** The prepared statement's name. */
2700
+ readonly name: string
2701
+ /** The statement to prepare under `name` (`PgPool.register` or `PgConnection.prepare`, no parameter types); each row's last column is the ordinal it routes by. */
2702
+ readonly sql: string
2703
+ // The queue, a ring: each request's tag, its key's hash and encoded size,
2704
+ // and the key: an int8, or a window of `store`, where text keys sit in the
2705
+ // order they were added, from the oldest's start to `storeEnd`.
2706
+ private readonly qTags: i32[]
2707
+ private readonly qHashes: i32[]
2708
+ private readonly qSizes: i32[]
2709
+ private readonly qInts: i64[]
2710
+ private readonly qStarts: i32[]
2711
+ private readonly qLengths: i32[]
2712
+ private readonly store: u8[]
2713
+ // `take`'s open-addressed table of the keys it has bound, each a key number
2714
+ // plus one, 0 for an empty slot; and for each key, its slot and its first request.
2715
+ private readonly table: i32[]
2716
+ private readonly keySlots: i32[]
2717
+ private readonly keyEntries: i32[]
2718
+ private readonly keyCounts: i32[]
2719
+ private readonly takenKeys: i32[]
2720
+ private readonly runs: PgBatchRun[]
2721
+ /** The request the last `next(run)` answered about; -1 for none. */
2722
+ tag: i32 = -1
2723
+ /** The number of runs failed by a bad ordinal (missing, NULL, not an integer, or naming no key still open), stopping at 2^31 - 1: each counts its first refused row, and the rows after it, dropped with the failed run, are not counted. */
2724
+ refused: i32 = 0
2725
+ /** The number of `row`, `end` and `fail` calls on a spent handle, each ignored, stopping at 2^31 - 1. */
2726
+ stray: i32 = 0
2727
+ private readonly keyType: i32
2728
+ private readonly maxRequests: i32
2729
+ private readonly maxBytes: i32
2730
+ private storeEnd: i32 = 0
2731
+ private head: i32 = 0
2732
+ private count: i32 = 0
2733
+
2734
+ /** A batcher for `config`; panics when a name or query is empty or holds a NUL, or a cap is out of range. */
2735
+ constructor(config: PgBatchConfig) {
2736
+ const keyType: i32 = config.keyType
2737
+ if (keyType !== PG_BATCH_INT && keyType !== PG_BATCH_TEXT) {
2738
+ panic("nish/net/postgres: a PgBatchConfig keyType out of range")
2739
+ }
2740
+ if (
2741
+ config.name.length === 0 ||
2742
+ config.select.length === 0 ||
2743
+ pgHasNul(config.name) ||
2744
+ pgHasNul(config.select) ||
2745
+ pgHasNul(config.orderBy)
2746
+ ) {
2747
+ panic("nish/net/postgres: a PgBatchConfig name or query empty or with a NUL")
2748
+ }
2749
+ const maxRequests: i32 = config.maxRequests
2750
+ if (
2751
+ maxRequests < 1 ||
2752
+ maxRequests > PG_BATCH_MAX_REQUESTS ||
2753
+ config.maxBytes < PG_BATCH_MIN_BYTES ||
2754
+ config.maxBytes > PG_MAX_CAP ||
2755
+ config.queueSize < 1 ||
2756
+ config.queueSize > PG_BATCH_MAX_QUEUE ||
2757
+ config.queueBytes < 0 ||
2758
+ config.queueBytes > PG_MAX_CAP ||
2759
+ config.runs < 1 ||
2760
+ config.runs > PG_BATCH_MAX_RUNS
2761
+ ) {
2762
+ panic("nish/net/postgres: a PgBatchConfig cap out of range")
2763
+ }
2764
+ this.keyType = keyType
2765
+ this.maxRequests = maxRequests
2766
+ this.maxBytes = config.maxBytes
2767
+ this.name = config.name
2768
+ const type: string = keyType === PG_BATCH_INT ? "int8" : "text"
2769
+ const order: string = config.orderBy.length > 0 ? `, ${config.orderBy}` : ""
2770
+ this.sql = `select t.*, q.ord from unnest($1::${type}[]) with ordinality as q(key, ord) cross join lateral (${config.select}) as t order by q.ord${order}`
2771
+ const queueSize: i32 = config.queueSize
2772
+ this.qTags = new Array<i32>(queueSize)
2773
+ this.qHashes = new Array<i32>(queueSize)
2774
+ this.qSizes = new Array<i32>(queueSize)
2775
+ const text: boolean = keyType === PG_BATCH_TEXT
2776
+ const intSize: i32 = text ? 0 : queueSize
2777
+ const textSize: i32 = text ? queueSize : 0
2778
+ const storeSize: i32 = text ? config.queueBytes : 0
2779
+ this.qInts = new Array<i64>(intSize)
2780
+ this.qStarts = new Array<i32>(textSize)
2781
+ this.qLengths = new Array<i32>(textSize)
2782
+ this.store = new Array<u8>(storeSize)
2783
+ // At least twice the keys a take can hold, a power of two for the mask.
2784
+ let tableSize: i32 = 2
2785
+ while (tableSize < 2 * maxRequests) {
2786
+ tableSize *= 2
2787
+ }
2788
+ this.table = new Array<i32>(tableSize)
2789
+ this.keySlots = new Array<i32>(maxRequests)
2790
+ this.keyEntries = new Array<i32>(maxRequests)
2791
+ this.keyCounts = new Array<i32>(maxRequests)
2792
+ this.takenKeys = new Array<i32>(maxRequests)
2793
+ this.runs = []
2794
+ for (let r: i32 = 0; r < config.runs; r++) {
2795
+ this.runs.push(new PgBatchRun(maxRequests))
2796
+ }
2797
+ }
2798
+
2799
+ /** The number of requests waiting for `take`. */
2800
+ waiting(): i32 {
2801
+ return this.count
2802
+ }
2803
+
2804
+ /** Queues a request for the int8 `key`; false when `queueSize` are waiting. Panics on a text batcher. */
2805
+ addInt(key: i64, tag: i32): boolean {
2806
+ if (this.keyType !== PG_BATCH_INT) {
2807
+ panic("nish/net/postgres: PgBatcher.addInt on a text batcher")
2808
+ }
2809
+ const q: i32 = this.slotToAdd()
2810
+ if (q < 0) {
2811
+ return false
2812
+ }
2813
+ // Both halves folded to 31 bits, so that no step can overflow.
2814
+ const h: i32 = toI32(key & toI64(0x7fffffff)) ^ toI32((key >> toI64(31)) & toI64(0x7fffffff))
2815
+ this.qInts[q] = key
2816
+ this.added(q, tag, h ^ (h >> 15), pgDecimalLength(key))
2817
+ return true
2818
+ }
2819
+
2820
+ /**
2821
+ * Queues a request for the text key `bytes[start, start + length)`,
2822
+ * which is copied; false when the key is not UTF-8 or holds a NUL (the
2823
+ * server would fail the whole execution over it), `queueSize` are waiting,
2824
+ * the key would not fit in `queueBytes` beside those waiting, or it is too
2825
+ * long for one execution's `maxBytes`. Panics on an int8 batcher, or when
2826
+ * the window is not inside `bytes`.
2827
+ */
2828
+ addText(bytes: u8[], start: i32, length: i32, tag: i32): boolean {
2829
+ if (this.keyType !== PG_BATCH_TEXT) {
2830
+ panic("nish/net/postgres: PgBatcher.addText on an int8 batcher")
2831
+ }
2832
+ if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
2833
+ panic("nish/net/postgres: PgBatcher.addText outside its source")
2834
+ }
2835
+ const end: i32 = start + length
2836
+ if (!pgIsText(bytes, start, end)) {
2837
+ return false
2838
+ }
2839
+ // The element as `arrayElement` writes it: quoted, `"` and `\` escaped;
2840
+ // and FNV-1a, whose unsigned multiply wraps by definition.
2841
+ let size: i64 = toI64(length) + 2
2842
+ let h: u32 = 2166136261
2843
+ for (let i: i32 = start; i < end && i < toI32(bytes.length); i++) {
2844
+ const c: u8 = bytes[i]
2845
+ if (toI32(c) === PG_QUOTE || toI32(c) === PG_BACKSLASH) {
2846
+ size += 1
2847
+ }
2848
+ h = (h ^ toU32(c)) * 16777619
2849
+ }
2850
+ if (size + 2 > toI64(this.maxBytes)) {
2851
+ return false
2852
+ }
2853
+ const q: i32 = this.slotToAdd()
2854
+ if (q < 0) {
2855
+ return false
2856
+ }
2857
+ let at: i32 = this.storeEnd
2858
+ if (length > toI32(this.store.length) - at) {
2859
+ // The waiting keys are moved to the front, the room the taken ones left after them.
2860
+ // Refused first when even that would not make room, so a full store is not moved for nothing.
2861
+ const from: i32 = this.count > 0 ? this.qStarts[this.head] : at
2862
+ if (length > toI32(this.store.length) - (at - from)) {
2863
+ return false
2864
+ }
2865
+ copyBytes(this.store, 0, this.store, from, at - from)
2866
+ for (let i: i32 = 0; i < this.count; i++) {
2867
+ const w: i32 = (this.head + i) % toI32(this.qStarts.length)
2868
+ this.qStarts[w] -= from
2869
+ }
2870
+ at -= from
2871
+ this.storeEnd = at
2872
+ }
2873
+ copyBytes(this.store, at, bytes, start, length)
2874
+ this.storeEnd = at + length
2875
+ this.qStarts[q] = at
2876
+ this.qLengths[q] = length
2877
+ this.added(q, tag, toI32(h >> 1), toI32(size))
2878
+ return true
2879
+ }
2880
+
2881
+ /** The ring slot the next request goes in, or -1 when the queue is full. */
2882
+ private slotToAdd(): i32 {
2883
+ const size: i32 = toI32(this.qTags.length)
2884
+ return this.count === size ? -1 : (this.head + this.count) % size
2885
+ }
2886
+
2887
+ private added(q: i32, tag: i32, hash: i32, size: i32): void {
2888
+ this.qTags[q] = tag
2889
+ this.qHashes[q] = hash
2890
+ this.qSizes[q] = size
2891
+ this.count += 1
2892
+ }
2893
+
2894
+ /** Whether the keys of the queued requests `a` and `b` are equal. */
2895
+ private sameKey(a: i32, b: i32): boolean {
2896
+ if (this.keyType === PG_BATCH_INT) {
2897
+ return this.qInts[a] === this.qInts[b]
2898
+ }
2899
+ const n: i32 = this.qLengths[a]
2900
+ if (n !== this.qLengths[b]) {
2901
+ return false
2902
+ }
2903
+ const x: i32 = this.qStarts[a]
2904
+ const y: i32 = this.qStarts[b]
2905
+ for (let i: i32 = 0; i < n; i++) {
2906
+ if (this.store[x + i] !== this.store[y + i]) {
2907
+ return false
2908
+ }
2909
+ }
2910
+ return true
2911
+ }
2912
+
2913
+ /**
2914
+ * Binds the oldest waiting requests on `conn` as one execution of `sql`,
2915
+ * prepared there already, and answers its run's handle, which names this
2916
+ * execution and no later one of the same run; the caller ends the group
2917
+ * (`sync()`, or `PgPool.send()`). It takes up to `maxRequests` requests,
2918
+ * oldest first, and stops at the first whose key would pass `maxBytes` or
2919
+ * the connection's `maxOutput`; those not taken stay queued. -1, and
2920
+ * nothing written, when none waits, every run is in flight, or `conn`
2921
+ * refused the bind or the first key.
2922
+ */
2923
+ take(conn: PgConnection): i32 {
2924
+ if (this.count === 0) {
2925
+ return -1
2926
+ }
2927
+ let r: i32 = -1
2928
+ for (let i: i32 = 0; i < toI32(this.runs.length); i++) {
2929
+ if (!this.runs[i].open) {
2930
+ r = i
2931
+ break
2932
+ }
2933
+ }
2934
+ if (r < 0 || !conn.bindStart(this.name)) {
2935
+ return -1
2936
+ }
2937
+ if (!conn.arrayStart()) {
2938
+ conn.bindCancel()
2939
+ return -1
2940
+ }
2941
+ const ring: i32 = toI32(this.qTags.length)
2942
+ const mask: i32 = toI32(this.table.length) - 1
2943
+ let keys: i32 = 0
2944
+ let taken: i32 = 0
2945
+ let bytes: i32 = 2
2946
+ while (taken < this.count && taken < this.maxRequests && taken < toI32(this.takenKeys.length)) {
2947
+ const q: i32 = (this.head + taken) % ring
2948
+ // Probe for the key: a slot holding it, or the empty slot it would take.
2949
+ let slot: i32 = this.qHashes[q] & mask
2950
+ let k: i32 = -1
2951
+ while (this.table[slot] > 0) {
2952
+ const seen: i32 = this.table[slot] - 1
2953
+ const e: i32 = this.keyEntries[seen]
2954
+ if (this.qHashes[e] === this.qHashes[q] && this.sameKey(e, q)) {
2955
+ k = seen
2956
+ break
2957
+ }
2958
+ slot = (slot + 1) & mask
2959
+ }
2960
+ if (k < 0) {
2961
+ const cost: i32 = this.qSizes[q] + (keys > 0 ? 1 : 0)
2962
+ if (cost > this.maxBytes - bytes || !this.bindKey(conn, q)) {
2963
+ break
2964
+ }
2965
+ bytes += cost
2966
+ k = keys
2967
+ keys += 1
2968
+ this.table[slot] = keys
2969
+ this.keySlots[k] = slot
2970
+ this.keyEntries[k] = q
2971
+ this.keyCounts[k] = 0
2972
+ }
2973
+ this.keyCounts[k] += 1
2974
+ this.takenKeys[taken] = k
2975
+ taken += 1
2976
+ }
2977
+ for (let k: i32 = 0; k < keys && k < toI32(this.keySlots.length); k++) {
2978
+ this.table[this.keySlots[k]] = 0
2979
+ }
2980
+ if (taken === 0) {
2981
+ conn.bindCancel()
2982
+ return -1
2983
+ }
2984
+ conn.arrayEnd()
2985
+ conn.bindEnd()
2986
+ // The requests grouped by key, each key's in the order they were added.
2987
+ const run: PgBatchRun = this.runs[r]
2988
+ run.first[0] = 0
2989
+ for (let k: i32 = 0; k < keys && k < toI32(this.keyCounts.length); k++) {
2990
+ run.first[k + 1] = run.first[k] + this.keyCounts[k]
2991
+ this.keyCounts[k] = run.first[k]
2992
+ }
2993
+ for (let i: i32 = 0; i < taken && i < toI32(this.takenKeys.length); i++) {
2994
+ const k: i32 = this.takenKeys[i]
2995
+ run.tags[this.keyCounts[k]] = this.qTags[(this.head + i) % ring]
2996
+ this.keyCounts[k] += 1
2997
+ }
2998
+ run.reset(keys)
2999
+ run.handle = run.uses * toI32(this.runs.length) + r
3000
+ run.uses = (run.uses + 1) % PG_BATCH_USES
3001
+ this.head = (this.head + taken) % ring
3002
+ this.count -= taken
3003
+ if (this.count === 0) {
3004
+ this.storeEnd = 0
3005
+ }
3006
+ return run.handle
3007
+ }
3008
+
3009
+ /** Writes the queued request `q`'s key as the open array's next element; false when `conn` has no room. */
3010
+ private bindKey(conn: PgConnection, q: i32): boolean {
3011
+ if (this.keyType === PG_BATCH_INT) {
3012
+ return conn.arrayInt(this.qInts[q])
3013
+ }
3014
+ return conn.arrayElement(this.store, this.qStarts[q], this.qLengths[q])
3015
+ }
3016
+
3017
+ /** The run handle `h` names while its execution's events are owed; null once it is spent. Panics on a handle `take` cannot give. */
3018
+ private run(h: i32): PgBatchRun | null {
3019
+ const n: i32 = toI32(this.runs.length)
3020
+ if (h < 0 || h >= n * PG_BATCH_USES) {
3021
+ panic("nish/net/postgres: a PgBatcher run out of range")
3022
+ }
3023
+ const run: PgBatchRun = this.runs[h % n]
3024
+ if (run.handle !== h || !run.open) {
3025
+ return null
3026
+ }
3027
+ return run
3028
+ }
3029
+
3030
+ /** The run handle `h` names, not ended and with its events all answered; null, counted in `stray`, once it is spent. */
3031
+ private quietRun(h: i32, what: string): PgBatchRun | null {
3032
+ const run: PgBatchRun | null = this.run(h)
3033
+ if (run === null) {
3034
+ if (this.stray < 2147483647) {
3035
+ this.stray += 1
3036
+ }
3037
+ return null
3038
+ }
3039
+ if (run.target >= 0 || run.ended) {
3040
+ panic(`nish/net/postgres: PgBatcher.${what} before next(run) answered PG_BATCH_NONE`)
3041
+ }
3042
+ return run
3043
+ }
3044
+
3045
+ /**
3046
+ * Routes `conn`'s current row, of run `r`'s execution, by the ordinal in
3047
+ * its last column: `next(r)` then answers `PG_BATCH_ROW` for each request
3048
+ * of its key, after the last events of the keys before it. False, and the
3049
+ * row dropped, when the run has failed; and when the ordinal is NULL, not
3050
+ * an integer, or names no key still open (one out of range, or one before
3051
+ * the key the last row was for): that counts in `refused` and fails the
3052
+ * run; and on a spent handle, which counts in `stray`. Panics on a handle
3053
+ * `take` cannot give, or when the run's events are not all answered.
3054
+ */
3055
+ row(r: i32, conn: PgConnection): boolean {
3056
+ const run: PgBatchRun | null = this.quietRun(r, "row")
3057
+ if (run === null) {
3058
+ return false
3059
+ }
3060
+ if (run.failed) {
3061
+ return false
3062
+ }
3063
+ const last: i32 = conn.columns - 1
3064
+ let ordinal: i64 = 0
3065
+ let bad: boolean = last < 0 || conn.isNull(last)
3066
+ if (!bad) {
3067
+ // Read without touching `text.failed`, which belongs to the program's own decoding.
3068
+ const failed: boolean = conn.text.failed
3069
+ conn.text.failed = false
3070
+ ordinal = conn.int(last)
3071
+ bad = conn.text.failed
3072
+ conn.text.failed = failed
3073
+ }
3074
+ // Ordinals count from 1 and come in order: a row may name the current key again, never one before it.
3075
+ if (bad || ordinal < toI64(run.cur) + 1 || ordinal > toI64(run.keys)) {
3076
+ if (this.refused < 2147483647) {
3077
+ this.refused += 1
3078
+ }
3079
+ run.failed = true
3080
+ return false
3081
+ }
3082
+ run.target = toI32(ordinal) - 1
3083
+ return true
3084
+ }
3085
+
3086
+ /** Ends run `r`'s execution, on its `PG_COMPLETE`: `next(r)` then closes every request left. Ignores a spent handle, and panics, as `row` does. */
3087
+ end(r: i32): void {
3088
+ const run: PgBatchRun | null = this.quietRun(r, "end")
3089
+ if (run !== null) {
3090
+ run.ended = true
3091
+ }
3092
+ }
3093
+
3094
+ /** Fails run `r`'s execution, on its `PG_ERROR` or `PG_LOST`: `next(r)` then answers `PG_BATCH_FAILED` for every request left. Ignores a spent handle, and panics, as `row` does. */
3095
+ fail(r: i32): void {
3096
+ const run: PgBatchRun | null = this.quietRun(r, "fail")
3097
+ if (run !== null) {
3098
+ run.failed = true
3099
+ run.ended = true
3100
+ }
3101
+ }
3102
+
3103
+ // Names the current key's next unanswered request as `tag`; false when it has none left.
3104
+ private emit(run: PgBatchRun): boolean {
3105
+ const at: i32 = run.first[run.cur] + run.at
3106
+ if (at >= run.first[run.cur + 1]) {
3107
+ return false
3108
+ }
3109
+ run.at += 1
3110
+ this.tag = run.tags[at]
3111
+ return true
3112
+ }
3113
+
3114
+ /**
3115
+ * The next event `row`, `end` or `fail` owes on run `r`, with `tag` the
3116
+ * request it is about; `PG_BATCH_NONE`, and `tag` -1, when there is none.
3117
+ * Once `end` or `fail` has had all its events the run is over, free for
3118
+ * `take` again, and its handle spent: `PG_BATCH_NONE` ever after. Panics
3119
+ * on a handle `take` cannot give.
3120
+ */
3121
+ next(r: i32): i32 {
3122
+ const found: PgBatchRun | null = this.run(r)
3123
+ this.tag = -1
3124
+ if (found === null) {
3125
+ return PG_BATCH_NONE
3126
+ }
3127
+ const run: PgBatchRun = found
3128
+ // The keys before the row's, or every key once the execution is over, have had all their rows.
3129
+ let limit: i32 = run.cur
3130
+ if (run.target >= 0) {
3131
+ limit = run.target
3132
+ } else if (run.ended) {
3133
+ limit = run.keys
3134
+ }
3135
+ while (run.cur < limit && run.cur < toI32(run.tags.length)) {
3136
+ if (this.emit(run)) {
3137
+ if (run.failed) {
3138
+ return PG_BATCH_FAILED
3139
+ }
3140
+ return run.curRows ? PG_BATCH_DONE : PG_BATCH_MISSING
3141
+ }
3142
+ run.cur += 1
3143
+ run.at = 0
3144
+ run.curRows = false
3145
+ }
3146
+ if (run.target >= 0 && run.cur < toI32(run.tags.length)) {
3147
+ run.curRows = true
3148
+ if (this.emit(run)) {
3149
+ return PG_BATCH_ROW
3150
+ }
3151
+ run.target = -1
3152
+ run.at = 0
3153
+ }
3154
+ if (run.ended && run.cur >= run.keys) {
3155
+ run.open = false
3156
+ }
3157
+ return PG_BATCH_NONE
3158
+ }
3159
+ }