@amritk/nish-x86_64-linux 0.22.1 → 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.
- package/bin/nish +0 -0
- package/docs/AI.md +32 -0
- package/package.json +1 -1
- package/std/README.md +1 -1
- package/std/net/postgres.ts +868 -3
package/bin/nish
CHANGED
|
Binary file
|
package/docs/AI.md
CHANGED
|
@@ -2584,6 +2584,38 @@ most 100 pairs. (No block, as for `nish/text`; `tests/link/std_url` is its
|
|
|
2584
2584
|
program, and the user guide's [standard-library
|
|
2585
2585
|
page](guide/using-nish/standard-library.md#nishneturl) its walk-through.)
|
|
2586
2586
|
|
|
2587
|
+
**Pass a list to a PostgreSQL statement** — there is no driver that expands
|
|
2588
|
+
`IN (...)` and no `pg`'s array serialiser: in `nish/net/postgres`, write
|
|
2589
|
+
`= any($1::int8[])` or `unnest($1::text[])` and bind the list as one
|
|
2590
|
+
parameter. `paramIntArray(values, start, count)` binds an `i64[]` window;
|
|
2591
|
+
`arrayStart()`, `arrayElement(bytes, start, length)` or `arrayNull()` per
|
|
2592
|
+
element, then `arrayEnd()`, builds a `text[]` straight into the Bind from
|
|
2593
|
+
windows of a buffer. An element refused for room answers `false` and leaves
|
|
2594
|
+
the array open: end it and send the rest in the next statement. Every other
|
|
2595
|
+
`param...` call and `bindEnd()` panic until `arrayEnd()`. (No block, as for
|
|
2596
|
+
`nish/text`; `tests/postgres/live` runs it against a server, and the user
|
|
2597
|
+
guide's [PostgreSQL page](guide/using-nish/postgres.md#array-parameters) its
|
|
2598
|
+
walk-through.)
|
|
2599
|
+
|
|
2600
|
+
**Batch a server's keyed reads into one PostgreSQL query** — there is no
|
|
2601
|
+
DataLoader and no `setTimeout` to collect a tick's keys: in `nish/net/postgres`,
|
|
2602
|
+
a `PgBatcher` built from a `PgBatchConfig` (`name`, `select` reading the key as
|
|
2603
|
+
`q.key`, `keyType` `PG_BATCH_INT` or `PG_BATCH_TEXT`, caps) queues requests with
|
|
2604
|
+
`addInt(key, tag)` or `addText(bytes, start, length, tag)` — `false` when its
|
|
2605
|
+
queue is full, or for a text key too long for `maxBytes`, with no room left in
|
|
2606
|
+
`queueBytes`, with a NUL or not UTF-8 — and `take(conn)`
|
|
2607
|
+
binds the waiting ones, each key once, as one execution of its `sql` over
|
|
2608
|
+
`unnest($1) with ordinality`, answering a run handle or -1. Hand that
|
|
2609
|
+
execution's `PG_ROW` to `row(run, conn)`, `PG_COMPLETE` to `end(run)`, an error
|
|
2610
|
+
or `PG_LOST` to `fail(run)` (on a spent handle these do nothing and count in
|
|
2611
|
+
`stray`), and after each call `next(run)` until
|
|
2612
|
+
`PG_BATCH_NONE`: `PG_BATCH_ROW` (the connection's row answers request `tag`),
|
|
2613
|
+
then one of `PG_BATCH_DONE`, `PG_BATCH_MISSING` or `PG_BATCH_FAILED` per
|
|
2614
|
+
request. It reads no clock: when to take is the loop's choice. (No block;
|
|
2615
|
+
`tests/postgres/live`'s `batch` scenario runs it against a server, and the user
|
|
2616
|
+
guide's [PostgreSQL page](guide/using-nish/postgres.md#batching-keyed-reads)
|
|
2617
|
+
its walk-through.)
|
|
2618
|
+
|
|
2587
2619
|
**Draw to the whole terminal** (a game, a dashboard) — there is no curses and
|
|
2588
2620
|
no `process.stdout.on("resize")`: `runTerminal(options, state, update, render)`
|
|
2589
2621
|
from `nish/term-io` opens the terminal, probes what it can draw, steps
|
package/package.json
CHANGED
package/std/README.md
CHANGED
|
@@ -202,7 +202,7 @@ reads one, and it still takes its randomness as an argument.
|
|
|
202
202
|
| [`net/http3-server.ts`](./net/http3-server.ts) | `Http3Server`: HTTP/3 over QUIC on one UDP socket for a program that owns its loop — a pool of `QuicConnection` slots with an `Http3Connection` each, a slot's built on the first Initial accepted into it, slots 0 up, and reused, while the connection-ID index, the timer wheel and the rings are sized from the cap at start-up; a `QuicListener` in front; datagrams routed by a hashed index of every slot's connection IDs; `receive(now, key)`, `tick(now)`, `ready()` for the next slot with news, `flush(now)` sending paced GSO flights, `timeout(now)` from a timer wheel. A handshake that did not choose ALPN `h3` is closed with no_application_protocol (RFC 9114 §3.1); a slot done after GOAWAY is closed and freed. Every slot's final responses carry the server's one `HttpDate` (`date`), refreshed at `connection(slot)` and `ready()` and formatted at most once a second. `stop()` releases every connection and the listener and wipes the configuration's stateless reset and Retry token keys, which are the server's once handed in, and its last answer; the keys are zeroed in place, so a `QuicServerConfig` serves one server (QUIC-5). Native only, as `nish:net` is | A Nish client across loopback (`tests/link/net_http3_server`): requests, a paced 300 KB response, GOAWAY and the slot freed, ALPN, a full pool, the arena over connections through one slot while the pinned date moves a second on for each, one date formatted for two slots; every key read back as zero after `stop()` (`tests/link/net_quic_server_wipes`); aioquic and `curl --http3` against its `serve` mode |
|
|
203
203
|
| [`net/webtransport.ts`](./net/webtransport.ts) | `WebTransport`, the server side of WebTransport over HTTP/3 as draft-ietf-webtrans-http3-02 has it and Chrome and `wtransport` 0.7 negotiate it, a layer over one `Http3Connection` whose `Http3Config.webtransportSessions` turns it on (extended CONNECT, SETTINGS_H3_DATAGRAM and the draft's two WebTransport settings): `next()` answers `WT_SESSION` (the program `accept`s or `refuse`s it), `WT_DATAGRAM`, `WT_STREAM`, `WT_STREAM_DATA`, `WT_STREAM_END`, `WT_STREAM_RESET`, `WT_STREAM_STOPPED`, `WT_WRITABLE`, `WT_DRAIN` and `WT_CLOSED`, and passes a plain request's `H3_*` events through; `sendDatagram` (the quarter stream ID, then the payload), `openStream`, `write`, `resetStream`, `stopSending`, `close` (CLOSE_WEBTRANSPORT_SESSION) and `drain`. Streams that arrive before their session wait, bounded; every cap is fixed at start-up, and a warm session allocates nothing per datagram or stream (`docs/security/webtransport.md`) | RFC 9220, RFC 9297 and draft-02: a Nish client over QUIC — sessions, datagrams at the size limits, streams both ways, close and drain from either side, every refusal — and one exchange recorded from a `wtransport` 0.7 client replayed byte for byte (`tests/link/net_webtransport`) |
|
|
204
204
|
| [`net/router.ts`](./net/router.ts) | `Router`, a method and path router that knows no carrier and allocates nothing per match: `add(method, pattern)` answers the route's index (0, 1, 2 in the order added) or a refusal — `ROUTER_BAD_PATTERN`, `ROUTER_BAD_METHOD`, `ROUTER_TOO_LONG`, `ROUTER_AMBIGUOUS` (`*` not last), `ROUTER_DUPLICATE` (the same shape and method, whatever the parameter names) or `ROUTER_FULL`, against `RouterLimits`; `match(method, mStart, mEnd, target, tStart, tEnd, params)` answers the index, `ROUTER_NOT_FOUND` or `ROUTER_METHOD_NOT_ALLOWED`, writing each `:name` and a final `*` as a `[start, end)` pair into the caller's `params` (`ROUTER_TOO_MANY_PARAMS` when it is short). Literal before parameter before `*`, segment by segment, backtracking; a lookup enters each node at most once. The query is left unparsed and nothing is decoded: `routerDecode` decodes one parameter (`ROUTER_BAD_ESCAPE`, `ROUTER_NO_ROOM`) (`docs/security/router.md`) | Every refusal code, precedence and backtracking, 405 against 404, `%2F` kept in one segment, the query, `*` and authority-form targets, a target deeper than any route, the lookup bound, params past storage, and the arena flat over matches (`tests/link/net_router`, `net_router_f64`, five `net_router_negative_*`) |
|
|
205
|
-
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramNull`, `bindEnd`, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`) |
|
|
205
|
+
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first, to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`) |
|
|
206
206
|
|
|
207
207
|
**How a carrier drives `TlsServer`.** The server speaks in handshake
|
|
208
208
|
*messages*, tagged by level: `TLS_LEVEL_INITIAL` (cleartext — ClientHello,
|
package/std/net/postgres.ts
CHANGED
|
@@ -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
|
|
@@ -104,6 +109,21 @@
|
|
|
104
109
|
* is a whole carrier: one `pollWait` loop holding the pool's sockets beside an
|
|
105
110
|
* HTTP listener.
|
|
106
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
|
+
*
|
|
107
127
|
* Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
|
|
108
128
|
* and RFC 5802 and RFC 7677; not ported from another implementation.
|
|
109
129
|
*/
|
|
@@ -349,6 +369,17 @@ const pgBytesOf = (text: string): u8[] => {
|
|
|
349
369
|
return out
|
|
350
370
|
}
|
|
351
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
|
+
|
|
352
383
|
/** Whether `text` holds a NUL byte, which a protocol string cannot. */
|
|
353
384
|
const pgHasNul = (text: string): boolean => {
|
|
354
385
|
const n: i32 = toI32(text.length)
|
|
@@ -360,6 +391,61 @@ const pgHasNul = (text: string): boolean => {
|
|
|
360
391
|
return false
|
|
361
392
|
}
|
|
362
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
|
+
|
|
363
449
|
/** The first `byte` in `b[from, end)`, or `end`. */
|
|
364
450
|
const pgFind = (b: u8[], from: i32, end: i32, byte: i32): i32 => {
|
|
365
451
|
if (from < 0 || end > toI32(b.length)) {
|
|
@@ -775,9 +861,14 @@ export class PgConnection {
|
|
|
775
861
|
private groupOpen: boolean = false
|
|
776
862
|
/** Whether a statement in the group `PG_GROUP_DONE` just finished failed. */
|
|
777
863
|
groupFailed: boolean = false
|
|
864
|
+
// Whether the open bind opened its group, which `bindCancel` closes again.
|
|
865
|
+
private bindOpened: boolean = false
|
|
778
866
|
private bindAt: i32 = -1
|
|
779
867
|
private paramCountAt: i32 = 0
|
|
780
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
|
|
781
872
|
private clientNonce: string = ""
|
|
782
873
|
private serverSignature: u8[]
|
|
783
874
|
|
|
@@ -1516,6 +1607,9 @@ export class PgConnection {
|
|
|
1516
1607
|
this.bindAt -= shift
|
|
1517
1608
|
this.paramCountAt -= shift
|
|
1518
1609
|
}
|
|
1610
|
+
if (this.arrayAt >= 0) {
|
|
1611
|
+
this.arrayAt -= shift
|
|
1612
|
+
}
|
|
1519
1613
|
}
|
|
1520
1614
|
|
|
1521
1615
|
/** Makes the output buffer at least `want` bytes long. */
|
|
@@ -1646,9 +1740,11 @@ export class PgConnection {
|
|
|
1646
1740
|
* order, then `bindEnd()`. False as `prepare` is.
|
|
1647
1741
|
*/
|
|
1648
1742
|
bindStart(name: string): boolean {
|
|
1743
|
+
const opens: boolean = !this.groupOpen
|
|
1649
1744
|
if (pgHasNul(name) || toI32(name.length) > PG_MAX_CAP || !this.canOpen(11 + toI32(name.length))) {
|
|
1650
1745
|
return false
|
|
1651
1746
|
}
|
|
1747
|
+
this.bindOpened = opens
|
|
1652
1748
|
this.bindAt = this.startMessage(PG_FE_BIND)
|
|
1653
1749
|
this.putByte(PG_ZERO)
|
|
1654
1750
|
this.putCString(name)
|
|
@@ -1665,6 +1761,7 @@ export class PgConnection {
|
|
|
1665
1761
|
* bind has 65535 parameters already, or there is no room under `maxOutput`.
|
|
1666
1762
|
*/
|
|
1667
1763
|
private startParam(size: i32): i32 {
|
|
1764
|
+
this.noArrayOpen()
|
|
1668
1765
|
if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size)) {
|
|
1669
1766
|
return -1
|
|
1670
1767
|
}
|
|
@@ -1678,6 +1775,12 @@ export class PgConnection {
|
|
|
1678
1775
|
setI32BE(this.out, at, this.outEnd - at - 4)
|
|
1679
1776
|
}
|
|
1680
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
|
+
|
|
1681
1784
|
// Every `param...` call answers false, and writes nothing, when no bind is
|
|
1682
1785
|
// open, the bind has 65535 parameters already, or the parameter would pass
|
|
1683
1786
|
// `maxOutput`; a program that checked `bindStart()` meets only the last.
|
|
@@ -1695,6 +1798,7 @@ export class PgConnection {
|
|
|
1695
1798
|
|
|
1696
1799
|
/** A text parameter from `bytes[start, start + length)`, UTF-8; false too when the window is not inside `bytes`. */
|
|
1697
1800
|
paramBytes(bytes: u8[], start: i32, length: i32): boolean {
|
|
1801
|
+
this.noArrayOpen()
|
|
1698
1802
|
if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
|
|
1699
1803
|
return false
|
|
1700
1804
|
}
|
|
@@ -1724,6 +1828,13 @@ export class PgConnection {
|
|
|
1724
1828
|
if (at < 0) {
|
|
1725
1829
|
return false
|
|
1726
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 {
|
|
1727
1838
|
this.reserve(this.outEnd + 20)
|
|
1728
1839
|
// The digits are written from the least significant, then reversed; the
|
|
1729
1840
|
// value is made negative first so that the most negative i64 has its digits.
|
|
@@ -1750,6 +1861,39 @@ export class PgConnection {
|
|
|
1750
1861
|
lo++
|
|
1751
1862
|
hi--
|
|
1752
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)
|
|
1753
1897
|
this.endParam(at)
|
|
1754
1898
|
return true
|
|
1755
1899
|
}
|
|
@@ -1795,6 +1939,124 @@ export class PgConnection {
|
|
|
1795
1939
|
return true
|
|
1796
1940
|
}
|
|
1797
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
|
+
|
|
1798
2060
|
/**
|
|
1799
2061
|
* Ends the Bind (every result column in text) and queues its Execute, for
|
|
1800
2062
|
* every row. False, and nothing written, when no bind is open; never for
|
|
@@ -1804,6 +2066,9 @@ export class PgConnection {
|
|
|
1804
2066
|
if (this.bindAt < 0) {
|
|
1805
2067
|
return false
|
|
1806
2068
|
}
|
|
2069
|
+
if (this.arrayAt >= 0) {
|
|
2070
|
+
panic("nish/net/postgres: bindEnd() while an array is open")
|
|
2071
|
+
}
|
|
1807
2072
|
this.tailRoom(PG_TAIL)
|
|
1808
2073
|
const at: i32 = this.bindAt
|
|
1809
2074
|
setU16BE(this.out, this.paramCountAt, toU16(this.paramCount))
|
|
@@ -1817,6 +2082,30 @@ export class PgConnection {
|
|
|
1817
2082
|
return true
|
|
1818
2083
|
}
|
|
1819
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
|
+
|
|
1820
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. */
|
|
1821
2110
|
sync(): boolean {
|
|
1822
2111
|
if (!this.groupOpen || this.bindAt >= 0) {
|
|
@@ -1832,8 +2121,7 @@ export class PgConnection {
|
|
|
1832
2121
|
/** Terminate: tells the server the connection is ending; close the socket once it is sent. A bind still open is dropped unsent. */
|
|
1833
2122
|
terminate(): void {
|
|
1834
2123
|
if (this.bindAt >= 0) {
|
|
1835
|
-
this.
|
|
1836
|
-
this.bindAt = -1
|
|
2124
|
+
this.dropBind()
|
|
1837
2125
|
}
|
|
1838
2126
|
this.tailRoom(toI32(5))
|
|
1839
2127
|
const at: i32 = this.startMessage(PG_FE_TERMINATE)
|
|
@@ -2292,3 +2580,580 @@ export class PgPool {
|
|
|
2292
2580
|
}
|
|
2293
2581
|
}
|
|
2294
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
|
+
}
|