@clickhouse/client 1.22.0 → 1.23.0-head.287977a.1
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/README.md +2 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.js +11 -4
- package/dist/client.js.map +1 -1
- package/dist/common/clickhouse_types.d.ts +98 -0
- package/dist/common/clickhouse_types.js +30 -0
- package/dist/common/clickhouse_types.js.map +1 -0
- package/dist/common/client.d.ts +233 -0
- package/dist/common/client.js +414 -0
- package/dist/common/client.js.map +1 -0
- package/dist/common/config.d.ts +234 -0
- package/dist/common/config.js +364 -0
- package/dist/common/config.js.map +1 -0
- package/dist/common/connection.d.ts +124 -0
- package/dist/common/connection.js +3 -0
- package/dist/common/connection.js.map +1 -0
- package/dist/common/data_formatter/format_query_params.d.ts +11 -0
- package/dist/common/data_formatter/format_query_params.js +128 -0
- package/dist/common/data_formatter/format_query_params.js.map +1 -0
- package/dist/common/data_formatter/format_query_settings.d.ts +2 -0
- package/dist/common/data_formatter/format_query_settings.js +20 -0
- package/dist/common/data_formatter/format_query_settings.js.map +1 -0
- package/dist/common/data_formatter/formatter.d.ts +41 -0
- package/dist/common/data_formatter/formatter.js +78 -0
- package/dist/common/data_formatter/formatter.js.map +1 -0
- package/dist/common/data_formatter/index.d.ts +3 -0
- package/dist/common/data_formatter/index.js +24 -0
- package/dist/common/data_formatter/index.js.map +1 -0
- package/dist/common/error/error.d.ts +20 -0
- package/dist/common/error/error.js +73 -0
- package/dist/common/error/error.js.map +1 -0
- package/dist/common/error/index.d.ts +1 -0
- package/dist/common/error/index.js +18 -0
- package/dist/common/error/index.js.map +1 -0
- package/dist/common/index.d.ts +67 -0
- package/dist/common/index.js +97 -0
- package/dist/common/index.js.map +1 -0
- package/dist/common/logger.d.ts +80 -0
- package/dist/common/logger.js +154 -0
- package/dist/common/logger.js.map +1 -0
- package/dist/common/parse/column_types.d.ts +155 -0
- package/dist/common/parse/column_types.js +594 -0
- package/dist/common/parse/column_types.js.map +1 -0
- package/dist/common/parse/index.d.ts +2 -0
- package/dist/common/parse/index.js +19 -0
- package/dist/common/parse/index.js.map +1 -0
- package/dist/common/parse/json_handling.d.ts +19 -0
- package/dist/common/parse/json_handling.js +8 -0
- package/dist/common/parse/json_handling.js.map +1 -0
- package/dist/common/result.d.ts +90 -0
- package/dist/common/result.js +3 -0
- package/dist/common/result.js.map +1 -0
- package/dist/common/settings.d.ts +2007 -0
- package/dist/common/settings.js +19 -0
- package/dist/common/settings.js.map +1 -0
- package/dist/common/tracing.d.ts +146 -0
- package/dist/common/tracing.js +76 -0
- package/dist/common/tracing.js.map +1 -0
- package/dist/common/ts_utils.d.ts +4 -0
- package/dist/common/ts_utils.js +3 -0
- package/dist/common/ts_utils.js.map +1 -0
- package/dist/common/utils/connection.d.ts +21 -0
- package/dist/common/utils/connection.js +43 -0
- package/dist/common/utils/connection.js.map +1 -0
- package/dist/common/utils/index.d.ts +5 -0
- package/dist/common/utils/index.js +22 -0
- package/dist/common/utils/index.js.map +1 -0
- package/dist/common/utils/multipart.d.ts +34 -0
- package/dist/common/utils/multipart.js +81 -0
- package/dist/common/utils/multipart.js.map +1 -0
- package/dist/common/utils/sleep.d.ts +4 -0
- package/dist/common/utils/sleep.js +12 -0
- package/dist/common/utils/sleep.js.map +1 -0
- package/dist/common/utils/stream.d.ts +15 -0
- package/dist/common/utils/stream.js +50 -0
- package/dist/common/utils/stream.js.map +1 -0
- package/dist/common/utils/url.d.ts +20 -0
- package/dist/common/utils/url.js +67 -0
- package/dist/common/utils/url.js.map +1 -0
- package/dist/common/version.d.ts +2 -0
- package/dist/common/version.js +4 -0
- package/dist/common/version.js.map +1 -0
- package/dist/config.d.ts +22 -2
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/connection/compression.d.ts +2 -2
- package/dist/connection/compression.js +4 -4
- package/dist/connection/compression.js.map +1 -1
- package/dist/connection/create_connection.d.ts +1 -1
- package/dist/connection/node_base_connection.d.ts +3 -3
- package/dist/connection/node_base_connection.js +22 -22
- package/dist/connection/node_base_connection.js.map +1 -1
- package/dist/connection/node_custom_agent_connection.js +2 -2
- package/dist/connection/node_custom_agent_connection.js.map +1 -1
- package/dist/connection/node_http_connection.js +2 -2
- package/dist/connection/node_http_connection.js.map +1 -1
- package/dist/connection/node_https_connection.d.ts +1 -1
- package/dist/connection/node_https_connection.js +3 -3
- package/dist/connection/node_https_connection.js.map +1 -1
- package/dist/connection/socket_pool.d.ts +1 -1
- package/dist/connection/socket_pool.js +30 -30
- package/dist/connection/socket_pool.js.map +1 -1
- package/dist/connection/stream.d.ts +1 -1
- package/dist/connection/stream.js +9 -9
- package/dist/connection/stream.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.js +26 -24
- package/dist/index.js.map +1 -1
- package/dist/result_set.d.ts +1 -1
- package/dist/result_set.js +10 -10
- package/dist/result_set.js.map +1 -1
- package/dist/utils/encoder.d.ts +1 -1
- package/dist/utils/encoder.js +5 -5
- package/dist/utils/encoder.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +7 -5
- package/skills/clickhouse-js-node-rowbinary-parser/EXAMPLES.md +48 -0
- package/skills/clickhouse-js-node-rowbinary-parser/README.md +255 -0
- package/skills/clickhouse-js-node-rowbinary-parser/SKILL.md +206 -0
- package/skills/clickhouse-js-node-rowbinary-parser/case-studies/iot-rowbinary-vs-json.md +83 -0
- package/skills/clickhouse-js-node-rowbinary-parser/case-studies/ledger-rowbinary-vs-json.md +103 -0
- package/skills/clickhouse-js-node-rowbinary-parser/case-studies/logs-json-wins.md +86 -0
- package/skills/clickhouse-js-node-rowbinary-parser/case-studies/wasm-vs-js.md +172 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/aggregateFunction.ts +34 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/bool.ts +10 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/columnar.ts +125 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/compile.ts +318 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/composite.ts +181 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/core.ts +77 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/datetime.ts +113 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/decimals.ts +57 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/dynamic.ts +328 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/enums.ts +28 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/examples/carts.ts +71 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/examples/events.ts +51 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/examples/iot.ts +158 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/examples/ledger.ts +98 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/examples/logs.ts +73 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/examples/observability.ts +142 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/examples/orders.ts +65 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/examples/profiles.ts +60 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/examples/telemetry.ts +102 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/floats.ts +32 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/geo.ts +109 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/header.ts +29 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/integers.ts +95 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/interval.ts +54 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/ip.ts +93 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/json.ts +33 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/lowCardinality.ts +18 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/nested.ts +23 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/nothing.ts +29 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/reader.ts +68 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/rowBinaryWithNamesAndTypes.ts +155 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/rows.ts +58 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/simpleAggregateFunction.ts +20 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/stream.ts +276 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/strings.ts +55 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/time.ts +61 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/uuid.ts +153 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/varint.ts +70 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Reader } from "./core.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `AggregateFunction(func, T…)` holds an OPAQUE serialized aggregation STATE
|
|
5
|
+
* (what `-State` combinators produce). In RowBinary this state is written RAW,
|
|
6
|
+
* with **NO length prefix** and a layout entirely specific to `func` (and to the
|
|
7
|
+
* ClickHouse version): `sumState(UInt64)` is 8 bytes, `uniqState(...)` is a
|
|
8
|
+
* variable-length hash-set blob, etc.
|
|
9
|
+
*
|
|
10
|
+
* So it cannot be decoded generically (there is no schema in the bytes) and
|
|
11
|
+
* cannot even be SKIPPED generically (there is no length to skip past) — without
|
|
12
|
+
* knowing `func`'s exact byte layout you cannot find where it ends, and every
|
|
13
|
+
* later column in the row misaligns. There is therefore NO generic reader.
|
|
14
|
+
*
|
|
15
|
+
* Fix it server-side (RECOMMENDED): finalize with the `-Merge` combinator or
|
|
16
|
+
* `finalizeAggregation()` in SQL so the column becomes a normal value
|
|
17
|
+
* (`sum` -> `UInt64`, `uniq` -> `UInt64`, `avg` -> `Float64`, …) and use the
|
|
18
|
+
* matching reader. Never ship raw `-State` columns to the client unless you
|
|
19
|
+
* intend to merge them later.
|
|
20
|
+
*
|
|
21
|
+
* ESCAPE HATCH: a few functions' state IS just a value of a known type (e.g.
|
|
22
|
+
* `sumState(UInt64)` is literally that `UInt64`), so you may decode it as that
|
|
23
|
+
* type — fragile and version-specific; only when you truly know the layout. See
|
|
24
|
+
* `tests/aggregateFunction.test.ts`.
|
|
25
|
+
*
|
|
26
|
+
* This reader throws to stop a generic parser from silently misaligning the row.
|
|
27
|
+
*/
|
|
28
|
+
export const readAggregateFunction: Reader<never> = () => {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"RowBinary: AggregateFunction is opaque, unframed aggregation state with no " +
|
|
31
|
+
"length prefix — not generically decodable or skippable. Finalize server-side " +
|
|
32
|
+
"(-Merge / finalizeAggregation()) and decode the concrete result type instead.",
|
|
33
|
+
);
|
|
34
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Cursor } from "./core.js";
|
|
2
|
+
import { readUInt8 } from "./integers.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read a `Bool`: 1 byte, stored as `UInt8` (`0` = false, `1` = true). Treats any
|
|
6
|
+
* non-zero byte as true.
|
|
7
|
+
*/
|
|
8
|
+
export function readBool(state: Cursor): boolean {
|
|
9
|
+
return readUInt8(state) !== 0;
|
|
10
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming COLUMNAR decode for an all-numeric, fixed-width RowBinary result —
|
|
3
|
+
* one concrete example reader that ties together the three wins this skill keeps
|
|
4
|
+
* pointing at. The schema is hard-coded on purpose: a real columnar reader is
|
|
5
|
+
* MONOMORPHIZED to its result, so the row loop is straight-line constant-offset
|
|
6
|
+
* reads with no per-field dispatch. Generate one shaped like this per schema.
|
|
7
|
+
*
|
|
8
|
+
* The example schema (`sensor_id UInt32, ts DateTime64(3), value Float64,
|
|
9
|
+
* quality Float32, status UInt8`) — every column fixed-width, stride 25 bytes:
|
|
10
|
+
*
|
|
11
|
+
* sensor_id UInt32 @ o+0 getUint32 -> Uint32Array
|
|
12
|
+
* ts DateTime64(3) @ o+4 2x getUint32 -> BigInt64Array (raw ms ticks)
|
|
13
|
+
* value Float64 @ o+12 getFloat64 -> Float64Array
|
|
14
|
+
* quality Float32 @ o+20 getFloat32 -> Float32Array
|
|
15
|
+
* status UInt8 @ o+24 buf[o+24] -> Uint8Array
|
|
16
|
+
*
|
|
17
|
+
* The three wins:
|
|
18
|
+
*
|
|
19
|
+
* 1. COLUMNAR (struct-of-arrays). One typed array per column, not one object
|
|
20
|
+
* per row — removes the per-row object / `Date` / number-boxing allocation
|
|
21
|
+
* that dominates a numeric decode (~4x in plain JS; see `src/examples/iot.ts`
|
|
22
|
+
* and `tests/iot.columnar.bench.ts`). Keep `ts` as raw `BigInt64Array` ticks
|
|
23
|
+
* and make a `Date` lazily, per displayed row — never allocate 50k `Date`s.
|
|
24
|
+
* The `Int64` column is itself filled WITHOUT allocating a bigint per row:
|
|
25
|
+
* copy the two little-endian 32-bit words straight into a `Uint32Array` view
|
|
26
|
+
* over its buffer (`getBigInt64` would box a bigint each row); the bigint is
|
|
27
|
+
* materialized lazily, only when the consumer reads `ts[i]`.
|
|
28
|
+
*
|
|
29
|
+
* 2. TRANSFERABLE. Each column is a fresh, exactly-sized typed array that OWNS
|
|
30
|
+
* its `ArrayBuffer` at offset 0, so a batch ships to a Worker / WASM kernel
|
|
31
|
+
* zero-copy: `postMessage(batch, columns.map(c => c.buffer))`.
|
|
32
|
+
*
|
|
33
|
+
* 3. RESPECTS INCOMPLETE BUFFERS (streaming). Because the stride is constant,
|
|
34
|
+
* honoring a partial trailing row is pure ARITHMETIC: the number of complete
|
|
35
|
+
* rows in the buffer is `(work.length / STRIDE) | 0`. No `advance()`, no
|
|
36
|
+
* `NeedMoreData`, no throw/restart — the leftover `work.length % STRIDE` bytes
|
|
37
|
+
* just carry to the next chunk. Strictly cheaper than the row-oriented
|
|
38
|
+
* `streamRowBatches`, which re-decodes the partial row on every boundary.
|
|
39
|
+
*
|
|
40
|
+
* SCOPE: fixed-width numeric columns only — the ClickHouse types with a 1:1
|
|
41
|
+
* native TypedArray (`Int8/16/32/64`, `UInt8/16/32/64`, `Float32/64`). Anything
|
|
42
|
+
* whose value isn't one native-typed number has no constant stride to divide by
|
|
43
|
+
* (`String`/`Array`/`Map`/`Tuple`) or no 1:1 array (`Int128`+, `Decimal*`,
|
|
44
|
+
* `BFloat16`); decode those row-wise. `Bool`/`Enum`/`Date*`/`DateTime*` ride
|
|
45
|
+
* their underlying int here for the RAW value.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** One decoded batch of the example schema: `rows` complete rows, one typed array per column. */
|
|
49
|
+
export interface SensorColumnBatch {
|
|
50
|
+
/** Number of complete rows decoded in this batch. */
|
|
51
|
+
rows: number;
|
|
52
|
+
columns: {
|
|
53
|
+
sensor_id: Uint32Array;
|
|
54
|
+
ts: BigInt64Array; // raw DateTime64(3) ms ticks
|
|
55
|
+
value: Float64Array;
|
|
56
|
+
quality: Float32Array;
|
|
57
|
+
status: Uint8Array;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Byte stride of one fixed-width row: 4 + 8 + 8 + 4 + 1. */
|
|
62
|
+
const STRIDE = 25;
|
|
63
|
+
|
|
64
|
+
const EMPTY_CHUNK = Buffer.alloc(0);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Stream a chunked RowBinary response of the example schema into columnar
|
|
68
|
+
* batches: one `{ rows, columns }` per incoming chunk, holding exactly the rows
|
|
69
|
+
* that completed within it.
|
|
70
|
+
*
|
|
71
|
+
* BACKPRESSURE: a pull stream — the next chunk is requested only when the
|
|
72
|
+
* consumer asks for the next batch. SMALL CHUNKS: tiny chunks mean tiny batches
|
|
73
|
+
* (more allocations, worse Worker amortization); compose `coalesceChunks` (from
|
|
74
|
+
* `./stream.js`) in front to merge them up to a target size first.
|
|
75
|
+
*/
|
|
76
|
+
export async function* streamSensorColumns(
|
|
77
|
+
chunks: AsyncIterable<Uint8Array>,
|
|
78
|
+
): AsyncGenerator<SensorColumnBatch, void, undefined> {
|
|
79
|
+
let carry: Buffer = EMPTY_CHUNK;
|
|
80
|
+
for await (const chunk of chunks) {
|
|
81
|
+
// Wrap as a Buffer VIEW over the chunk's bytes — no copy (a Buffer made from
|
|
82
|
+
// an ArrayBuffer slice shares it). We own the chunk for the life of this
|
|
83
|
+
// generator, so holding a view into it is safe.
|
|
84
|
+
const incoming = Buffer.from(
|
|
85
|
+
chunk.buffer,
|
|
86
|
+
chunk.byteOffset,
|
|
87
|
+
chunk.byteLength,
|
|
88
|
+
);
|
|
89
|
+
const work =
|
|
90
|
+
carry.length === 0 ? incoming : Buffer.concat([carry, incoming]);
|
|
91
|
+
|
|
92
|
+
// Complete rows available right now — pure arithmetic, since STRIDE is fixed.
|
|
93
|
+
const n = (work.length / STRIDE) | 0;
|
|
94
|
+
if (n > 0) {
|
|
95
|
+
const view = new DataView(work.buffer, work.byteOffset, work.byteLength);
|
|
96
|
+
const sensor_id = new Uint32Array(n);
|
|
97
|
+
const ts = new BigInt64Array(n);
|
|
98
|
+
// Uint32 view over ts's OWN bytes: 2 little-endian words per Int64,
|
|
99
|
+
// [lo, hi, lo, hi, ...]. Filling ts through this view copies the raw bytes
|
|
100
|
+
// and skips the per-row bigint allocation `getBigInt64` would force; the
|
|
101
|
+
// bigint is materialized lazily, only for rows the consumer indexes.
|
|
102
|
+
const tsWords = new Uint32Array(ts.buffer);
|
|
103
|
+
const value = new Float64Array(n);
|
|
104
|
+
const quality = new Float32Array(n);
|
|
105
|
+
const status = new Uint8Array(n);
|
|
106
|
+
for (let i = 0, o = 0; i < n; i++, o += STRIDE) {
|
|
107
|
+
sensor_id[i] = view.getUint32(o, true); // UInt32 @ o+0
|
|
108
|
+
// DateTime64(3) Int64 @ o+4: two LE 32-bit words, no bigint allocated.
|
|
109
|
+
tsWords[i * 2] = view.getUint32(o + 4, true); // low word
|
|
110
|
+
tsWords[i * 2 + 1] = view.getUint32(o + 8, true); // high word
|
|
111
|
+
value[i] = view.getFloat64(o + 12, true); // Float64 @ o+12
|
|
112
|
+
quality[i] = view.getFloat32(o + 20, true); // Float32 @ o+20
|
|
113
|
+
status[i] = work[o + 24]!; // UInt8 @ o+24
|
|
114
|
+
}
|
|
115
|
+
yield { rows: n, columns: { sensor_id, ts, value, quality, status } };
|
|
116
|
+
}
|
|
117
|
+
// Carry the partial trailing row (if any) to the next chunk.
|
|
118
|
+
carry = work.subarray(n * STRIDE);
|
|
119
|
+
}
|
|
120
|
+
if (carry.length > 0) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`RowBinary stream ended mid-row: ${carry.length} trailing byte(s) left undecoded`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fold: turn a parsed ClickHouse data-type AST (from
|
|
3
|
+
* `@clickhouse/datatype-parser`) into a RowBinary value {@link Reader}.
|
|
4
|
+
*
|
|
5
|
+
* AST in, reader out — nothing else. Reading the `RowBinaryWithNamesAndTypes`
|
|
6
|
+
* header, parsing type strings, and assembling a row reader live in
|
|
7
|
+
* `rowBinaryWithNamesAndTypes.ts`; this module is just the type-to-reader
|
|
8
|
+
* mapping.
|
|
9
|
+
*
|
|
10
|
+
* It composes the existing GENERIC curried combinators at runtime, exactly as a
|
|
11
|
+
* hand-written reader would — no code generation and no monomorphization (those
|
|
12
|
+
* are the deliberate next step; see the `MONOMORPHIZE` notes throughout the
|
|
13
|
+
* combinator modules). The shape mirrors {@link readDynamicType} in
|
|
14
|
+
* `dynamic.ts`, which performs the same "type → Reader" mapping but driven by
|
|
15
|
+
* ClickHouse's BINARY type encoding rather than the textual type's AST.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { NodeKind, type Node } from "@clickhouse/datatype-parser";
|
|
19
|
+
|
|
20
|
+
import type { Reader } from "./core.js";
|
|
21
|
+
import {
|
|
22
|
+
readInt8,
|
|
23
|
+
readInt16,
|
|
24
|
+
readInt32,
|
|
25
|
+
readInt64,
|
|
26
|
+
readInt128,
|
|
27
|
+
readInt256,
|
|
28
|
+
readUInt8,
|
|
29
|
+
readUInt16,
|
|
30
|
+
readUInt32,
|
|
31
|
+
readUInt64,
|
|
32
|
+
readUInt128,
|
|
33
|
+
readUInt256,
|
|
34
|
+
} from "./integers.js";
|
|
35
|
+
import { readFloat32, readFloat64, readBFloat16 } from "./floats.js";
|
|
36
|
+
import { readBool } from "./bool.js";
|
|
37
|
+
import { readString, readFixedString } from "./strings.js";
|
|
38
|
+
import { readUUID } from "./uuid.js";
|
|
39
|
+
import { readIPv4, readIPv6 } from "./ip.js";
|
|
40
|
+
import {
|
|
41
|
+
readDate,
|
|
42
|
+
readDate32,
|
|
43
|
+
readDateTime,
|
|
44
|
+
readDateTime64,
|
|
45
|
+
} from "./datetime.js";
|
|
46
|
+
import { readTime, readTime64 } from "./time.js";
|
|
47
|
+
import { readInterval } from "./interval.js";
|
|
48
|
+
import {
|
|
49
|
+
readDecimal32,
|
|
50
|
+
readDecimal64,
|
|
51
|
+
readDecimal128,
|
|
52
|
+
readDecimal256,
|
|
53
|
+
} from "./decimals.js";
|
|
54
|
+
import { readEnum8, readEnum16 } from "./enums.js";
|
|
55
|
+
import {
|
|
56
|
+
readArray,
|
|
57
|
+
readMap,
|
|
58
|
+
readNullable,
|
|
59
|
+
readTuple,
|
|
60
|
+
readTupleNamed,
|
|
61
|
+
readVariant,
|
|
62
|
+
readQBit,
|
|
63
|
+
} from "./composite.js";
|
|
64
|
+
import { readLowCardinality } from "./lowCardinality.js";
|
|
65
|
+
import { readNested } from "./nested.js";
|
|
66
|
+
import { readNothing } from "./nothing.js";
|
|
67
|
+
import {
|
|
68
|
+
readPoint,
|
|
69
|
+
readRing,
|
|
70
|
+
readLineString,
|
|
71
|
+
readPolygon,
|
|
72
|
+
readMultiLineString,
|
|
73
|
+
readMultiPolygon,
|
|
74
|
+
readGeometry,
|
|
75
|
+
} from "./geo.js";
|
|
76
|
+
import { readJSON } from "./json.js";
|
|
77
|
+
import { readDynamic } from "./dynamic.js";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Thrown when a ClickHouse type cannot be turned into a RowBinary reader —
|
|
81
|
+
* either the type string did not parse, or the parsed type is unsupported or
|
|
82
|
+
* malformed for RowBinary decoding (`AggregateFunction`, a missing argument, a
|
|
83
|
+
* malformed `Nested(...)`, …).
|
|
84
|
+
*
|
|
85
|
+
* A dedicated class so callers can `catch (e) { if (e instanceof
|
|
86
|
+
* RowBinaryTypeError) … }` and branch on a bad-type error specifically rather
|
|
87
|
+
* than string-matching a generic `Error`. Note the standalone
|
|
88
|
+
* `@clickhouse/datatype-parser` is itself NON-throwing (it returns a
|
|
89
|
+
* `ParseResult`); this is the error the compile layer raises on top of it.
|
|
90
|
+
*/
|
|
91
|
+
export class RowBinaryTypeError extends Error {
|
|
92
|
+
/** The full ClickHouse type string being compiled, when the throw site knows it. */
|
|
93
|
+
readonly typeString?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Byte offset into `typeString` where parsing stopped — set for PARSE
|
|
96
|
+
* failures (from the underlying parser), undefined for fold-time errors.
|
|
97
|
+
*/
|
|
98
|
+
readonly position?: number;
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
message: string,
|
|
102
|
+
options: { typeString?: string; position?: number } = {},
|
|
103
|
+
) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = "RowBinaryTypeError";
|
|
106
|
+
this.typeString = options.typeString;
|
|
107
|
+
this.position = options.position;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The fold itself: turn a parsed type-AST node into a value {@link Reader},
|
|
113
|
+
* recursing into element / key / field types for composites. The shape mirrors
|
|
114
|
+
* the server's `EXPLAIN AST` data-type subtree (see the parser's `ast.ts`).
|
|
115
|
+
*/
|
|
116
|
+
export function astToReader(node: Node): Reader<unknown> {
|
|
117
|
+
switch (node.kind) {
|
|
118
|
+
case NodeKind.EnumDataType:
|
|
119
|
+
// Explicit-value enum: the wire value is the underlying int; the
|
|
120
|
+
// name<->value map is metadata we don't need to decode.
|
|
121
|
+
return node.name === "Enum16" ? readEnum16 : readEnum8;
|
|
122
|
+
case NodeKind.TupleDataType:
|
|
123
|
+
return tupleReader(node);
|
|
124
|
+
case NodeKind.DataType:
|
|
125
|
+
return dataTypeReader(node);
|
|
126
|
+
default:
|
|
127
|
+
// Literal / Function / Identifier / NameTypePair are argument nodes,
|
|
128
|
+
// consumed by their parent — never a standalone column type.
|
|
129
|
+
throw new RowBinaryTypeError(
|
|
130
|
+
`cannot build a column reader for a ${node.kind} node (${node.name || "?"})`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fold a generic `DataType` node (`name` + optional `arguments`) — the bulk of
|
|
137
|
+
* the type system: scalars, the parameterized types, and the
|
|
138
|
+
* wrappers/composites that recurse through {@link astToReader}.
|
|
139
|
+
*/
|
|
140
|
+
function dataTypeReader(node: Node): Reader<unknown> {
|
|
141
|
+
switch (node.name) {
|
|
142
|
+
// --- wrappers & composites (recurse into argument types) ---
|
|
143
|
+
case "Nullable":
|
|
144
|
+
return readNullable(astToReader(requireArg(node, 0)));
|
|
145
|
+
case "LowCardinality":
|
|
146
|
+
// Transparent in RowBinary: just the inner type's reader.
|
|
147
|
+
return readLowCardinality(astToReader(requireArg(node, 0)));
|
|
148
|
+
case "Array":
|
|
149
|
+
return readArray(astToReader(requireArg(node, 0)));
|
|
150
|
+
case "QBit":
|
|
151
|
+
// QBit(element_type, dimension): wire-identical to Array(element_type);
|
|
152
|
+
// the dimension (arg 1) is metadata, not in the value stream.
|
|
153
|
+
return readQBit(astToReader(requireArg(node, 0)));
|
|
154
|
+
case "Map":
|
|
155
|
+
return readMap(
|
|
156
|
+
astToReader(requireArg(node, 0)),
|
|
157
|
+
astToReader(requireArg(node, 1)),
|
|
158
|
+
);
|
|
159
|
+
case "Variant":
|
|
160
|
+
return variantReader(node);
|
|
161
|
+
case "Nested":
|
|
162
|
+
return nestedReader(node);
|
|
163
|
+
|
|
164
|
+
// --- parameterized scalars ---
|
|
165
|
+
case "FixedString":
|
|
166
|
+
return readFixedString(literalInt(requireArg(node, 0)));
|
|
167
|
+
case "DateTime":
|
|
168
|
+
case "DateTime32":
|
|
169
|
+
// An optional timezone argument is metadata; the value wire is the same.
|
|
170
|
+
return readDateTime;
|
|
171
|
+
case "DateTime64":
|
|
172
|
+
// DateTime64(P [, 'tz']); default precision 3 if somehow omitted.
|
|
173
|
+
return readDateTime64(
|
|
174
|
+
node.arguments.length > 0 ? literalInt(node.arguments[0]!) : 3,
|
|
175
|
+
);
|
|
176
|
+
case "Time64":
|
|
177
|
+
return readTime64(literalInt(requireArg(node, 0)));
|
|
178
|
+
case "Decimal":
|
|
179
|
+
return decimalReader(node);
|
|
180
|
+
case "Decimal32":
|
|
181
|
+
return readDecimal32(literalInt(requireArg(node, 0)));
|
|
182
|
+
case "Decimal64":
|
|
183
|
+
return readDecimal64(literalInt(requireArg(node, 0)));
|
|
184
|
+
case "Decimal128":
|
|
185
|
+
return readDecimal128(literalInt(requireArg(node, 0)));
|
|
186
|
+
case "Decimal256":
|
|
187
|
+
return readDecimal256(literalInt(requireArg(node, 0)));
|
|
188
|
+
// Auto-assigned enums arrive as a plain DataType (no explicit values); the
|
|
189
|
+
// wire value is still the underlying int.
|
|
190
|
+
case "Enum8":
|
|
191
|
+
return readEnum8;
|
|
192
|
+
case "Enum16":
|
|
193
|
+
return readEnum16;
|
|
194
|
+
|
|
195
|
+
default: {
|
|
196
|
+
// Interval<Unit> (IntervalSecond, IntervalDay, …): all decode to the
|
|
197
|
+
// signed Int64 count; the unit lives in the type name, not the wire.
|
|
198
|
+
if (node.name.startsWith("Interval")) return readInterval;
|
|
199
|
+
const leaf = NULLARY[node.name];
|
|
200
|
+
if (leaf !== undefined) return leaf;
|
|
201
|
+
throw new RowBinaryTypeError(`unsupported RowBinary type: ${node.name}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Folds a `Tuple(...)` — named (object) when every element is named, else positional (array). */
|
|
207
|
+
function tupleReader(node: Node): Reader<unknown> {
|
|
208
|
+
const readers = node.arguments.map(astToReader);
|
|
209
|
+
const names = node.element_names;
|
|
210
|
+
const named =
|
|
211
|
+
names.length === readers.length && names.every((n) => n.length > 0);
|
|
212
|
+
if (named) {
|
|
213
|
+
const fields: Record<string, Reader<unknown>> = {};
|
|
214
|
+
for (let i = 0; i < names.length; i++) fields[names[i]!] = readers[i]!;
|
|
215
|
+
return readTupleNamed(fields);
|
|
216
|
+
}
|
|
217
|
+
return readTuple(readers);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Folds a `Variant(...)`. The 1-byte discriminant indexes the alternatives
|
|
222
|
+
* sorted by type NAME (ClickHouse's global ordering), and the server writes the
|
|
223
|
+
* type string with alternatives ALREADY in that sorted order — so for a
|
|
224
|
+
* header-sourced type the AST argument order is the discriminant order and we
|
|
225
|
+
* pass them straight through. (A hand-written, non-normalized `Variant(...)`
|
|
226
|
+
* string would need sorting first.)
|
|
227
|
+
*/
|
|
228
|
+
function variantReader(node: Node): Reader<unknown> {
|
|
229
|
+
return readVariant(node.arguments.map(astToReader));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Folds a `Nested(name Type, …)`: on the wire it IS `Array(Tuple(...))` with
|
|
234
|
+
* the field names, so we compose {@link readNested} (= `readArray(readTupleNamed)`)
|
|
235
|
+
* over the `NameTypePair` children.
|
|
236
|
+
*/
|
|
237
|
+
function nestedReader(node: Node): Reader<unknown> {
|
|
238
|
+
const fields: Record<string, Reader<unknown>> = {};
|
|
239
|
+
for (const child of node.arguments) {
|
|
240
|
+
if (child.kind !== NodeKind.NameTypePair || child.data_type === null) {
|
|
241
|
+
throw new RowBinaryTypeError(
|
|
242
|
+
"malformed Nested(...): expected name/type pairs",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
fields[child.name] = astToReader(child.data_type);
|
|
246
|
+
}
|
|
247
|
+
return readNested(fields);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Folds `Decimal(P, S)` to the right width by precision P; scale S drives decoding. */
|
|
251
|
+
function decimalReader(node: Node): Reader<unknown> {
|
|
252
|
+
const precision = literalInt(requireArg(node, 0));
|
|
253
|
+
const scale = literalInt(requireArg(node, 1));
|
|
254
|
+
if (precision <= 9) return readDecimal32(scale);
|
|
255
|
+
if (precision <= 18) return readDecimal64(scale);
|
|
256
|
+
if (precision <= 38) return readDecimal128(scale);
|
|
257
|
+
return readDecimal256(scale);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The nullary (no-argument) scalar types, mapped to their leaf readers. */
|
|
261
|
+
const NULLARY: Record<string, Reader<unknown>> = {
|
|
262
|
+
UInt8: readUInt8,
|
|
263
|
+
UInt16: readUInt16,
|
|
264
|
+
UInt32: readUInt32,
|
|
265
|
+
UInt64: readUInt64,
|
|
266
|
+
UInt128: readUInt128,
|
|
267
|
+
UInt256: readUInt256,
|
|
268
|
+
Int8: readInt8,
|
|
269
|
+
Int16: readInt16,
|
|
270
|
+
Int32: readInt32,
|
|
271
|
+
Int64: readInt64,
|
|
272
|
+
Int128: readInt128,
|
|
273
|
+
Int256: readInt256,
|
|
274
|
+
Float32: readFloat32,
|
|
275
|
+
Float64: readFloat64,
|
|
276
|
+
BFloat16: readBFloat16,
|
|
277
|
+
Bool: readBool,
|
|
278
|
+
String: readString,
|
|
279
|
+
UUID: readUUID,
|
|
280
|
+
IPv4: readIPv4,
|
|
281
|
+
IPv6: readIPv6,
|
|
282
|
+
Date: readDate,
|
|
283
|
+
Date32: readDate32,
|
|
284
|
+
Time: readTime,
|
|
285
|
+
Nothing: readNothing,
|
|
286
|
+
// Geo types.
|
|
287
|
+
Point: readPoint,
|
|
288
|
+
Ring: readRing,
|
|
289
|
+
LineString: readLineString,
|
|
290
|
+
Polygon: readPolygon,
|
|
291
|
+
MultiLineString: readMultiLineString,
|
|
292
|
+
MultiPolygon: readMultiPolygon,
|
|
293
|
+
Geometry: readGeometry as Reader<unknown>,
|
|
294
|
+
// Self-describing types: bare `JSON` / `Dynamic` (any args are metadata).
|
|
295
|
+
JSON: readJSON,
|
|
296
|
+
Dynamic: readDynamic,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/** Argument accessor that fails loudly instead of returning `undefined`. */
|
|
300
|
+
function requireArg(node: Node, index: number): Node {
|
|
301
|
+
const arg = node.arguments[index];
|
|
302
|
+
if (arg === undefined) {
|
|
303
|
+
throw new RowBinaryTypeError(
|
|
304
|
+
`type ${node.name} is missing argument ${index}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return arg;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Reads an integer out of a `Literal` argument node (e.g. the N in FixedString(N)). */
|
|
311
|
+
function literalInt(node: Node): number {
|
|
312
|
+
if (node.kind !== NodeKind.Literal) {
|
|
313
|
+
throw new RowBinaryTypeError(
|
|
314
|
+
`expected a literal argument, got a ${node.kind} node`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return Number(node.value);
|
|
318
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { type Reader } from "./core.js";
|
|
2
|
+
import { readUInt8 } from "./integers.js";
|
|
3
|
+
import { readUVarint } from "./varint.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read a `Nullable(T)`: a 1-byte null flag (0 = present, non-zero = NULL).
|
|
7
|
+
* Curried: pass the inner reader, get a `Reader<T | null>`.
|
|
8
|
+
*
|
|
9
|
+
* GOTCHA: the inner value bytes follow ONLY when the flag is 0. A NULL is the
|
|
10
|
+
* single `0x01` flag byte with nothing after it — so do NOT read the inner value
|
|
11
|
+
* when the flag is set, or the cursor desyncs.
|
|
12
|
+
*
|
|
13
|
+
* `readValue` decodes the inner `T`. This generic combinator is the reference
|
|
14
|
+
* shape; when generating code, MONOMORPHIZE — emit a dedicated `readNullableX`
|
|
15
|
+
* that inlines the inner read:
|
|
16
|
+
*
|
|
17
|
+
* const readNullableUInt32 = (s) => readUInt8(s) !== 0 ? null : readUInt32(s);
|
|
18
|
+
*/
|
|
19
|
+
export function readNullable<T>(readValue: Reader<T>): Reader<T | null> {
|
|
20
|
+
return (state) => (readUInt8(state) !== 0 ? null : readValue(state));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read an `Array(T)`: a LEB128 element count, then that many `T` values
|
|
25
|
+
* back-to-back. An empty array is just the count byte `0x00`. Curried: pass the
|
|
26
|
+
* element reader, get a `Reader<T[]>`.
|
|
27
|
+
*
|
|
28
|
+
* ARRAY LAYOUT: the count is known up front (the LEB128 prefix), so a generated
|
|
29
|
+
* reader can pre-size. Pick by how the result is used:
|
|
30
|
+
* - small / consumed-as-is (the common case) → DEFAULT to `new Array(n)` +
|
|
31
|
+
* index assignment; it skips `push`'s repeated capacity growth. A clean-room
|
|
32
|
+
* benchmark found this edged out `push` on the small composite arrays here
|
|
33
|
+
* (`baseline/README.md`).
|
|
34
|
+
* - large + iterated/computed-over downstream → `[]` + `push` keeps it a PACKED
|
|
35
|
+
* elements kind (faster to traverse; a pre-sized array is HOLEY), or use a
|
|
36
|
+
* typed array (`Float64Array`…) for numeric elements.
|
|
37
|
+
* This generic combinator uses `push` for simplicity; the monomorphized
|
|
38
|
+
* `readArrayX` below should choose per the rule above.
|
|
39
|
+
*
|
|
40
|
+
* `readElement` decodes one element. This generic combinator is the reference
|
|
41
|
+
* shape; when generating code, MONOMORPHIZE — emit a dedicated `readArrayX` that
|
|
42
|
+
* inlines the element read in the loop (and pre-sizes for the common small case):
|
|
43
|
+
*
|
|
44
|
+
* function readArrayUInt32(s) {
|
|
45
|
+
* const n = readUVarint(s);
|
|
46
|
+
* const out = new Array(n);
|
|
47
|
+
* for (let i = 0; i < n; i++) out[i] = readUInt32(s);
|
|
48
|
+
* return out;
|
|
49
|
+
* }
|
|
50
|
+
*/
|
|
51
|
+
export function readArray<T>(readElement: Reader<T>): Reader<T[]> {
|
|
52
|
+
return (state) => {
|
|
53
|
+
const n = readUVarint(state);
|
|
54
|
+
const out: T[] = [];
|
|
55
|
+
for (let i = 0; i < n; i++) out.push(readElement(state));
|
|
56
|
+
return out;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read a `QBit(element_type, dimension)` vector. `QBit` is a vector-search type
|
|
62
|
+
* whose ON-DISK layout is quantized and bit-transposed — but that is a STORAGE /
|
|
63
|
+
* Native-format concern. In RowBinary a `QBit` is fully TRANSPARENT: it is the
|
|
64
|
+
* plain vector, encoded byte-for-byte like `Array(element_type)` (a LEB128
|
|
65
|
+
* length, then `dimension` element values). So this is just {@link readArray}.
|
|
66
|
+
*
|
|
67
|
+
* `element_type` is one of `BFloat16` / `Float32` / `Float64`, so `readElement`
|
|
68
|
+
* is the matching float reader. When generating code, MONOMORPHIZE — inline the
|
|
69
|
+
* element read in the loop.
|
|
70
|
+
*/
|
|
71
|
+
export function readQBit<T>(readElement: Reader<T>): Reader<T[]> {
|
|
72
|
+
return readArray(readElement);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read a `Tuple(...)` into a positional array: each element's value back-to-back,
|
|
77
|
+
* with NO count and NO delimiter. Curried: pass one reader per element (in
|
|
78
|
+
* order), get a `Reader` of the tuple. For a named tuple as an object, use
|
|
79
|
+
* {@link readTupleNamed} (identical wire).
|
|
80
|
+
*
|
|
81
|
+
* Reference shape; when generating code, MONOMORPHIZE — emit the inline sequence
|
|
82
|
+
* with no array-of-readers and no loop:
|
|
83
|
+
*
|
|
84
|
+
* [readUInt32(s), readString(s)]
|
|
85
|
+
*/
|
|
86
|
+
export function readTuple<T extends readonly unknown[]>(readers: {
|
|
87
|
+
[K in keyof T]: Reader<T[K]>;
|
|
88
|
+
}): Reader<T> {
|
|
89
|
+
return (state) => {
|
|
90
|
+
const out: unknown[] = [];
|
|
91
|
+
for (const read of readers as ReadonlyArray<Reader<unknown>>) {
|
|
92
|
+
out.push(read(state));
|
|
93
|
+
}
|
|
94
|
+
return out as unknown as T;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read a named `Tuple(name1 T1, ...)` into an object. The wire is identical to
|
|
100
|
+
* an unnamed tuple — values back-to-back, no count or delimiter — so the
|
|
101
|
+
* `readers` object's keys MUST be listed in the tuple's declared field order
|
|
102
|
+
* (JS iterates string keys in insertion order), and each reader runs in that
|
|
103
|
+
* order. Curried: pass the readers object, get a `Reader` of the result object.
|
|
104
|
+
*
|
|
105
|
+
* Reference shape; when generating code, MONOMORPHIZE — emit the inline object
|
|
106
|
+
* literal instead of looping over entries:
|
|
107
|
+
*
|
|
108
|
+
* { id: readUInt32(s), name: readString(s) }
|
|
109
|
+
*/
|
|
110
|
+
export function readTupleNamed<T extends Record<string, unknown>>(readers: {
|
|
111
|
+
[K in keyof T]: Reader<T[K]>;
|
|
112
|
+
}): Reader<T> {
|
|
113
|
+
const fns = readers as Record<string, Reader<unknown>>;
|
|
114
|
+
const keys = Object.keys(fns);
|
|
115
|
+
return (state) => {
|
|
116
|
+
const out: Record<string, unknown> = {};
|
|
117
|
+
for (const key of keys) out[key] = fns[key]!(state);
|
|
118
|
+
return out as T;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read a `Map(K, V)`: a LEB128 pair count, then that many key/value pairs with
|
|
124
|
+
* key and value interleaved (k, v, k, v, ...) — a flattened `Array(Tuple(K, V))`.
|
|
125
|
+
* An empty map is just the count byte `0x00`. Curried: pass the key and value
|
|
126
|
+
* readers, get a `Reader<Map<K, V>>`.
|
|
127
|
+
*
|
|
128
|
+
* The key is read BEFORE the value in each pair. Returns a JS `Map`, which keeps
|
|
129
|
+
* insertion order and accepts any key type.
|
|
130
|
+
*
|
|
131
|
+
* Reference shape; when generating code, MONOMORPHIZE — inline both reads in the
|
|
132
|
+
* loop.
|
|
133
|
+
*/
|
|
134
|
+
export function readMap<K, V>(
|
|
135
|
+
readKey: Reader<K>,
|
|
136
|
+
readValue: Reader<V>,
|
|
137
|
+
): Reader<Map<K, V>> {
|
|
138
|
+
return (state) => {
|
|
139
|
+
const n = readUVarint(state);
|
|
140
|
+
const out = new Map<K, V>();
|
|
141
|
+
for (let i = 0; i < n; i++) {
|
|
142
|
+
const key = readKey(state);
|
|
143
|
+
out.set(key, readValue(state));
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Read a `Variant(T1, ..., Tn)`: a 1-byte discriminant selecting the active
|
|
151
|
+
* alternative, then that alternative's value. Discriminant `0xFF` means NULL.
|
|
152
|
+
* Curried: pass the alternative readers (in sorted-type-name order), get a
|
|
153
|
+
* `Reader`.
|
|
154
|
+
*
|
|
155
|
+
* GOTCHA: the discriminant indexes the alternatives sorted by type NAME
|
|
156
|
+
* (ClickHouse globally sorts them), NOT their declaration order. So `readers`
|
|
157
|
+
* MUST be ordered by sorted type name. E.g. `Variant(UInt8, String)` sorts to
|
|
158
|
+
* ["String", "UInt8"], so discriminant 0 = String and 1 = UInt8.
|
|
159
|
+
*
|
|
160
|
+
* Reference shape; when generating code, MONOMORPHIZE — emit a `switch` over the
|
|
161
|
+
* discriminant with each branch inlined, alternatives in sorted order, `0xFF`
|
|
162
|
+
* -> null.
|
|
163
|
+
*/
|
|
164
|
+
export function readVariant<T extends readonly unknown[]>(readers: {
|
|
165
|
+
[K in keyof T]: Reader<T[K]>;
|
|
166
|
+
}): Reader<T[number] | null> {
|
|
167
|
+
const fns = readers as ReadonlyArray<Reader<T[number]>>;
|
|
168
|
+
return (state) => {
|
|
169
|
+
const discriminant = readUInt8(state);
|
|
170
|
+
if (discriminant === 0xff) return null;
|
|
171
|
+
const fn = fns[discriminant];
|
|
172
|
+
if (fn === undefined) {
|
|
173
|
+
// Out-of-range discriminant (corrupted/truncated input): fail loudly
|
|
174
|
+
// instead of throwing a cryptic "fns[discriminant] is not a function".
|
|
175
|
+
throw new RangeError(
|
|
176
|
+
`RowBinary Variant: discriminant ${discriminant} out of range (${fns.length} alternatives)`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return fn(state);
|
|
180
|
+
};
|
|
181
|
+
}
|