@amritk/nish-x86_64-linux 0.29.0 → 0.31.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/INSTALL.md +3 -3
- package/bin/nish +0 -0
- package/docs/AI.md +71 -4
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -0
- package/runtime/core/host.mjs +9 -0
- package/runtime/nish.d.ts +48 -2
- package/runtime/nish.h +31 -3
- package/runtime/nish.mjs +36 -0
- package/runtime/runtime-net.c +32 -3
- package/runtime/runtime-os.c +33 -1
- package/runtime/runtime-region.c +243 -0
- package/runtime/runtime.c +19 -2
- package/scripts/build.sh +19 -6
- package/std/README.md +10 -7
- package/std/net/{http-client.ts → fetch.ts} +17 -17
- package/std/net/handler.ts +1 -1
- package/std/net/{http1-client.ts → http1-client-codec.ts} +276 -47
- package/std/net/{http1.ts → http1-codec.ts} +252 -49
- package/std/net/http1-server.ts +72 -13
- package/std/net/outbound.ts +2 -2
- package/std/net/postgres-carrier.ts +985 -0
- package/std/net/postgres.ts +680 -53
- package/std/net/prefork.ts +149 -13
- package/std/net/router.ts +1 -1
- package/std/net/upstream.ts +759 -0
- package/std/net/websocket.ts +5 -5
- package/std/region.ts +95 -0
package/std/net/postgres.ts
CHANGED
|
@@ -59,8 +59,9 @@
|
|
|
59
59
|
* does not need: a program prepares a statement and executes it in the same
|
|
60
60
|
* flight.
|
|
61
61
|
*
|
|
62
|
-
* **Text results, no copies.** Every parameter is in text format
|
|
63
|
-
*
|
|
62
|
+
* **Text results, no copies.** Every parameter is in text format unless it
|
|
63
|
+
* is one of the binary integer arrays below, and every result column is text
|
|
64
|
+
* unless the bind asks otherwise. A row is read where it
|
|
64
65
|
* lies in the input buffer: `next()` frames each message once, validates its
|
|
65
66
|
* layout, and records each column's start and length in two reused arrays, so
|
|
66
67
|
* a row costs no allocation. `int`, `bool`, `isNull` and `PgText.timestamp`
|
|
@@ -72,6 +73,21 @@
|
|
|
72
73
|
* Bind, so a list needs no string built first. A program whose element was
|
|
73
74
|
* refused for room can drop the whole bind, unsent, with `bindCancel`.
|
|
74
75
|
*
|
|
76
|
+
* **Binary integer arrays.** `paramInt4Array` and `paramInt8Array` send an
|
|
77
|
+
* `i64[]` window as an `int4[]` or `int8[]` in PostgreSQL's binary layout:
|
|
78
|
+
* four or eight bytes an element and no digits to write or parse, which the
|
|
79
|
+
* benchmark (`bench/postgres/arrays.mjs`) measured at about a tenth of the
|
|
80
|
+
* text encoder's client CPU, and at 1,000 elements a 51% cut in the server's
|
|
81
|
+
* on `unnest` for `int8[]`, 30% for `int4[]`.
|
|
82
|
+
* `binaryArrayStart(oid)`, `binaryArrayInt` and `binaryArrayNull` build one
|
|
83
|
+
* element at a time, NULLs included, and `arrayEnd` closes it. The Bind's
|
|
84
|
+
* format codes stay a single zero word while every parameter is text; the
|
|
85
|
+
* first binary one makes `bindEnd` write a code for each, so an all-text bind
|
|
86
|
+
* costs nothing more. An `int4` element outside `int4` panics rather than
|
|
87
|
+
* wrapping. The element type must match the statement's parameter: the server
|
|
88
|
+
* answers a mismatch with `ErrorResponse` (42804) for that execution, and the
|
|
89
|
+
* connection goes on.
|
|
90
|
+
*
|
|
75
91
|
* **Binary results.** A bind opened with `bindStartBinary` instead of
|
|
76
92
|
* `bindStart` asks for every result column in binary format: an `int8` is
|
|
77
93
|
* eight bytes, a `timestamptz` eight bytes of microseconds since 2000, a
|
|
@@ -123,14 +139,18 @@
|
|
|
123
139
|
* `ticket` naming the request. Statements given to `register` are prepared on
|
|
124
140
|
* each connection ahead of its first request. A connection that fails reports
|
|
125
141
|
* `PG_LOST` for each request it took, then `PG_FAILED`; the program closes the
|
|
126
|
-
* socket and the slot is built again on its next use.
|
|
127
|
-
*
|
|
128
|
-
*
|
|
142
|
+
* socket and the slot is built again on its next use.
|
|
143
|
+
*
|
|
144
|
+
* **Carrier.** `PgCarrier`, in `nish/net/postgres-carrier`, moves a pool's
|
|
145
|
+
* bytes on an `HttpHandler`'s loop, so a route handler queries without
|
|
146
|
+
* blocking its worker. It is a module of its own because it reads sockets,
|
|
147
|
+
* the clock and the entropy source, and this one reaches none of them.
|
|
129
148
|
*
|
|
130
149
|
* **Batching.** `PgBatcher` turns many reads of one row shape by key — a user
|
|
131
150
|
* by id, an article by slug — into one execution of a statement over
|
|
132
151
|
* `unnest($1) with ordinality`. `addInt` or `addText` queues a request's key
|
|
133
|
-
* and tag; `take(conn)` binds the oldest waiting, each distinct key once
|
|
152
|
+
* and tag; `take(conn)` binds the oldest waiting, each distinct key once (past
|
|
153
|
+
* `PG_BATCH_PROBES` slots of its table probe, a key is bound again), up
|
|
134
154
|
* to `maxRequests` and `maxBytes`, and answers a run's handle; `row`, `end`
|
|
135
155
|
* and `fail` take that execution's events (a handle already answered makes
|
|
136
156
|
* them a no-op counted in `stray`, so a server's late event cannot panic),
|
|
@@ -156,12 +176,13 @@
|
|
|
156
176
|
* Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
|
|
157
177
|
* and RFC 5802 and RFC 7677; not ported from another implementation.
|
|
158
178
|
*/
|
|
159
|
-
import { copyBytes, getF64BE, getI32BE, getI64BE, getU16BE, setI32BE, setU16BE } from "nish/bytes"
|
|
179
|
+
import { copyBytes, getF64BE, getI32BE, getI64BE, getU16BE, setI32BE, setI64BE, setU16BE } from "nish/bytes"
|
|
160
180
|
import { base64StdPadDecode, base64StdPadEncode } from "nish/crypto/base64url"
|
|
161
181
|
import { timingSafeEqual } from "nish/crypto/ct"
|
|
162
182
|
import { hmacSha256, pbkdf2HmacSha256 } from "nish/crypto/hmac"
|
|
163
183
|
import { Sha256 } from "nish/crypto/sha256"
|
|
164
184
|
import { Secret, expose, exposeWith, secret, wipe } from "nish:secret"
|
|
185
|
+
import { urlDecode } from "nish/net/url"
|
|
165
186
|
|
|
166
187
|
/** `next()` needs more input before it can say anything. */
|
|
167
188
|
export const PG_NEED_MORE: i32 = 0
|
|
@@ -252,7 +273,8 @@ const PG_TAIL: i32 = 17
|
|
|
252
273
|
const PG_BINARY_TAIL: i32 = 2
|
|
253
274
|
// The result format code for binary.
|
|
254
275
|
const PG_FORMAT_BINARY: i32 = 1
|
|
255
|
-
// The element types `PgBinaryArray` reads,
|
|
276
|
+
// The element types `PgBinaryArray` reads, and the binary array parameters
|
|
277
|
+
// write (int4 and int8), by OID: int8, int2 and int4.
|
|
256
278
|
const PG_OID_INT8: i32 = 20
|
|
257
279
|
const PG_OID_INT2: i32 = 21
|
|
258
280
|
const PG_OID_INT4: i32 = 23
|
|
@@ -432,6 +454,16 @@ const pgDecimalLength = (value: i64): i32 => {
|
|
|
432
454
|
return n
|
|
433
455
|
}
|
|
434
456
|
|
|
457
|
+
/** Whether `value` is an `int4`, -2^31..2^31-1. */
|
|
458
|
+
const pgFitsInt4 = (value: i64): boolean => value >= toI64(-2147483648) && value <= toI64(2147483647)
|
|
459
|
+
|
|
460
|
+
/** Panics unless `values[start, start + count)` is inside `values`. */
|
|
461
|
+
const pgIntWindow = (values: i64[], start: i32, count: i32): void => {
|
|
462
|
+
if (start < 0 || count < 0 || count > toI32(values.length) - start) {
|
|
463
|
+
panic("nish/net/postgres: an int array parameter outside its array")
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
435
467
|
/** Whether `text` holds a NUL byte, which a protocol string cannot. */
|
|
436
468
|
const pgHasNul = (text: string): boolean => {
|
|
437
469
|
const n: i32 = toI32(text.length)
|
|
@@ -1207,9 +1239,19 @@ export class PgConnection {
|
|
|
1207
1239
|
private bindAt: i32 = -1
|
|
1208
1240
|
private paramCountAt: i32 = 0
|
|
1209
1241
|
private paramCount: i32 = 0
|
|
1242
|
+
// What the open bind's end owes beyond `PG_TAIL`: its binary result format
|
|
1243
|
+
// code, and once it holds a binary parameter, a format code per parameter.
|
|
1244
|
+
private bindOwed: i32 = 0
|
|
1245
|
+
// The open bind's binary parameters, by index; `bindEnd` back-patches their format codes.
|
|
1246
|
+
private binaryParams: i32[]
|
|
1247
|
+
private binaryParamCount: i32 = 0
|
|
1210
1248
|
// The open array parameter's length word, -1 when none is open, and its elements so far.
|
|
1211
1249
|
private arrayAt: i32 = -1
|
|
1212
1250
|
private arrayCount: i32 = 0
|
|
1251
|
+
// The open array's element OID when it is binary (`binaryArrayStart`), 0 when it is a text literal.
|
|
1252
|
+
private arrayOid: i32 = 0
|
|
1253
|
+
// Whether the open binary array holds a NULL element.
|
|
1254
|
+
private arrayNulls: boolean = false
|
|
1213
1255
|
private state: i32 = 0
|
|
1214
1256
|
private clientNonce: string = ""
|
|
1215
1257
|
private serverSignature: u8[]
|
|
@@ -1235,6 +1277,7 @@ export class PgConnection {
|
|
|
1235
1277
|
this.colStarts = new Array<i32>(16)
|
|
1236
1278
|
this.colLengths = new Array<i32>(16)
|
|
1237
1279
|
this.colTypes = new Array<i32>(16)
|
|
1280
|
+
this.binaryParams = []
|
|
1238
1281
|
this.serverSignature = []
|
|
1239
1282
|
const head: u8[] = []
|
|
1240
1283
|
for (let i: i32 = 0; i < PG_NONCE_BYTES && i < toI32(nonce.length); i++) {
|
|
@@ -1911,14 +1954,13 @@ export class PgConnection {
|
|
|
1911
1954
|
|
|
1912
1955
|
/**
|
|
1913
1956
|
* Whether a builder call may queue `size` more bytes and still leave
|
|
1914
|
-
* `PG_TAIL
|
|
1957
|
+
* `PG_TAIL`, and what the open bind's end owes, under `maxOutput`. When they would not fit after the end of the
|
|
1915
1958
|
* buffer, the unsent bytes move to its start first: a builder call asks
|
|
1916
1959
|
* before it takes any offset into the buffer, so nothing it holds moves.
|
|
1917
1960
|
*/
|
|
1918
1961
|
private room(size: i32): boolean {
|
|
1919
1962
|
const queued: i32 = this.outEnd - this.outStart
|
|
1920
|
-
|
|
1921
|
-
if (size < 0 || size > this.config.maxOutput - tail - queued) {
|
|
1963
|
+
if (size < 0 || size > this.config.maxOutput - PG_TAIL - this.bindOwed - queued) {
|
|
1922
1964
|
return false
|
|
1923
1965
|
}
|
|
1924
1966
|
if (this.outStart > 0 && this.outEnd + size > toI32(this.out.length)) {
|
|
@@ -2112,6 +2154,8 @@ export class PgConnection {
|
|
|
2112
2154
|
}
|
|
2113
2155
|
this.bindOpened = opens
|
|
2114
2156
|
this.bindBinary = binary
|
|
2157
|
+
this.bindOwed = extra
|
|
2158
|
+
this.binaryParamCount = 0
|
|
2115
2159
|
this.bindAt = this.startMessage(PG_FE_BIND)
|
|
2116
2160
|
this.putByte(PG_ZERO)
|
|
2117
2161
|
this.putCString(name)
|
|
@@ -2129,9 +2173,37 @@ export class PgConnection {
|
|
|
2129
2173
|
*/
|
|
2130
2174
|
private startParam(size: i32): i32 {
|
|
2131
2175
|
this.noArrayOpen()
|
|
2132
|
-
|
|
2176
|
+
// Once the bind holds a binary parameter, every parameter owes its format code.
|
|
2177
|
+
const owed: i32 = this.binaryParamCount > 0 ? 2 : 0
|
|
2178
|
+
if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size + owed)) {
|
|
2179
|
+
return -1
|
|
2180
|
+
}
|
|
2181
|
+
this.bindOwed += owed
|
|
2182
|
+
this.paramCount += 1
|
|
2183
|
+
const at: i32 = this.outEnd
|
|
2184
|
+
this.putI32(PG_ZERO)
|
|
2185
|
+
return at
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
/**
|
|
2189
|
+
* `startParam` for a parameter in binary format: the first one in a bind
|
|
2190
|
+
* owes a format code for every parameter before it as well as its own,
|
|
2191
|
+
* which `bindEnd` writes once the count is known. A text-only bind owes none
|
|
2192
|
+
* and writes the zero count it always has.
|
|
2193
|
+
*/
|
|
2194
|
+
private startBinaryParam(size: i32): i32 {
|
|
2195
|
+
this.noArrayOpen()
|
|
2196
|
+
const owed: i32 = this.binaryParamCount > 0 ? toI32(2) : toI32(2) * (this.paramCount + toI32(1))
|
|
2197
|
+
if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size + owed)) {
|
|
2133
2198
|
return -1
|
|
2134
2199
|
}
|
|
2200
|
+
if (this.binaryParamCount < toI32(this.binaryParams.length)) {
|
|
2201
|
+
this.binaryParams[this.binaryParamCount] = this.paramCount
|
|
2202
|
+
} else {
|
|
2203
|
+
this.binaryParams.push(this.paramCount)
|
|
2204
|
+
}
|
|
2205
|
+
this.binaryParamCount += 1
|
|
2206
|
+
this.bindOwed += owed
|
|
2135
2207
|
this.paramCount += 1
|
|
2136
2208
|
const at: i32 = this.outEnd
|
|
2137
2209
|
this.putI32(PG_ZERO)
|
|
@@ -2244,7 +2316,7 @@ export class PgConnection {
|
|
|
2244
2316
|
const end: i32 = start + count
|
|
2245
2317
|
// Its exact size first, so that room is asked for once and a refusal writes nothing.
|
|
2246
2318
|
let size: i64 = count > 0 ? toI64(count + 1) : toI64(2)
|
|
2247
|
-
for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
|
|
2319
|
+
for (let i: i32 = start; i >= 0 && i < end && i < toI32(values.length); i++) {
|
|
2248
2320
|
size += toI64(pgDecimalLength(values[i]))
|
|
2249
2321
|
}
|
|
2250
2322
|
const at: i32 = this.startParam(size > toI64(PG_MAX_CAP) ? PG_MAX_CAP + 1 : toI32(size))
|
|
@@ -2252,7 +2324,7 @@ export class PgConnection {
|
|
|
2252
2324
|
return false
|
|
2253
2325
|
}
|
|
2254
2326
|
this.putByte(PG_LBRACE)
|
|
2255
|
-
for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
|
|
2327
|
+
for (let i: i32 = start; i >= 0 && i < end && i < toI32(values.length); i++) {
|
|
2256
2328
|
// Read before the comma's call, which could drop the length fact.
|
|
2257
2329
|
const value: i64 = values[i]
|
|
2258
2330
|
if (i > start) {
|
|
@@ -2306,6 +2378,80 @@ export class PgConnection {
|
|
|
2306
2378
|
return true
|
|
2307
2379
|
}
|
|
2308
2380
|
|
|
2381
|
+
// Binary `int4[]` and `int8[]` parameters, in PostgreSQL's binary array
|
|
2382
|
+
// layout (`array_send`'s): the Bind's only parameters not in text, and its
|
|
2383
|
+
// format codes say so. Their elements are fixed-size big-endian integers,
|
|
2384
|
+
// so there are no digits to write and none for the server to parse. The
|
|
2385
|
+
// element type is explicit and must be the statement's: an `int4[]` sent
|
|
2386
|
+
// where the server expects `int8[]`, or the reverse, is refused by the
|
|
2387
|
+
// server as that execution's `PG_ERROR`, and the connection goes on. An
|
|
2388
|
+
// `int4` element outside -2^31..2^31-1 panics as a program error: false
|
|
2389
|
+
// would read as "no room", and a batching loop would retry it forever.
|
|
2390
|
+
|
|
2391
|
+
/**
|
|
2392
|
+
* An `int4[]` parameter in binary from `values[start, start + count)`;
|
|
2393
|
+
* false as the `param...` calls are. Panics, writing nothing, when the
|
|
2394
|
+
* window is not inside `values` or an element is outside `int4`.
|
|
2395
|
+
*/
|
|
2396
|
+
paramInt4Array(values: i64[], start: i32, count: i32): boolean {
|
|
2397
|
+
pgIntWindow(values, start, count)
|
|
2398
|
+
const end: i32 = start + count
|
|
2399
|
+
for (let i: i32 = start; i >= 0 && i < end && i < toI32(values.length); i++) {
|
|
2400
|
+
if (!pgFitsInt4(values[i])) {
|
|
2401
|
+
panic("nish/net/postgres: paramInt4Array() with an element outside int4")
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
return this.paramBinaryInts(values, start, count, PG_OID_INT4)
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
/**
|
|
2408
|
+
* An `int8[]` parameter in binary from `values[start, start + count)`;
|
|
2409
|
+
* false as the `param...` calls are. Panics when the window is not inside
|
|
2410
|
+
* `values`.
|
|
2411
|
+
*/
|
|
2412
|
+
paramInt8Array(values: i64[], start: i32, count: i32): boolean {
|
|
2413
|
+
pgIntWindow(values, start, count)
|
|
2414
|
+
return this.paramBinaryInts(values, start, count, PG_OID_INT8)
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
private paramBinaryInts(values: i64[], start: i32, count: i32, oid: i32): boolean {
|
|
2418
|
+
const width: i32 = oid === PG_OID_INT4 ? 4 : 8
|
|
2419
|
+
// The header, its one dimension when there are elements, then each element's length word and bytes.
|
|
2420
|
+
const size: i64 =
|
|
2421
|
+
count > 0
|
|
2422
|
+
? toI64(PG_ARRAY_HEADER + PG_ARRAY_DIM) + toI64(count) * toI64(4 + width)
|
|
2423
|
+
: toI64(PG_ARRAY_HEADER)
|
|
2424
|
+
const at: i32 = this.startBinaryParam(size > toI64(PG_MAX_CAP) ? PG_MAX_CAP + 1 : toI32(size))
|
|
2425
|
+
if (at < 0) {
|
|
2426
|
+
return false
|
|
2427
|
+
}
|
|
2428
|
+
this.reserve(this.outEnd + toI32(size))
|
|
2429
|
+
this.putArrayHeader(oid, count)
|
|
2430
|
+
const end: i32 = start + count
|
|
2431
|
+
for (let i: i32 = start; i >= 0 && i < end && i < toI32(values.length); i++) {
|
|
2432
|
+
setI32BE(this.out, this.outEnd, width)
|
|
2433
|
+
if (width === 4) {
|
|
2434
|
+
setI32BE(this.out, this.outEnd + 4, toI32(values[i]))
|
|
2435
|
+
} else {
|
|
2436
|
+
setI64BE(this.out, this.outEnd + 4, values[i])
|
|
2437
|
+
}
|
|
2438
|
+
this.outEnd += 4 + width
|
|
2439
|
+
}
|
|
2440
|
+
this.endParam(at)
|
|
2441
|
+
return true
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
/** A binary array's header with no NULLs: no dimension when it is empty, as `array_send` writes one, else one of `count` elements from 1. */
|
|
2445
|
+
private putArrayHeader(oid: i32, count: i32): void {
|
|
2446
|
+
this.putI32(count > 0 ? toI32(1) : PG_ZERO)
|
|
2447
|
+
this.putI32(PG_ZERO)
|
|
2448
|
+
this.putI32(oid)
|
|
2449
|
+
if (count > 0) {
|
|
2450
|
+
this.putI32(count)
|
|
2451
|
+
this.putI32(toI32(1))
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2309
2455
|
// An array parameter built element by element: `arrayStart()`, then
|
|
2310
2456
|
// `arrayElement`, `arrayInt` or `arrayNull` for each element, then `arrayEnd()`. An
|
|
2311
2457
|
// element refused for room writes nothing and leaves the array open, so a
|
|
@@ -2335,6 +2481,9 @@ export class PgConnection {
|
|
|
2335
2481
|
if (this.arrayAt < 0) {
|
|
2336
2482
|
panic("nish/net/postgres: an array element with no arrayStart()")
|
|
2337
2483
|
}
|
|
2484
|
+
if (this.arrayOid !== 0) {
|
|
2485
|
+
panic("nish/net/postgres: a text array element in a binary array")
|
|
2486
|
+
}
|
|
2338
2487
|
}
|
|
2339
2488
|
|
|
2340
2489
|
/** Whether the open array has room for an element of `size` bytes, its comma, and the `}` it owes. */
|
|
@@ -2413,13 +2562,98 @@ export class PgConnection {
|
|
|
2413
2562
|
return true
|
|
2414
2563
|
}
|
|
2415
2564
|
|
|
2416
|
-
|
|
2565
|
+
// The binary twin: `binaryArrayStart(elementOid)` opens an `int4[]` (OID
|
|
2566
|
+
// 23) or `int8[]` (OID 20) parameter in binary, `binaryArrayInt` and
|
|
2567
|
+
// `binaryArrayNull` add its elements, and the same `arrayEnd()` closes it.
|
|
2568
|
+
// The text builder's rules hold, and a text element in a binary array, or a
|
|
2569
|
+
// binary element in a text one, panics.
|
|
2570
|
+
|
|
2571
|
+
/** Opens a binary array parameter of `elementOid`'s elements; false as the `param...` calls are. Panics on another OID, or while an array is open. */
|
|
2572
|
+
binaryArrayStart(elementOid: i32): boolean {
|
|
2573
|
+
if (this.arrayAt >= 0) {
|
|
2574
|
+
panic("nish/net/postgres: binaryArrayStart() while an array is open")
|
|
2575
|
+
}
|
|
2576
|
+
if (elementOid !== PG_OID_INT4 && elementOid !== PG_OID_INT8) {
|
|
2577
|
+
panic("nish/net/postgres: binaryArrayStart() with an element OID other than int4 (23) or int8 (20)")
|
|
2578
|
+
}
|
|
2579
|
+
// The header with its dimension; `arrayEnd()` fills in the length and the
|
|
2580
|
+
// NULL flag, or drops the dimension when there are no elements.
|
|
2581
|
+
const at: i32 = this.startBinaryParam(PG_ARRAY_HEADER + PG_ARRAY_DIM)
|
|
2582
|
+
if (at < 0) {
|
|
2583
|
+
return false
|
|
2584
|
+
}
|
|
2585
|
+
this.putArrayHeader(elementOid, toI32(1))
|
|
2586
|
+
this.arrayAt = at
|
|
2587
|
+
this.arrayCount = 0
|
|
2588
|
+
this.arrayOid = elementOid
|
|
2589
|
+
this.arrayNulls = false
|
|
2590
|
+
return true
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
private binaryArrayOpen(): void {
|
|
2594
|
+
if (this.arrayAt < 0 || this.arrayOid === 0) {
|
|
2595
|
+
panic("nish/net/postgres: a binary array element with no binaryArrayStart()")
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* An integer element of the open binary array; false, and nothing written,
|
|
2601
|
+
* when there is no room. Panics when no binary array is open, or when it is
|
|
2602
|
+
* an `int4[]` and `value` is outside `int4`.
|
|
2603
|
+
*/
|
|
2604
|
+
binaryArrayInt(value: i64): boolean {
|
|
2605
|
+
this.binaryArrayOpen()
|
|
2606
|
+
const width: i32 = this.arrayOid === PG_OID_INT4 ? 4 : 8
|
|
2607
|
+
if (width === 4 && !pgFitsInt4(value)) {
|
|
2608
|
+
panic("nish/net/postgres: binaryArrayInt() with an element outside int4")
|
|
2609
|
+
}
|
|
2610
|
+
if (!this.room(4 + width)) {
|
|
2611
|
+
return false
|
|
2612
|
+
}
|
|
2613
|
+
this.putI32(width)
|
|
2614
|
+
if (width === 4) {
|
|
2615
|
+
this.putI32(toI32(value))
|
|
2616
|
+
} else {
|
|
2617
|
+
this.reserve(this.outEnd + 8)
|
|
2618
|
+
setI64BE(this.out, this.outEnd, value)
|
|
2619
|
+
this.outEnd += 8
|
|
2620
|
+
}
|
|
2621
|
+
this.arrayCount += 1
|
|
2622
|
+
return true
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
/** A NULL element of the open binary array, length -1; false, and nothing written, when there is no room. Panics when no binary array is open. */
|
|
2626
|
+
binaryArrayNull(): boolean {
|
|
2627
|
+
this.binaryArrayOpen()
|
|
2628
|
+
if (!this.room(toI32(4))) {
|
|
2629
|
+
return false
|
|
2630
|
+
}
|
|
2631
|
+
this.putI32(toI32(-1))
|
|
2632
|
+
this.arrayNulls = true
|
|
2633
|
+
this.arrayCount += 1
|
|
2634
|
+
return true
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
/** Ends the open array parameter, text or binary; never refused for room, which its elements kept. Panics when no array is open. */
|
|
2417
2638
|
arrayEnd(): void {
|
|
2418
2639
|
if (this.arrayAt < 0) {
|
|
2419
2640
|
panic("nish/net/postgres: arrayEnd() with no arrayStart()")
|
|
2420
2641
|
}
|
|
2421
|
-
this.
|
|
2422
|
-
|
|
2642
|
+
if (this.arrayOid !== 0) {
|
|
2643
|
+
// The header follows the parameter's length word: ndim, the NULL flag, the OID, then the dimension.
|
|
2644
|
+
const header: i32 = this.arrayAt + 4
|
|
2645
|
+
if (this.arrayCount === 0) {
|
|
2646
|
+
setI32BE(this.out, header, PG_ZERO)
|
|
2647
|
+
this.outEnd = header + PG_ARRAY_HEADER
|
|
2648
|
+
} else {
|
|
2649
|
+
setI32BE(this.out, header + 4, this.arrayNulls ? 1 : 0)
|
|
2650
|
+
setI32BE(this.out, header + PG_ARRAY_HEADER, this.arrayCount)
|
|
2651
|
+
}
|
|
2652
|
+
this.arrayOid = 0
|
|
2653
|
+
} else {
|
|
2654
|
+
this.tailRoom(toI32(1))
|
|
2655
|
+
this.putByte(PG_RBRACE)
|
|
2656
|
+
}
|
|
2423
2657
|
this.endParam(this.arrayAt)
|
|
2424
2658
|
this.arrayAt = -1
|
|
2425
2659
|
}
|
|
@@ -2438,8 +2672,11 @@ export class PgConnection {
|
|
|
2438
2672
|
panic("nish/net/postgres: bindEnd() while an array is open")
|
|
2439
2673
|
}
|
|
2440
2674
|
const binary: boolean = this.bindBinary
|
|
2441
|
-
this.tailRoom(
|
|
2675
|
+
this.tailRoom(PG_TAIL + this.bindOwed)
|
|
2442
2676
|
const at: i32 = this.bindAt
|
|
2677
|
+
if (this.binaryParamCount > 0) {
|
|
2678
|
+
this.putFormatCodes()
|
|
2679
|
+
}
|
|
2443
2680
|
setU16BE(this.out, this.paramCountAt, toU16(this.paramCount))
|
|
2444
2681
|
if (binary) {
|
|
2445
2682
|
// One format code, which the server applies to every column.
|
|
@@ -2450,6 +2687,7 @@ export class PgConnection {
|
|
|
2450
2687
|
}
|
|
2451
2688
|
this.endMessage(at)
|
|
2452
2689
|
this.bindAt = -1
|
|
2690
|
+
this.bindOwed = 0
|
|
2453
2691
|
const execute: i32 = this.startMessage(PG_FE_EXECUTE)
|
|
2454
2692
|
this.putByte(PG_ZERO)
|
|
2455
2693
|
this.putI32(PG_ZERO)
|
|
@@ -2478,7 +2716,30 @@ export class PgConnection {
|
|
|
2478
2716
|
private dropBind(): void {
|
|
2479
2717
|
this.outEnd = this.bindAt
|
|
2480
2718
|
this.bindAt = -1
|
|
2719
|
+
this.bindOwed = 0
|
|
2481
2720
|
this.arrayAt = -1
|
|
2721
|
+
this.arrayOid = 0
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
/**
|
|
2725
|
+
* Writes the format codes a bind holding a binary parameter owes, in front
|
|
2726
|
+
* of its parameter count: every parameter moves up by a code each, and
|
|
2727
|
+
* every code is text's but the binary parameters'.
|
|
2728
|
+
*/
|
|
2729
|
+
private putFormatCodes(): void {
|
|
2730
|
+
const from: i32 = this.paramCountAt
|
|
2731
|
+
const codes: i32 = 2 * this.paramCount
|
|
2732
|
+
this.reserve(this.outEnd + codes)
|
|
2733
|
+
copyBytes(this.out, from + codes, this.out, from, this.outEnd - from)
|
|
2734
|
+
this.outEnd += codes
|
|
2735
|
+
setU16BE(this.out, from - 2, toU16(this.paramCount))
|
|
2736
|
+
for (let i: i32 = 0; i < this.paramCount; i++) {
|
|
2737
|
+
setU16BE(this.out, from + 2 * i, toU16(0))
|
|
2738
|
+
}
|
|
2739
|
+
for (let k: i32 = 0; k < this.binaryParamCount && k < toI32(this.binaryParams.length); k++) {
|
|
2740
|
+
setU16BE(this.out, from + 2 * this.binaryParams[k], toU16(PG_FORMAT_BINARY))
|
|
2741
|
+
}
|
|
2742
|
+
this.paramCountAt = from + codes
|
|
2482
2743
|
}
|
|
2483
2744
|
|
|
2484
2745
|
/** Sync: ends the open group, which the server answers with `PG_GROUP_DONE`. False when no group is open or a bind is; never for room. */
|
|
@@ -3001,10 +3262,22 @@ export const PG_BATCH_MISSING: i32 = 3
|
|
|
3001
3262
|
/** `PgBatcher.next(run)`: the request `tag`'s execution failed, and its rows so far may be incomplete. */
|
|
3002
3263
|
export const PG_BATCH_FAILED: i32 = 4
|
|
3003
3264
|
|
|
3004
|
-
//
|
|
3005
|
-
//
|
|
3006
|
-
//
|
|
3007
|
-
|
|
3265
|
+
// The most requests one execution answers. The cost of a take does not grow
|
|
3266
|
+
// with it: each request probes at most `PG_BATCH_PROBES` slots, and the
|
|
3267
|
+
// arrays sized by it grow with the batches taken, not with the cap. It is
|
|
3268
|
+
// 65536 so that one full run's arrays stay at 512 KiB, and one execution's
|
|
3269
|
+
// int8 key array, which the server reads into memory whole, at about 1.4 MiB
|
|
3270
|
+
// (65536 keys of at most 20 digits and a comma); a text key array is bounded
|
|
3271
|
+
// by `maxBytes` instead.
|
|
3272
|
+
const PG_BATCH_MAX_REQUESTS: i32 = 65536
|
|
3273
|
+
// The most occupied slots one request's probe visits in `take` before its key
|
|
3274
|
+
// is bound again rather than found. A take of n requests holds at most n - 1
|
|
3275
|
+
// keys when it probes, so up to 1024 requests (the cap this replaced) no probe
|
|
3276
|
+
// stops; past that, keys chosen to collide cost each request this many probes
|
|
3277
|
+
// and key comparisons, not one per key before it.
|
|
3278
|
+
const PG_BATCH_PROBES: i32 = 1024
|
|
3279
|
+
// The fewest entries the arrays that grow with the batches taken start at.
|
|
3280
|
+
const PG_BATCH_MIN_GROWTH: i32 = 16
|
|
3008
3281
|
const PG_BATCH_MAX_QUEUE: i32 = 1048576
|
|
3009
3282
|
const PG_BATCH_MAX_RUNS: i32 = 1024
|
|
3010
3283
|
// The takes a run's handles count before they wrap: handles stay under 2^30.
|
|
@@ -3012,6 +3285,28 @@ const PG_BATCH_USES: i32 = 1048576
|
|
|
3012
3285
|
// The array's `{}` and the longest int8 element, `-9223372036854775808`.
|
|
3013
3286
|
const PG_BATCH_MIN_BYTES: i32 = 22
|
|
3014
3287
|
|
|
3288
|
+
// The low 32 bits of an int8 key.
|
|
3289
|
+
const PG_LOW32: i64 = 4294967295
|
|
3290
|
+
|
|
3291
|
+
/**
|
|
3292
|
+
* The batcher's table hash of an int8 key, 31 bits: the high half times an
|
|
3293
|
+
* odd constant xored into the low half, then MurmurHash3's 32-bit finalizer,
|
|
3294
|
+
* whose unsigned multiplies wrap by definition. Every step is a bijection on
|
|
3295
|
+
* 32 bits, so keys of one high half never collide, and the finalizer spreads
|
|
3296
|
+
* a dense run of ids over the whole table: runs that would land on one
|
|
3297
|
+
* another's slots under a near-identity hash do not merge into one cluster
|
|
3298
|
+
* longer than `PG_BATCH_PROBES`.
|
|
3299
|
+
*/
|
|
3300
|
+
const pgIntHash = (key: i64): i32 => {
|
|
3301
|
+
const lo: u32 = toU32(key & PG_LOW32)
|
|
3302
|
+
const hi: u32 = toU32((key >> toI64(32)) & PG_LOW32)
|
|
3303
|
+
let h: u32 = lo ^ (hi * 2654435769)
|
|
3304
|
+
h = (h ^ (h >> 16)) * 2246822507
|
|
3305
|
+
h = (h ^ (h >> 13)) * 3266489909
|
|
3306
|
+
h = h ^ (h >> 16)
|
|
3307
|
+
return toI32(h >> 1)
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3015
3310
|
/** What a `PgBatcher` sends: one statement shape, its key, and its caps. */
|
|
3016
3311
|
export class PgBatchConfig {
|
|
3017
3312
|
/** The prepared statement's name, which `PgBatcher.sql` is prepared under. */
|
|
@@ -3038,9 +3333,9 @@ export class PgBatchConfig {
|
|
|
3038
3333
|
|
|
3039
3334
|
/** One execution's requests, grouped by key, and how far its rows have been routed. */
|
|
3040
3335
|
class PgBatchRun {
|
|
3041
|
-
/** Key `k`'s requests' tags are `tags[first[k], first[k + 1])`, in the order they were added. */
|
|
3042
|
-
|
|
3043
|
-
|
|
3336
|
+
/** Key `k`'s requests' tags are `tags[first[k], first[k + 1])`, in the order they were added; grown by `take` to the batch. */
|
|
3337
|
+
tags: i32[]
|
|
3338
|
+
first: i32[]
|
|
3044
3339
|
keys: i32 = 0
|
|
3045
3340
|
/** The key rows are being read for, whether it has had one, and the next of its requests to answer. */
|
|
3046
3341
|
cur: i32 = 0
|
|
@@ -3055,9 +3350,9 @@ class PgBatchRun {
|
|
|
3055
3350
|
ended: boolean = false
|
|
3056
3351
|
failed: boolean = false
|
|
3057
3352
|
|
|
3058
|
-
constructor(
|
|
3059
|
-
this.tags =
|
|
3060
|
-
this.first =
|
|
3353
|
+
constructor() {
|
|
3354
|
+
this.tags = []
|
|
3355
|
+
this.first = []
|
|
3061
3356
|
}
|
|
3062
3357
|
|
|
3063
3358
|
/** Opens the run for `keys` keys, none of whose rows have been read. */
|
|
@@ -3078,7 +3373,9 @@ class PgBatchRun {
|
|
|
3078
3373
|
* it reads no clock and makes no socket. Each request is a key and the
|
|
3079
3374
|
* caller's `tag`, queued by `addInt` or `addText`. When a connection is free,
|
|
3080
3375
|
* `take(conn)` binds up to `maxRequests` of them, and up to `maxBytes` of
|
|
3081
|
-
* keys, as one execution of `sql`, each distinct key once
|
|
3376
|
+
* keys, as one execution of `sql`, each distinct key once (a key whose
|
|
3377
|
+
* probe of the batcher's table passes 1024 slots, which takes keys chosen to
|
|
3378
|
+
* collide, is bound again and answered the same), and answers the
|
|
3082
3379
|
* execution's run handle. Each `PG_ROW` of that execution goes to
|
|
3083
3380
|
* `row(run, conn)`, its `PG_COMPLETE` to `end(run)`, and an error or a lost
|
|
3084
3381
|
* connection to `fail(run)`; after each, `next(run)` answers events until
|
|
@@ -3118,12 +3415,13 @@ export class PgBatcher {
|
|
|
3118
3415
|
private readonly qLengths: i32[]
|
|
3119
3416
|
private readonly store: u8[]
|
|
3120
3417
|
// `take`'s open-addressed table of the keys it has bound, each a key number
|
|
3121
|
-
// plus one, 0 for an empty slot; and for each key, its slot
|
|
3122
|
-
|
|
3123
|
-
private
|
|
3124
|
-
private
|
|
3125
|
-
private
|
|
3126
|
-
private
|
|
3418
|
+
// plus one, 0 for an empty slot; and for each key, its slot (-1 when its
|
|
3419
|
+
// probe stopped) and its first request. Grown by `take` to the batch.
|
|
3420
|
+
private table: i32[]
|
|
3421
|
+
private keySlots: i32[]
|
|
3422
|
+
private keyEntries: i32[]
|
|
3423
|
+
private keyCounts: i32[]
|
|
3424
|
+
private takenKeys: i32[]
|
|
3127
3425
|
private readonly runs: PgBatchRun[]
|
|
3128
3426
|
/** The request the last `next(run)` answered about; -1 for none. */
|
|
3129
3427
|
tag: i32 = -1
|
|
@@ -3131,8 +3429,9 @@ export class PgBatcher {
|
|
|
3131
3429
|
refused: i32 = 0
|
|
3132
3430
|
/** The number of `row`, `end` and `fail` calls on a spent handle, each ignored, stopping at 2^31 - 1. */
|
|
3133
3431
|
stray: i32 = 0
|
|
3432
|
+
/** The most requests one execution takes, from `PgBatchConfig.maxRequests`. */
|
|
3433
|
+
readonly maxRequests: i32
|
|
3134
3434
|
private readonly keyType: i32
|
|
3135
|
-
private readonly maxRequests: i32
|
|
3136
3435
|
private readonly maxBytes: i32
|
|
3137
3436
|
private readonly binary: boolean
|
|
3138
3437
|
private storeEnd: i32 = 0
|
|
@@ -3189,19 +3488,14 @@ export class PgBatcher {
|
|
|
3189
3488
|
this.qStarts = new Array<i32>(textSize)
|
|
3190
3489
|
this.qLengths = new Array<i32>(textSize)
|
|
3191
3490
|
this.store = new Array<u8>(storeSize)
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
this.table = new Array<i32>(tableSize)
|
|
3198
|
-
this.keySlots = new Array<i32>(maxRequests)
|
|
3199
|
-
this.keyEntries = new Array<i32>(maxRequests)
|
|
3200
|
-
this.keyCounts = new Array<i32>(maxRequests)
|
|
3201
|
-
this.takenKeys = new Array<i32>(maxRequests)
|
|
3491
|
+
this.table = []
|
|
3492
|
+
this.keySlots = []
|
|
3493
|
+
this.keyEntries = []
|
|
3494
|
+
this.keyCounts = []
|
|
3495
|
+
this.takenKeys = []
|
|
3202
3496
|
this.runs = []
|
|
3203
3497
|
for (let r: i32 = 0; r < config.runs; r++) {
|
|
3204
|
-
this.runs.push(new PgBatchRun(
|
|
3498
|
+
this.runs.push(new PgBatchRun())
|
|
3205
3499
|
}
|
|
3206
3500
|
}
|
|
3207
3501
|
|
|
@@ -3210,6 +3504,24 @@ export class PgBatcher {
|
|
|
3210
3504
|
return this.count
|
|
3211
3505
|
}
|
|
3212
3506
|
|
|
3507
|
+
/**
|
|
3508
|
+
* Drops the oldest waiting request and answers its tag, or -1 when none
|
|
3509
|
+
* waits: for a program that cannot reach the server to fail requests
|
|
3510
|
+
* rather than hold them.
|
|
3511
|
+
*/
|
|
3512
|
+
shed(): i32 {
|
|
3513
|
+
if (this.count === 0) {
|
|
3514
|
+
return -1
|
|
3515
|
+
}
|
|
3516
|
+
const tag: i32 = this.qTags[this.head]
|
|
3517
|
+
this.head = (this.head + 1) % toI32(this.qTags.length)
|
|
3518
|
+
this.count -= 1
|
|
3519
|
+
if (this.count === 0) {
|
|
3520
|
+
this.storeEnd = 0
|
|
3521
|
+
}
|
|
3522
|
+
return tag
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3213
3525
|
/** Queues a request for the int8 `key`; false when `queueSize` are waiting. Panics on a text batcher. */
|
|
3214
3526
|
addInt(key: i64, tag: i32): boolean {
|
|
3215
3527
|
if (this.keyType !== PG_BATCH_INT) {
|
|
@@ -3219,10 +3531,8 @@ export class PgBatcher {
|
|
|
3219
3531
|
if (q < 0) {
|
|
3220
3532
|
return false
|
|
3221
3533
|
}
|
|
3222
|
-
// Both halves folded to 31 bits, so that no step can overflow.
|
|
3223
|
-
const h: i32 = toI32(key & toI64(0x7fffffff)) ^ toI32((key >> toI64(31)) & toI64(0x7fffffff))
|
|
3224
3534
|
this.qInts[q] = key
|
|
3225
|
-
this.added(q, tag,
|
|
3535
|
+
this.added(q, tag, pgIntHash(key), pgDecimalLength(key))
|
|
3226
3536
|
return true
|
|
3227
3537
|
}
|
|
3228
3538
|
|
|
@@ -3371,15 +3681,19 @@ export class PgBatcher {
|
|
|
3371
3681
|
return -1
|
|
3372
3682
|
}
|
|
3373
3683
|
const ring: i32 = toI32(this.qTags.length)
|
|
3684
|
+
this.grow(this.count < this.maxRequests ? this.count : this.maxRequests)
|
|
3374
3685
|
const mask: i32 = toI32(this.table.length) - 1
|
|
3375
3686
|
let keys: i32 = 0
|
|
3376
3687
|
let taken: i32 = 0
|
|
3377
3688
|
let bytes: i32 = 2
|
|
3378
3689
|
while (taken < this.count && taken < this.maxRequests && taken < toI32(this.takenKeys.length)) {
|
|
3379
3690
|
const q: i32 = (this.head + taken) % ring
|
|
3380
|
-
// Probe for the key: a slot holding it, or the empty slot it would take
|
|
3691
|
+
// Probe for the key: a slot holding it, or the empty slot it would take;
|
|
3692
|
+
// -1 after `PG_BATCH_PROBES` occupied ones, when the key is bound again,
|
|
3693
|
+
// its own ordinal and rows, and left out of the table.
|
|
3381
3694
|
let slot: i32 = this.qHashes[q] & mask
|
|
3382
3695
|
let k: i32 = -1
|
|
3696
|
+
let probes: i32 = 0
|
|
3383
3697
|
while (this.table[slot] > 0) {
|
|
3384
3698
|
const seen: i32 = this.table[slot] - 1
|
|
3385
3699
|
const e: i32 = this.keyEntries[seen]
|
|
@@ -3387,6 +3701,11 @@ export class PgBatcher {
|
|
|
3387
3701
|
k = seen
|
|
3388
3702
|
break
|
|
3389
3703
|
}
|
|
3704
|
+
probes += 1
|
|
3705
|
+
if (probes === PG_BATCH_PROBES) {
|
|
3706
|
+
slot = -1
|
|
3707
|
+
break
|
|
3708
|
+
}
|
|
3390
3709
|
slot = (slot + 1) & mask
|
|
3391
3710
|
}
|
|
3392
3711
|
if (k < 0) {
|
|
@@ -3397,7 +3716,9 @@ export class PgBatcher {
|
|
|
3397
3716
|
bytes += cost
|
|
3398
3717
|
k = keys
|
|
3399
3718
|
keys += 1
|
|
3400
|
-
|
|
3719
|
+
if (slot >= 0) {
|
|
3720
|
+
this.table[slot] = keys
|
|
3721
|
+
}
|
|
3401
3722
|
this.keySlots[k] = slot
|
|
3402
3723
|
this.keyEntries[k] = q
|
|
3403
3724
|
this.keyCounts[k] = 0
|
|
@@ -3407,7 +3728,10 @@ export class PgBatcher {
|
|
|
3407
3728
|
taken += 1
|
|
3408
3729
|
}
|
|
3409
3730
|
for (let k: i32 = 0; k < keys && k < toI32(this.keySlots.length); k++) {
|
|
3410
|
-
this.
|
|
3731
|
+
const slot: i32 = this.keySlots[k]
|
|
3732
|
+
if (slot >= 0) {
|
|
3733
|
+
this.table[slot] = 0
|
|
3734
|
+
}
|
|
3411
3735
|
}
|
|
3412
3736
|
if (taken === 0) {
|
|
3413
3737
|
conn.bindCancel()
|
|
@@ -3417,6 +3741,11 @@ export class PgBatcher {
|
|
|
3417
3741
|
conn.bindEnd()
|
|
3418
3742
|
// The requests grouped by key, each key's in the order they were added.
|
|
3419
3743
|
const run: PgBatchRun = this.runs[r]
|
|
3744
|
+
if (toI32(run.tags.length) < taken) {
|
|
3745
|
+
// Grown together: `first` holds one more than `tags`, which bounds the keys too.
|
|
3746
|
+
run.tags = new Array<i32>(toI32(this.takenKeys.length))
|
|
3747
|
+
run.first = new Array<i32>(toI32(this.takenKeys.length) + 1)
|
|
3748
|
+
}
|
|
3420
3749
|
run.first[0] = 0
|
|
3421
3750
|
for (let k: i32 = 0; k < keys && k < toI32(this.keyCounts.length); k++) {
|
|
3422
3751
|
run.first[k + 1] = run.first[k] + this.keyCounts[k]
|
|
@@ -3438,6 +3767,34 @@ export class PgBatcher {
|
|
|
3438
3767
|
return run.handle
|
|
3439
3768
|
}
|
|
3440
3769
|
|
|
3770
|
+
/**
|
|
3771
|
+
* Grows `take`'s arrays, when they are smaller, to hold `need` requests:
|
|
3772
|
+
* to a power of two, at least `PG_BATCH_MIN_GROWTH` and at most
|
|
3773
|
+
* `maxRequests`, and the table to twice that, so it is at most half full.
|
|
3774
|
+
* A batcher whose batches stay small never holds arrays the size of its cap.
|
|
3775
|
+
*/
|
|
3776
|
+
private grow(need: i32): void {
|
|
3777
|
+
if (toI32(this.takenKeys.length) >= need) {
|
|
3778
|
+
return
|
|
3779
|
+
}
|
|
3780
|
+
let size: i32 = PG_BATCH_MIN_GROWTH
|
|
3781
|
+
while (size < need) {
|
|
3782
|
+
size *= 2
|
|
3783
|
+
}
|
|
3784
|
+
if (size > this.maxRequests) {
|
|
3785
|
+
size = this.maxRequests
|
|
3786
|
+
}
|
|
3787
|
+
let tableSize: i32 = 2
|
|
3788
|
+
while (tableSize < 2 * size) {
|
|
3789
|
+
tableSize *= 2
|
|
3790
|
+
}
|
|
3791
|
+
this.table = new Array<i32>(tableSize)
|
|
3792
|
+
this.keySlots = new Array<i32>(size)
|
|
3793
|
+
this.keyEntries = new Array<i32>(size)
|
|
3794
|
+
this.keyCounts = new Array<i32>(size)
|
|
3795
|
+
this.takenKeys = new Array<i32>(size)
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3441
3798
|
/** Writes the queued request `q`'s key as the open array's next element; false when `conn` has no room. */
|
|
3442
3799
|
private bindKey(conn: PgConnection, q: i32): boolean {
|
|
3443
3800
|
if (this.keyType === PG_BATCH_INT) {
|
|
@@ -3958,3 +4315,273 @@ export class PgBatchTurns {
|
|
|
3958
4315
|
}
|
|
3959
4316
|
}
|
|
3960
4317
|
}
|
|
4318
|
+
|
|
4319
|
+
/** `PgTarget.failure`: none; connect as `unix` says. */
|
|
4320
|
+
export const PG_TARGET_OK: i32 = 0
|
|
4321
|
+
/** `PgTarget.failure`: not a `postgres://` or `postgresql://` URL, or a query parameter with no `=`. */
|
|
4322
|
+
export const PG_TARGET_BAD_URL: i32 = 1
|
|
4323
|
+
/** `PgTarget.failure`: a `%` not followed by two hex digits, `%00`, or escapes that decode to bytes that are not UTF-8. */
|
|
4324
|
+
export const PG_TARGET_BAD_ESCAPE: i32 = 2
|
|
4325
|
+
/** `PgTarget.failure`: no host, more than one (a `,`), an unclosed `[` or text after its `]`, a raw `@` past the user information, or a NUL byte. */
|
|
4326
|
+
export const PG_TARGET_BAD_HOST: i32 = 3
|
|
4327
|
+
/** `PgTarget.failure`: a port that is not 1 to 65535 in decimal digits. */
|
|
4328
|
+
export const PG_TARGET_BAD_PORT: i32 = 4
|
|
4329
|
+
/** `PgTarget.failure`: a socket path longer than Linux's `sun_path` holds. */
|
|
4330
|
+
export const PG_TARGET_TOO_LONG: i32 = 5
|
|
4331
|
+
|
|
4332
|
+
/** The port a target names when nothing else does. */
|
|
4333
|
+
const PG_DEFAULT_PORT: i32 = 5432
|
|
4334
|
+
/** The longest socket path, NUL excluded: Linux's 108-byte `sun_path`, less its NUL; an `@` name has no NUL, so its `@` takes that byte. */
|
|
4335
|
+
const PG_SOCKET_PATH_MAX: i32 = 107
|
|
4336
|
+
|
|
4337
|
+
/**
|
|
4338
|
+
* Where a connection goes, from libpq's `host` and `port` or a
|
|
4339
|
+
* `postgres://` URL: `pgTarget` and `pgTargetUrl` make one, reading no
|
|
4340
|
+
* socket and no file. When `failure` is `PG_TARGET_OK`, a `unix` target is
|
|
4341
|
+
* `unixConnect(path)`, and any other is `host` and `port`, for
|
|
4342
|
+
* `netAddress` and `tcpConnect`.
|
|
4343
|
+
*
|
|
4344
|
+
* A host starting with `/` is a socket directory, and one starting with `@`
|
|
4345
|
+
* a Linux abstract name, as in libpq: the socket is
|
|
4346
|
+
* `<host>/.s.PGSQL.<port>`. The host is always the directory, never the
|
|
4347
|
+
* socket file itself. Any other host is TCP.
|
|
4348
|
+
*/
|
|
4349
|
+
export class PgTarget {
|
|
4350
|
+
/** `PG_TARGET_OK`, or the `PG_TARGET_*` reason the input was refused. */
|
|
4351
|
+
failure: i32 = 0
|
|
4352
|
+
/** Whether the target is a Unix socket, at `path`. */
|
|
4353
|
+
unix: boolean = false
|
|
4354
|
+
/** The host as given (a URL's decoded, without an IPv6 address's brackets); for a socket, its directory or `@` name. */
|
|
4355
|
+
host: string = ""
|
|
4356
|
+
/** The port: the TCP port, or the number in the socket's name. */
|
|
4357
|
+
port: i32 = 5432
|
|
4358
|
+
/** The socket's path, `<host>/.s.PGSQL.<port>`, when `unix`; otherwise "". */
|
|
4359
|
+
path: string = ""
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
/** A target refused for `failure`. */
|
|
4363
|
+
const pgTargetFailed = (failure: i32): PgTarget => {
|
|
4364
|
+
const t = new PgTarget()
|
|
4365
|
+
t.failure = failure
|
|
4366
|
+
return t
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
/**
|
|
4370
|
+
* The target `host` and `port` name, as libpq's `host` and `port` do: a host
|
|
4371
|
+
* starting with `/` or `@` is the socket `<host>/.s.PGSQL.<port>`, any other
|
|
4372
|
+
* a TCP host. The host is not percent-decoded. A port outside 1 to 65535, an
|
|
4373
|
+
* empty host, a `,` (more than one host) or a NUL byte is refused, and so is
|
|
4374
|
+
* a socket path over 107 bytes (an `@` name over 107 after its `@`), never
|
|
4375
|
+
* cut short.
|
|
4376
|
+
*/
|
|
4377
|
+
export const pgTarget = (host: string, port: i32): PgTarget => {
|
|
4378
|
+
if (port < 1 || port > 65535) {
|
|
4379
|
+
return pgTargetFailed(PG_TARGET_BAD_PORT)
|
|
4380
|
+
}
|
|
4381
|
+
const n: i32 = toI32(host.length)
|
|
4382
|
+
if (n === 0) {
|
|
4383
|
+
return pgTargetFailed(PG_TARGET_BAD_HOST)
|
|
4384
|
+
}
|
|
4385
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
4386
|
+
const c: i32 = toI32(host.charCodeAt(i))
|
|
4387
|
+
if (c === 0 || c === 44) {
|
|
4388
|
+
return pgTargetFailed(PG_TARGET_BAD_HOST)
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
const t = new PgTarget()
|
|
4392
|
+
t.host = host
|
|
4393
|
+
t.port = port
|
|
4394
|
+
const first: i32 = toI32(host.charCodeAt(0))
|
|
4395
|
+
if (first === 47 || first === 64) {
|
|
4396
|
+
const path: string = `${host}/.s.PGSQL.${port}`
|
|
4397
|
+
// An `@` name's NUL-free bytes after the `@` fill what a path's NUL would.
|
|
4398
|
+
const limit: i32 = first === 64 ? PG_SOCKET_PATH_MAX + 1 : PG_SOCKET_PATH_MAX
|
|
4399
|
+
if (toI32(path.length) > limit) {
|
|
4400
|
+
return pgTargetFailed(PG_TARGET_TOO_LONG)
|
|
4401
|
+
}
|
|
4402
|
+
t.unix = true
|
|
4403
|
+
t.path = path
|
|
4404
|
+
}
|
|
4405
|
+
return t
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
/** `text[start, end)` percent-decoded, or null for a `%` without two hex digits after it, `%00`, or bytes that are not UTF-8. */
|
|
4409
|
+
const pgDecoded = (text: string, start: i32, end: i32): string | null =>
|
|
4410
|
+
urlDecode(text.substring(start, end), 0)
|
|
4411
|
+
|
|
4412
|
+
/** The port `text` spells, 1 to 5 decimal digits naming 1 to 65535, or -1. */
|
|
4413
|
+
const pgPortValue = (text: string): i32 => {
|
|
4414
|
+
const n: i32 = toI32(text.length)
|
|
4415
|
+
if (n === 0 || n > 5) {
|
|
4416
|
+
return -1
|
|
4417
|
+
}
|
|
4418
|
+
let v: i32 = 0
|
|
4419
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
4420
|
+
const c: i32 = toI32(text.charCodeAt(i))
|
|
4421
|
+
if (c < 48 || c > 57) {
|
|
4422
|
+
return -1
|
|
4423
|
+
}
|
|
4424
|
+
v = v * 10 + (c - 48)
|
|
4425
|
+
}
|
|
4426
|
+
if (v < 1 || v > 65535) {
|
|
4427
|
+
return -1
|
|
4428
|
+
}
|
|
4429
|
+
return v
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
/**
|
|
4433
|
+
* The target a `postgres://` or `postgresql://` URL names, as libpq reads
|
|
4434
|
+
* one: `postgres://[user[:password]@][host][:port][/dbname][?key=value&...]`.
|
|
4435
|
+
* The host, the port and every query key and value are percent-decoded, so a
|
|
4436
|
+
* socket directory is written `%2Ftmp` or passed as `?host=/tmp`; a `host` or
|
|
4437
|
+
* `port` in the query overrides the authority's, and other keys are ignored.
|
|
4438
|
+
* The user, password and database are not read here, but a bad escape in them
|
|
4439
|
+
* still refuses the URL. A `%` without two hex digits after it, `%00`, or a
|
|
4440
|
+
* part that decodes to bytes that are not UTF-8 is `PG_TARGET_BAD_ESCAPE`.
|
|
4441
|
+
* The user information ends at the first `@`, as in libpq; a raw `@` after a
|
|
4442
|
+
* `?` or after that first `@` in the authority, or a raw NUL byte anywhere,
|
|
4443
|
+
* is `PG_TARGET_BAD_HOST`, as is a URL with no host anywhere, where libpq
|
|
4444
|
+
* would use its default socket directory. An empty port or query value is
|
|
4445
|
+
* refused, where libpq would take it as unset.
|
|
4446
|
+
* The decoded host is then resolved as `pgTarget` resolves one.
|
|
4447
|
+
*/
|
|
4448
|
+
export const pgTargetUrl = (url: string): PgTarget => {
|
|
4449
|
+
let at: i32 = 0
|
|
4450
|
+
if (url.startsWith("postgresql://")) {
|
|
4451
|
+
at = 13
|
|
4452
|
+
} else if (url.startsWith("postgres://")) {
|
|
4453
|
+
at = 11
|
|
4454
|
+
} else {
|
|
4455
|
+
return pgTargetFailed(PG_TARGET_BAD_URL)
|
|
4456
|
+
}
|
|
4457
|
+
const n: i32 = toI32(url.length)
|
|
4458
|
+
// The authority runs to the first `/` or `?`; the path to the `?`.
|
|
4459
|
+
let authorityEnd: i32 = at
|
|
4460
|
+
while (authorityEnd < n) {
|
|
4461
|
+
const c: i32 = toI32(url.charCodeAt(authorityEnd))
|
|
4462
|
+
if (c === 47 || c === 63) {
|
|
4463
|
+
break
|
|
4464
|
+
}
|
|
4465
|
+
authorityEnd += 1
|
|
4466
|
+
}
|
|
4467
|
+
let queryStart: i32 = authorityEnd
|
|
4468
|
+
while (queryStart < n && toI32(url.charCodeAt(queryStart)) !== 63) {
|
|
4469
|
+
queryStart += 1
|
|
4470
|
+
}
|
|
4471
|
+
// A raw NUL byte anywhere is refused: no part may carry one past here.
|
|
4472
|
+
for (let i: i32 = at; i < n; i++) {
|
|
4473
|
+
if (toI32(url.charCodeAt(i)) === 0) {
|
|
4474
|
+
return pgTargetFailed(PG_TARGET_BAD_HOST)
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
// libpq ends the user information at the first `@` before a `/`, a `?`
|
|
4478
|
+
// included. One after a `?`, or a second in the host and port, reads
|
|
4479
|
+
// differently by other parsers, so either is refused. The loops below also
|
|
4480
|
+
// test `>= 0` and `< n`, always true as `authorityEnd <= n`, so the prover
|
|
4481
|
+
// drops each access's bounds check.
|
|
4482
|
+
let firstAt: i32 = at
|
|
4483
|
+
while (firstAt >= 0 && firstAt < n) {
|
|
4484
|
+
const c: i32 = toI32(url.charCodeAt(firstAt))
|
|
4485
|
+
if (c === 64 || c === 47) {
|
|
4486
|
+
break
|
|
4487
|
+
}
|
|
4488
|
+
firstAt += 1
|
|
4489
|
+
}
|
|
4490
|
+
let hostStart: i32 = at
|
|
4491
|
+
if (firstAt < n && toI32(url.charCodeAt(firstAt)) === 64) {
|
|
4492
|
+
if (firstAt > authorityEnd) {
|
|
4493
|
+
return pgTargetFailed(PG_TARGET_BAD_HOST)
|
|
4494
|
+
}
|
|
4495
|
+
hostStart = firstAt + 1
|
|
4496
|
+
}
|
|
4497
|
+
for (let i: i32 = hostStart; i >= 0 && i < authorityEnd && i < n; i++) {
|
|
4498
|
+
if (toI32(url.charCodeAt(i)) === 64) {
|
|
4499
|
+
return pgTargetFailed(PG_TARGET_BAD_HOST)
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
if (hostStart > at && pgDecoded(url, at, hostStart - 1) === null) {
|
|
4503
|
+
return pgTargetFailed(PG_TARGET_BAD_ESCAPE)
|
|
4504
|
+
}
|
|
4505
|
+
if (authorityEnd < queryStart && pgDecoded(url, authorityEnd, queryStart) === null) {
|
|
4506
|
+
return pgTargetFailed(PG_TARGET_BAD_ESCAPE)
|
|
4507
|
+
}
|
|
4508
|
+
// The host ends at a `:` before the port, after an IPv6 address's `]`.
|
|
4509
|
+
let hostEnd: i32 = authorityEnd
|
|
4510
|
+
let portStart: i32 = -1
|
|
4511
|
+
let bracketed: boolean = false
|
|
4512
|
+
if (hostStart < authorityEnd && toI32(url.charCodeAt(hostStart)) === 91) {
|
|
4513
|
+
let close: i32 = hostStart + 1
|
|
4514
|
+
while (close >= 0 && close < authorityEnd && close < n && toI32(url.charCodeAt(close)) !== 93) {
|
|
4515
|
+
close += 1
|
|
4516
|
+
}
|
|
4517
|
+
if (close === authorityEnd) {
|
|
4518
|
+
return pgTargetFailed(PG_TARGET_BAD_HOST)
|
|
4519
|
+
}
|
|
4520
|
+
bracketed = true
|
|
4521
|
+
hostEnd = close + 1
|
|
4522
|
+
if (hostEnd < authorityEnd) {
|
|
4523
|
+
if (toI32(url.charCodeAt(hostEnd)) !== 58) {
|
|
4524
|
+
return pgTargetFailed(PG_TARGET_BAD_HOST)
|
|
4525
|
+
}
|
|
4526
|
+
portStart = hostEnd + 1
|
|
4527
|
+
}
|
|
4528
|
+
} else {
|
|
4529
|
+
for (let i: i32 = hostStart; i >= 0 && i < authorityEnd && i < n; i++) {
|
|
4530
|
+
if (toI32(url.charCodeAt(i)) === 58) {
|
|
4531
|
+
hostEnd = i
|
|
4532
|
+
portStart = i + 1
|
|
4533
|
+
break
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
const decodedHost: string | null = bracketed
|
|
4538
|
+
? pgDecoded(url, hostStart + 1, hostEnd - 1)
|
|
4539
|
+
: pgDecoded(url, hostStart, hostEnd)
|
|
4540
|
+
if (decodedHost === null) {
|
|
4541
|
+
return pgTargetFailed(PG_TARGET_BAD_ESCAPE)
|
|
4542
|
+
}
|
|
4543
|
+
let host: string = decodedHost
|
|
4544
|
+
let port: i32 = PG_DEFAULT_PORT
|
|
4545
|
+
if (portStart >= 0) {
|
|
4546
|
+
const decodedPort: string | null = pgDecoded(url, portStart, authorityEnd)
|
|
4547
|
+
if (decodedPort === null) {
|
|
4548
|
+
return pgTargetFailed(PG_TARGET_BAD_ESCAPE)
|
|
4549
|
+
}
|
|
4550
|
+
port = pgPortValue(decodedPort)
|
|
4551
|
+
if (port < 0) {
|
|
4552
|
+
return pgTargetFailed(PG_TARGET_BAD_PORT)
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
// The query: `key=value` pairs split at `&`.
|
|
4556
|
+
let i: i32 = queryStart + 1
|
|
4557
|
+
while (i >= 0 && i < n) {
|
|
4558
|
+
let end: i32 = i
|
|
4559
|
+
let eq: i32 = -1
|
|
4560
|
+
while (end < n && toI32(url.charCodeAt(end)) !== 38) {
|
|
4561
|
+
if (eq < 0 && toI32(url.charCodeAt(end)) === 61) {
|
|
4562
|
+
eq = end
|
|
4563
|
+
}
|
|
4564
|
+
end += 1
|
|
4565
|
+
}
|
|
4566
|
+
if (end > i) {
|
|
4567
|
+
if (eq < 0) {
|
|
4568
|
+
return pgTargetFailed(PG_TARGET_BAD_URL)
|
|
4569
|
+
}
|
|
4570
|
+
const key: string | null = pgDecoded(url, i, eq)
|
|
4571
|
+
const value: string | null = pgDecoded(url, eq + 1, end)
|
|
4572
|
+
if (key === null || value === null) {
|
|
4573
|
+
return pgTargetFailed(PG_TARGET_BAD_ESCAPE)
|
|
4574
|
+
}
|
|
4575
|
+
if (key === "host") {
|
|
4576
|
+
host = value
|
|
4577
|
+
} else if (key === "port") {
|
|
4578
|
+
port = pgPortValue(value)
|
|
4579
|
+
if (port < 0) {
|
|
4580
|
+
return pgTargetFailed(PG_TARGET_BAD_PORT)
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
i = end + 1
|
|
4585
|
+
}
|
|
4586
|
+
return pgTarget(host, port)
|
|
4587
|
+
}
|