@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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link advance} when the buffer lacks the bytes a read needs — the
|
|
3
|
+
* "need more bytes" signal for incremental decoding over a still-filling buffer.
|
|
4
|
+
* A driver catches it (`err === NeedMoreData`), waits for more input, and retries
|
|
5
|
+
* the row from its last committed position.
|
|
6
|
+
*
|
|
7
|
+
* A bare sentinel, NOT an `Error` subclass, on purpose: constructing an Error
|
|
8
|
+
* captures a stack trace — the expensive part of throwing — and on a path that
|
|
9
|
+
* starves once per chunk that cost is pure waste. Throwing a constant skips it,
|
|
10
|
+
* which is why throw + restart beats a generator's yield for realistic chunks
|
|
11
|
+
* (see `streamingRow.bench.ts`).
|
|
12
|
+
*/
|
|
13
|
+
export const NeedMoreData = Symbol("RowBinary.NeedMoreData");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The cursor state every reader threads through: the input `Buffer`, the current
|
|
17
|
+
* position, and a `DataView` over the same bytes.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately STATE only — no read methods. Decoding lives in the free
|
|
20
|
+
* `readX(state, ...)` functions in the sibling modules, so a generated parser
|
|
21
|
+
* pulls in only the per-type readers a result needs. `view`/`buf` are public so
|
|
22
|
+
* those free functions can reach them.
|
|
23
|
+
*/
|
|
24
|
+
export class Cursor {
|
|
25
|
+
pos = 0;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Node-only skill, so the input is a `Buffer`: number reads go through
|
|
29
|
+
* {@link Cursor.view} (DataView), while `String`/`FixedString` use the
|
|
30
|
+
* fast `buf.toString("utf8", ...)`.
|
|
31
|
+
*/
|
|
32
|
+
readonly buf: Buffer;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `DataView` over the same bytes, for fixed-width integer/float reads. Built
|
|
36
|
+
* with the buffer's own `byteOffset`/`byteLength`: a `Buffer` is often a window
|
|
37
|
+
* into a larger pooled `ArrayBuffer`, so `new DataView(buf.buffer)` alone would
|
|
38
|
+
* point at the wrong bytes.
|
|
39
|
+
*/
|
|
40
|
+
readonly view: DataView;
|
|
41
|
+
|
|
42
|
+
constructor(buf: Buffer) {
|
|
43
|
+
this.buf = buf;
|
|
44
|
+
this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A `Reader<T>` decodes one value of type `T` from the cursor, advancing it. Leaf
|
|
50
|
+
* readers (e.g. `readUInt32`) are `Reader`s directly; combinators (e.g.
|
|
51
|
+
* `readArray`) take sub-`Reader`s and return a `Reader`, so types compose with no
|
|
52
|
+
* per-element closures.
|
|
53
|
+
*/
|
|
54
|
+
export type Reader<T> = (state: Cursor) => T;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reserve `n` bytes for the next read: bounds-check them, advance the cursor past
|
|
58
|
+
* them, and return the offset the read starts at (the value BEFORE advancing).
|
|
59
|
+
* Every fixed-width read goes through this, so the length check and cursor
|
|
60
|
+
* bookkeeping live in one place:
|
|
61
|
+
*
|
|
62
|
+
* function readInt32(s) { return s.view.getInt32(advance(s, 4), true); }
|
|
63
|
+
*
|
|
64
|
+
* Throws {@link NeedMoreData} when fewer than `n` bytes remain, WITHOUT moving the
|
|
65
|
+
* cursor, so a driver can rewind to its last committed row and retry.
|
|
66
|
+
*
|
|
67
|
+
* SAFE TO TOGGLE: for a complete in-memory buffer the check never fires — a parser
|
|
68
|
+
* for that case can drop `advance` and read against `state.pos` directly, trading
|
|
69
|
+
* streaming tolerance for one fewer compare per read.
|
|
70
|
+
*/
|
|
71
|
+
export function advance(state: Cursor, n: number): number {
|
|
72
|
+
const start = state.pos;
|
|
73
|
+
const next = start + n;
|
|
74
|
+
if (next > state.buf.length) throw NeedMoreData;
|
|
75
|
+
state.pos = next;
|
|
76
|
+
return start;
|
|
77
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type Reader, Cursor } from "./core.js";
|
|
2
|
+
import { readInt32, readInt64, readUInt16, readUInt32 } from "./integers.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Semantic aliases for `number` that mark the unit of a temporal value in a
|
|
6
|
+
* return type. They are plain `number`s (no runtime brand) — purely to make
|
|
7
|
+
* `[Date, Nanoseconds]` etc. self-documenting at the call site.
|
|
8
|
+
*/
|
|
9
|
+
export type Milliseconds = number;
|
|
10
|
+
export type Microseconds = number;
|
|
11
|
+
export type Nanoseconds = number;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read a `Date`: 2-byte `UInt16` count of days since 1970-01-01 (UTC), returned
|
|
15
|
+
* as a JS `Date` at UTC midnight. A ClickHouse `Date` has no time or timezone;
|
|
16
|
+
* `.toISOString().slice(0, 10)` gives "YYYY-MM-DD".
|
|
17
|
+
*
|
|
18
|
+
* SAFE TO TOGGLE: a `Date` is an object allocation per value. On a hot path that
|
|
19
|
+
* only needs the calendar number, read the raw `UInt16` (days) instead.
|
|
20
|
+
*/
|
|
21
|
+
export function readDate(state: Cursor): Date {
|
|
22
|
+
return new Date(readUInt16(state) * 86_400_000);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read a `Date32`: 4-byte signed `Int32` count of days since 1970-01-01 (UTC),
|
|
27
|
+
* returned as a JS `Date` at UTC midnight (pre-1970 dates are negative day
|
|
28
|
+
* counts, which `Date` handles).
|
|
29
|
+
*/
|
|
30
|
+
export function readDate32(state: Cursor): Date {
|
|
31
|
+
return new Date(readInt32(state) * 86_400_000);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read a `DateTime` (and `DateTime(tz)`): 4-byte `UInt32` Unix seconds, returned
|
|
36
|
+
* as a JS `Date` (exact at second resolution). The instant is UTC-based; a
|
|
37
|
+
* column's timezone is display metadata, not in the bytes.
|
|
38
|
+
*/
|
|
39
|
+
export function readDateTime(state: Cursor): Date {
|
|
40
|
+
return new Date(readUInt32(state) * 1000);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read a `DateTime64(P)` (and `DateTime64(P, tz)`): 8-byte signed `Int64` count
|
|
45
|
+
* of `10^-P`-second ticks since the epoch. Curried: `readDateTime64(P)` returns
|
|
46
|
+
* the reader.
|
|
47
|
+
*
|
|
48
|
+
* Returns a pair `[date, nanoseconds]`: `date` is a JS `Date` truncated to whole
|
|
49
|
+
* seconds, and `nanoseconds` is the sub-second remainder in nanoseconds
|
|
50
|
+
* (0..999_999_999). The split keeps full precision a `Date` alone (millisecond
|
|
51
|
+
* resolution) can't hold. `nanoseconds` is always in ns regardless of P. Timezone
|
|
52
|
+
* is metadata.
|
|
53
|
+
*
|
|
54
|
+
* For the typical precisions, prefer the specialized variants
|
|
55
|
+
* {@link readDateTime64P3} (ms — returns a plain `Date`),
|
|
56
|
+
* {@link readDateTime64P6} (µs), and {@link readDateTime64P9} (ns).
|
|
57
|
+
*/
|
|
58
|
+
export function readDateTime64(precision: number): Reader<[Date, Nanoseconds]> {
|
|
59
|
+
return (state) => {
|
|
60
|
+
const ticks = readInt64(state);
|
|
61
|
+
const scale = 10n ** BigInt(precision);
|
|
62
|
+
let sec = ticks / scale;
|
|
63
|
+
let frac = ticks % scale;
|
|
64
|
+
if (frac < 0n) {
|
|
65
|
+
// Floor toward -inf so the fractional remainder stays in [0, scale).
|
|
66
|
+
frac += scale;
|
|
67
|
+
sec -= 1n;
|
|
68
|
+
}
|
|
69
|
+
return [new Date(Number(sec) * 1000), Number(frac) * 10 ** (9 - precision)];
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read a `DateTime64(3)` ({@link Milliseconds}) — the most common precision — as
|
|
75
|
+
* a plain JS `Date`. P=3 is exactly a `Date`'s own millisecond resolution, so the
|
|
76
|
+
* instant is represented losslessly with no separate fraction. Specialized
|
|
77
|
+
* variant of {@link readDateTime64} with the scale baked in.
|
|
78
|
+
*/
|
|
79
|
+
export function readDateTime64P3(state: Cursor): Date {
|
|
80
|
+
return new Date(Number(readInt64(state)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read a `DateTime64(6)` (microseconds) as `[date, microseconds]`: a JS `Date`
|
|
85
|
+
* truncated to whole seconds plus the sub-second remainder in microseconds.
|
|
86
|
+
* Specialized variant of {@link readDateTime64}.
|
|
87
|
+
*/
|
|
88
|
+
export function readDateTime64P6(state: Cursor): [Date, Microseconds] {
|
|
89
|
+
const ticks = readInt64(state);
|
|
90
|
+
let sec = ticks / 1_000_000n;
|
|
91
|
+
let frac = ticks % 1_000_000n; // microseconds within the second
|
|
92
|
+
if (frac < 0n) {
|
|
93
|
+
frac += 1_000_000n;
|
|
94
|
+
sec -= 1n;
|
|
95
|
+
}
|
|
96
|
+
return [new Date(Number(sec) * 1000), Number(frac)];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read a `DateTime64(9)` (nanoseconds) as `[date, nanoseconds]`: a JS `Date`
|
|
101
|
+
* truncated to whole seconds plus the sub-second remainder in nanoseconds.
|
|
102
|
+
* Specialized variant of {@link readDateTime64} with the scale baked in.
|
|
103
|
+
*/
|
|
104
|
+
export function readDateTime64P9(state: Cursor): [Date, Nanoseconds] {
|
|
105
|
+
const ticks = readInt64(state);
|
|
106
|
+
let sec = ticks / 1_000_000_000n;
|
|
107
|
+
let frac = ticks % 1_000_000_000n; // nanoseconds within the second
|
|
108
|
+
if (frac < 0n) {
|
|
109
|
+
frac += 1_000_000_000n;
|
|
110
|
+
sec -= 1n;
|
|
111
|
+
}
|
|
112
|
+
return [new Date(Number(sec) * 1000), Number(frac)];
|
|
113
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Reader } from "./core.js";
|
|
2
|
+
import { readInt32, readInt64, readInt128, readInt256 } from "./integers.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A decimal kept lossless as its raw parts: `value = unscaled / 10 ** scale`.
|
|
6
|
+
* The `readDecimal*` readers return this so no precision or scale information is
|
|
7
|
+
* thrown away at decode time.
|
|
8
|
+
*/
|
|
9
|
+
export type DecimalValue = readonly [unscaled: bigint, scale: number];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a {@link DecimalValue} as a fixed-point decimal string with `scale`
|
|
13
|
+
* fractional digits (e.g. `[15000n, 4]` -> `"1.5000"`). Plug in only when you
|
|
14
|
+
* need a string.
|
|
15
|
+
*
|
|
16
|
+
* Trailing zeros are preserved to reflect the declared scale, deliberately unlike
|
|
17
|
+
* ClickHouse's text output, which trims them (`"1.5"`) and drops the point for
|
|
18
|
+
* integers (`"10"`).
|
|
19
|
+
*/
|
|
20
|
+
export function formatDecimal([unscaled, scale]: DecimalValue): string {
|
|
21
|
+
if (scale === 0) return unscaled.toString();
|
|
22
|
+
if (unscaled < 0n) {
|
|
23
|
+
const digits = (-unscaled).toString().padStart(scale + 1, "0");
|
|
24
|
+
const point = digits.length - scale;
|
|
25
|
+
return `-${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
26
|
+
}
|
|
27
|
+
const digits = unscaled.toString().padStart(scale + 1, "0");
|
|
28
|
+
const point = digits.length - scale;
|
|
29
|
+
return `${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read a `Decimal32(P, S)`: a 4-byte little-endian signed integer (same wire
|
|
34
|
+
* shape as `Int32`) scaled by 10^S. Pass the column's scale `S`; returns a
|
|
35
|
+
* `Reader` of the raw `[unscaled, scale]` pair (see {@link formatDecimal}).
|
|
36
|
+
*
|
|
37
|
+
* `Decimal(P, S)` is an alias: pick the width reader by precision P — P<=9 ->
|
|
38
|
+
* Decimal32, <=18 -> Decimal64, <=38 -> Decimal128, <=76 -> Decimal256.
|
|
39
|
+
*/
|
|
40
|
+
export function readDecimal32(scale: number): Reader<DecimalValue> {
|
|
41
|
+
return (state) => [BigInt(readInt32(state)), scale];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Read a `Decimal64(P, S)`: 8-byte LE signed integer scaled by 10^S. Returns `[unscaled, scale]`; see {@link formatDecimal}. */
|
|
45
|
+
export function readDecimal64(scale: number): Reader<DecimalValue> {
|
|
46
|
+
return (state) => [readInt64(state), scale];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read a `Decimal128(P, S)`: 16-byte LE signed integer scaled by 10^S. Returns `[unscaled, scale]`; see {@link formatDecimal}. */
|
|
50
|
+
export function readDecimal128(scale: number): Reader<DecimalValue> {
|
|
51
|
+
return (state) => [readInt128(state), scale];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read a `Decimal256(P, S)`: 32-byte LE signed integer scaled by 10^S. Returns `[unscaled, scale]`; see {@link formatDecimal}. */
|
|
55
|
+
export function readDecimal256(scale: number): Reader<DecimalValue> {
|
|
56
|
+
return (state) => [readInt256(state), scale];
|
|
57
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { type Reader, Cursor } from "./core.js";
|
|
2
|
+
import { readUVarint } from "./varint.js";
|
|
3
|
+
import {
|
|
4
|
+
readInt8,
|
|
5
|
+
readInt16,
|
|
6
|
+
readInt32,
|
|
7
|
+
readInt64,
|
|
8
|
+
readInt128,
|
|
9
|
+
readInt256,
|
|
10
|
+
readUInt8,
|
|
11
|
+
readUInt16,
|
|
12
|
+
readUInt32,
|
|
13
|
+
readUInt64,
|
|
14
|
+
readUInt128,
|
|
15
|
+
readUInt256,
|
|
16
|
+
} from "./integers.js";
|
|
17
|
+
import { readBool } from "./bool.js";
|
|
18
|
+
import { readFloat32, readFloat64 } from "./floats.js";
|
|
19
|
+
import { readString, readFixedString } from "./strings.js";
|
|
20
|
+
import { readUUID } from "./uuid.js";
|
|
21
|
+
import { readIPv4, readIPv6 } from "./ip.js";
|
|
22
|
+
import {
|
|
23
|
+
readDate,
|
|
24
|
+
readDate32,
|
|
25
|
+
readDateTime,
|
|
26
|
+
readDateTime64,
|
|
27
|
+
} from "./datetime.js";
|
|
28
|
+
import {
|
|
29
|
+
readDecimal32,
|
|
30
|
+
readDecimal64,
|
|
31
|
+
readDecimal128,
|
|
32
|
+
readDecimal256,
|
|
33
|
+
} from "./decimals.js";
|
|
34
|
+
import {
|
|
35
|
+
INTERVAL_UNITS,
|
|
36
|
+
type IntervalValue,
|
|
37
|
+
readInterval,
|
|
38
|
+
} from "./interval.js";
|
|
39
|
+
import {
|
|
40
|
+
readArray,
|
|
41
|
+
readMap,
|
|
42
|
+
readNullable,
|
|
43
|
+
readTuple,
|
|
44
|
+
readTupleNamed,
|
|
45
|
+
readVariant,
|
|
46
|
+
} from "./composite.js";
|
|
47
|
+
import { readJSON } from "./json.js";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read one `Dynamic` value. A `Dynamic` is SELF-DESCRIBING: every value is a
|
|
51
|
+
* binary TYPE ENCODING followed by the value's RowBinary bytes. So unlike every
|
|
52
|
+
* other reader, the type is not known until runtime — this is the one place a
|
|
53
|
+
* generic runtime dispatch is correct and unavoidable. {@link readDynamicType}
|
|
54
|
+
* parses the type header into a value `Reader`; here we just invoke it.
|
|
55
|
+
*
|
|
56
|
+
* GOTCHA — wrappers are erased. `Dynamic` stores the CONCRETE type of the stored
|
|
57
|
+
* value, never a wrapper: a non-null `Nullable(UInt8)` is stored as plain
|
|
58
|
+
* `UInt8`, an active `Variant(...)` as just its current alternative's type. A
|
|
59
|
+
* NULL (from any source) is stored as `Nothing` (tag 0x00) and decodes to
|
|
60
|
+
* `null`. So you never see Nullable/Variant tags here.
|
|
61
|
+
*
|
|
62
|
+
* For a Dynamic-heavy hot path where the same few types recur, parse the type
|
|
63
|
+
* once and reuse the returned reader across rows instead of re-parsing.
|
|
64
|
+
*/
|
|
65
|
+
export function readDynamic(state: Cursor): unknown {
|
|
66
|
+
return readDynamicType(state)(state);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse a binary TYPE ENCODING (the header ClickHouse writes before each
|
|
71
|
+
* `Dynamic` value) and return a `Reader` that reads ONE value of that type. The
|
|
72
|
+
* type bytes are consumed now; the returned reader consumes only value bytes
|
|
73
|
+
* when called. Composites recurse: element/key/field types are parsed eagerly
|
|
74
|
+
* into inner readers, then composed with the existing combinators
|
|
75
|
+
* ({@link readArray}/{@link readTuple}/{@link readMap}/...).
|
|
76
|
+
*
|
|
77
|
+
* The leading byte is a 1-byte tag; parameterized types (FixedString, Enum,
|
|
78
|
+
* Decimal, DateTime64, timezone'd DateTime) carry extra LEB128/varint and string
|
|
79
|
+
* fields in the header, which we consume to reach the value reader.
|
|
80
|
+
*
|
|
81
|
+
* Only the tags ClickHouse actually emits for stored `Dynamic` values are
|
|
82
|
+
* handled (wrappers are erased — see {@link readDynamic}). Unknown tags throw
|
|
83
|
+
* with the tag value so you can extend this switch for the types your data
|
|
84
|
+
* actually contains.
|
|
85
|
+
*/
|
|
86
|
+
export function readDynamicType(state: Cursor): Reader<unknown> {
|
|
87
|
+
const tag = readUInt8(state);
|
|
88
|
+
switch (tag) {
|
|
89
|
+
// Nothing — a stored NULL. Zero value bytes.
|
|
90
|
+
case 0x00:
|
|
91
|
+
return () => null;
|
|
92
|
+
// Unsigned integers.
|
|
93
|
+
case 0x01:
|
|
94
|
+
return readUInt8;
|
|
95
|
+
case 0x02:
|
|
96
|
+
return readUInt16;
|
|
97
|
+
case 0x03:
|
|
98
|
+
return readUInt32;
|
|
99
|
+
case 0x04:
|
|
100
|
+
return readUInt64;
|
|
101
|
+
case 0x05:
|
|
102
|
+
return readUInt128;
|
|
103
|
+
case 0x06:
|
|
104
|
+
return readUInt256;
|
|
105
|
+
// Signed integers.
|
|
106
|
+
case 0x07:
|
|
107
|
+
return readInt8;
|
|
108
|
+
case 0x08:
|
|
109
|
+
return readInt16;
|
|
110
|
+
case 0x09:
|
|
111
|
+
return readInt32;
|
|
112
|
+
case 0x0a:
|
|
113
|
+
return readInt64;
|
|
114
|
+
case 0x0b:
|
|
115
|
+
return readInt128;
|
|
116
|
+
case 0x0c:
|
|
117
|
+
return readInt256;
|
|
118
|
+
// Floats.
|
|
119
|
+
case 0x0d:
|
|
120
|
+
return readFloat32;
|
|
121
|
+
case 0x0e:
|
|
122
|
+
return readFloat64;
|
|
123
|
+
// Dates and times. The timezone'd variants carry a tz string in the header
|
|
124
|
+
// (metadata only — identical value wire to the untimezoned form).
|
|
125
|
+
case 0x0f:
|
|
126
|
+
return readDate;
|
|
127
|
+
case 0x10:
|
|
128
|
+
return readDate32;
|
|
129
|
+
case 0x11:
|
|
130
|
+
return readDateTime;
|
|
131
|
+
case 0x12:
|
|
132
|
+
readString(state); // timezone name (metadata)
|
|
133
|
+
return readDateTime;
|
|
134
|
+
case 0x13:
|
|
135
|
+
return readDateTime64(readUVarint(state));
|
|
136
|
+
case 0x14: {
|
|
137
|
+
const precision = readUVarint(state);
|
|
138
|
+
readString(state); // timezone name (metadata)
|
|
139
|
+
return readDateTime64(precision);
|
|
140
|
+
}
|
|
141
|
+
// String / FixedString(N).
|
|
142
|
+
case 0x15:
|
|
143
|
+
return readString;
|
|
144
|
+
case 0x16:
|
|
145
|
+
return readFixedString(readUVarint(state));
|
|
146
|
+
// Enum8 / Enum16: a count then (name String, value Int8/Int16) pairs. The
|
|
147
|
+
// name<->value map is metadata; the stored value is the underlying int.
|
|
148
|
+
case 0x17: {
|
|
149
|
+
const n = readUVarint(state);
|
|
150
|
+
for (let i = 0; i < n; i++) {
|
|
151
|
+
readString(state);
|
|
152
|
+
readInt8(state);
|
|
153
|
+
}
|
|
154
|
+
return readInt8;
|
|
155
|
+
}
|
|
156
|
+
case 0x18: {
|
|
157
|
+
const n = readUVarint(state);
|
|
158
|
+
for (let i = 0; i < n; i++) {
|
|
159
|
+
readString(state);
|
|
160
|
+
readInt16(state);
|
|
161
|
+
}
|
|
162
|
+
return readInt16;
|
|
163
|
+
}
|
|
164
|
+
// Decimals: header carries precision P then scale S (both varint). Only S
|
|
165
|
+
// matters for decoding; P is consumed and dropped. Returns [unscaled, S].
|
|
166
|
+
case 0x19: {
|
|
167
|
+
readUVarint(state);
|
|
168
|
+
return readDecimal32(readUVarint(state));
|
|
169
|
+
}
|
|
170
|
+
case 0x1a: {
|
|
171
|
+
readUVarint(state);
|
|
172
|
+
return readDecimal64(readUVarint(state));
|
|
173
|
+
}
|
|
174
|
+
case 0x1b: {
|
|
175
|
+
readUVarint(state);
|
|
176
|
+
return readDecimal128(readUVarint(state));
|
|
177
|
+
}
|
|
178
|
+
case 0x1c: {
|
|
179
|
+
readUVarint(state);
|
|
180
|
+
return readDecimal256(readUVarint(state));
|
|
181
|
+
}
|
|
182
|
+
case 0x1d:
|
|
183
|
+
return readUUID;
|
|
184
|
+
// Array(T): parse the element type once, then read a length-prefixed run.
|
|
185
|
+
case 0x1e:
|
|
186
|
+
return readArray(readDynamicType(state));
|
|
187
|
+
// Tuple(...): a field count, then that many element type encodings.
|
|
188
|
+
case 0x1f: {
|
|
189
|
+
const n = readUVarint(state);
|
|
190
|
+
const fields: Array<Reader<unknown>> = [];
|
|
191
|
+
for (let i = 0; i < n; i++) fields.push(readDynamicType(state));
|
|
192
|
+
return readTuple(fields);
|
|
193
|
+
}
|
|
194
|
+
// Named Tuple: a count, then (name String, type) pairs. Names shape the
|
|
195
|
+
// result object; the value wire is identical to an unnamed tuple.
|
|
196
|
+
case 0x20: {
|
|
197
|
+
const n = readUVarint(state);
|
|
198
|
+
const fields: Record<string, Reader<unknown>> = {};
|
|
199
|
+
for (let i = 0; i < n; i++) {
|
|
200
|
+
const name = readString(state);
|
|
201
|
+
fields[name] = readDynamicType(state);
|
|
202
|
+
}
|
|
203
|
+
return readTupleNamed(fields);
|
|
204
|
+
}
|
|
205
|
+
// Set (0x21): a type used inside IN-expressions, not a stored column value.
|
|
206
|
+
case 0x21:
|
|
207
|
+
throw new RangeError(
|
|
208
|
+
"RowBinary: Dynamic type 0x21 (Set) has no decodable value form",
|
|
209
|
+
);
|
|
210
|
+
// Interval (0x22): the header carries a 1-byte unit kind (0x00 Nanosecond
|
|
211
|
+
// ... 0x0a Year), then the value is a signed Int64 count of that unit. Here
|
|
212
|
+
// — unlike a standalone Interval* column — the unit IS in the wire, so we
|
|
213
|
+
// pair it with the count as an IntervalValue rather than dropping it.
|
|
214
|
+
case 0x22: {
|
|
215
|
+
const kind = readUInt8(state);
|
|
216
|
+
const unit = INTERVAL_UNITS[kind];
|
|
217
|
+
if (unit === undefined) {
|
|
218
|
+
throw new RangeError(
|
|
219
|
+
`RowBinary: unknown Interval kind ${kind} in Dynamic type encoding`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return (s): IntervalValue => [readInterval(s), unit];
|
|
223
|
+
}
|
|
224
|
+
// Nullable(T): a NULL flag byte then (if not null) the inner value. At the
|
|
225
|
+
// TOP level Dynamic erases Nullable, but NESTED inside Array/Tuple/Map the
|
|
226
|
+
// element type really is Nullable(T) — e.g. Array(Nullable(UInt8)) — so the
|
|
227
|
+
// tag does appear here.
|
|
228
|
+
case 0x23:
|
|
229
|
+
return readNullable(readDynamicType(state));
|
|
230
|
+
// Function (0x24): a higher-order function type (lambda), header-only with no
|
|
231
|
+
// stored value form.
|
|
232
|
+
case 0x24:
|
|
233
|
+
throw new RangeError(
|
|
234
|
+
"RowBinary: Dynamic type 0x24 (Function) has no decodable value form",
|
|
235
|
+
);
|
|
236
|
+
// AggregateFunction (0x25): an opaque, UNFRAMED aggregation state with a
|
|
237
|
+
// function-specific layout and no length prefix, so it cannot be decoded OR
|
|
238
|
+
// skipped generically. Finalize server-side before putting it in a Dynamic.
|
|
239
|
+
case 0x25:
|
|
240
|
+
throw new RangeError(
|
|
241
|
+
"RowBinary: Dynamic type 0x25 (AggregateFunction) is an opaque unframed state — finalize it server-side",
|
|
242
|
+
);
|
|
243
|
+
// LowCardinality(T): transparent — keep the inner type's reader as-is.
|
|
244
|
+
case 0x26:
|
|
245
|
+
return readDynamicType(state);
|
|
246
|
+
// Map(K, V): parse the key type then the value type.
|
|
247
|
+
case 0x27: {
|
|
248
|
+
const key = readDynamicType(state);
|
|
249
|
+
const value = readDynamicType(state);
|
|
250
|
+
return readMap(key, value);
|
|
251
|
+
}
|
|
252
|
+
case 0x28:
|
|
253
|
+
return readIPv4;
|
|
254
|
+
case 0x29:
|
|
255
|
+
return readIPv6;
|
|
256
|
+
// Variant (0x2a): the header is (count, then each alternative's type
|
|
257
|
+
// encoding). ClickHouse writes the alternatives ALREADY SORTED by type name,
|
|
258
|
+
// so the parsed readers line up with the discriminant directly. The value is
|
|
259
|
+
// a 1-byte discriminant (0xff = NULL) then the chosen value. NOTE: top-level
|
|
260
|
+
// Dynamic erases Variant, so this tag only appears NESTED.
|
|
261
|
+
case 0x2a: {
|
|
262
|
+
const n = readUVarint(state);
|
|
263
|
+
const alternatives: Array<Reader<unknown>> = [];
|
|
264
|
+
for (let i = 0; i < n; i++) alternatives.push(readDynamicType(state));
|
|
265
|
+
return readVariant(alternatives);
|
|
266
|
+
}
|
|
267
|
+
// Dynamic (0x2b): a Dynamic nested inside a Dynamic. The header is a single
|
|
268
|
+
// max_dynamic_types byte; the value is itself a type-encoding + value, so it
|
|
269
|
+
// is just a recursive readDynamic. We skip max_dynamic_types because it does
|
|
270
|
+
// NOT affect value decoding — it is a storage/Native-format overflow
|
|
271
|
+
// threshold; in RowBinary every value is normalized to a plain (tag, value).
|
|
272
|
+
case 0x2b:
|
|
273
|
+
readUInt8(state); // max_dynamic_types — storage threshold, not used to decode
|
|
274
|
+
return readDynamic;
|
|
275
|
+
// Custom type (0x2c): the type name is written as a String and must be
|
|
276
|
+
// re-parsed to learn the real type — we don't have a type-name parser.
|
|
277
|
+
case 0x2c:
|
|
278
|
+
throw new RangeError(
|
|
279
|
+
"RowBinary: Dynamic type 0x2c (custom type, name-encoded) is not supported — requires parsing the type name string",
|
|
280
|
+
);
|
|
281
|
+
case 0x2d:
|
|
282
|
+
return readBool;
|
|
283
|
+
// SimpleAggregateFunction (0x2e): transparent — the value is just its
|
|
284
|
+
// underlying type T. The header is (function_name String, argument types);
|
|
285
|
+
// extend here by consuming those, then returning T's reader.
|
|
286
|
+
case 0x2e:
|
|
287
|
+
throw new RangeError(
|
|
288
|
+
"RowBinary: Dynamic type 0x2e (SimpleAggregateFunction) is not supported yet — consume the header, then read the inner T",
|
|
289
|
+
);
|
|
290
|
+
// Nested(...) (0x2f): on the wire it IS Array(Tuple(...)). The header is
|
|
291
|
+
// identical to a named Tuple's (count, then (name String, type) pairs), and
|
|
292
|
+
// the value is an Array of those tuples, so compose readArray + readTupleNamed.
|
|
293
|
+
case 0x2f: {
|
|
294
|
+
const n = readUVarint(state);
|
|
295
|
+
const fields: Record<string, Reader<unknown>> = {};
|
|
296
|
+
for (let i = 0; i < n; i++) {
|
|
297
|
+
const name = readString(state);
|
|
298
|
+
fields[name] = readDynamicType(state);
|
|
299
|
+
}
|
|
300
|
+
return readArray(readTupleNamed(fields));
|
|
301
|
+
}
|
|
302
|
+
// JSON (0x30): the type-encoding header is a version byte, max_dynamic_paths
|
|
303
|
+
// (varuint), max_dynamic_types (uint8), then the typed-path / skip-path /
|
|
304
|
+
// skip-regexp lists. We consume it to reach the value body. Typed paths are
|
|
305
|
+
// serialized WITHOUT a Dynamic tag, so a schema-less reader can't decode them
|
|
306
|
+
// — bail if any are declared.
|
|
307
|
+
case 0x30: {
|
|
308
|
+
readUInt8(state); // serialization version (observed 0x00)
|
|
309
|
+
readUVarint(state); // max_dynamic_paths
|
|
310
|
+
readUInt8(state); // max_dynamic_types
|
|
311
|
+
const typedPaths = readUVarint(state);
|
|
312
|
+
if (typedPaths !== 0) {
|
|
313
|
+
throw new RangeError(
|
|
314
|
+
"RowBinary: JSON with declared typed paths is not supported — read each typed path with its known type",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const skipPaths = readUVarint(state);
|
|
318
|
+
for (let i = 0; i < skipPaths; i++) readString(state);
|
|
319
|
+
const skipRegexps = readUVarint(state);
|
|
320
|
+
for (let i = 0; i < skipRegexps; i++) readString(state);
|
|
321
|
+
return readJSON;
|
|
322
|
+
}
|
|
323
|
+
default:
|
|
324
|
+
throw new RangeError(
|
|
325
|
+
`RowBinary: unknown Dynamic type tag 0x${tag.toString(16)} (not in the binary type encoding table)`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Cursor, advance } from "./core.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read an `Enum8`: the value's underlying signed `Int8`. The name<->value map
|
|
5
|
+
* lives in the column's type, not the bytes. Two strategies, both better than one
|
|
6
|
+
* shared name-resolving reader:
|
|
7
|
+
*
|
|
8
|
+
* - Keep the number: carry the raw Int8 and map to a name only where needed —
|
|
9
|
+
* most hot loops never need it.
|
|
10
|
+
* - Or generate a per-enum reader with a baked-in constant map, so the JIT can
|
|
11
|
+
* monomorphize each enum's decode:
|
|
12
|
+
*
|
|
13
|
+
* const STATUS = { 1: "active", 2: "closed" } as const;
|
|
14
|
+
* const readStatusEnum = (s) => STATUS[readInt8(s) as keyof typeof STATUS];
|
|
15
|
+
*/
|
|
16
|
+
export function readEnum8(state: Cursor): number {
|
|
17
|
+
return state.view.getInt8(advance(state, 1));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read an `Enum16`: the value's underlying signed `Int16` (2 bytes). The
|
|
22
|
+
* name<->value map lives in the column's type definition, not the bytes. Prefer
|
|
23
|
+
* keeping the number, or a generated per-enum reader with a baked-in constant
|
|
24
|
+
* map so the JIT can optimize each enum's decode independently.
|
|
25
|
+
*/
|
|
26
|
+
export function readEnum16(state: Cursor): number {
|
|
27
|
+
return state.view.getInt16(advance(state, 2), true);
|
|
28
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readArray, readNullable, readTupleNamed } from "../composite.js";
|
|
2
|
+
import { type Reader, advance } from "../core.js";
|
|
3
|
+
import { readInt32, readUInt16, readUInt32 } from "../integers.js";
|
|
4
|
+
import { readString } from "../strings.js";
|
|
5
|
+
import { readUVarint } from "../varint.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Example: a carts table — nested generics.
|
|
9
|
+
*
|
|
10
|
+
* Columns (the trigger):
|
|
11
|
+
* cart_id UInt32
|
|
12
|
+
* items Array(Tuple(sku String, qty UInt16))
|
|
13
|
+
* discounts Array(Nullable(Int32))
|
|
14
|
+
*
|
|
15
|
+
* The element reader of an `Array` can itself be a combinator: `items` is an
|
|
16
|
+
* `Array` whose element is a named `Tuple`, `discounts` an `Array` whose element
|
|
17
|
+
* is a `Nullable`. Combinators nest to any depth, matching the column type.
|
|
18
|
+
*/
|
|
19
|
+
export type CartRow = {
|
|
20
|
+
cartId: number;
|
|
21
|
+
items: { sku: string; qty: number }[];
|
|
22
|
+
discounts: (number | null)[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const readCartRow: Reader<CartRow> = (s) => ({
|
|
26
|
+
cartId: readUInt32(s),
|
|
27
|
+
items: readArray(readTupleNamed({ sku: readString, qty: readUInt16 }))(s),
|
|
28
|
+
discounts: readArray(readNullable(readInt32))(s),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Optimized {@link readCartRow}, monomorphized through the nesting: the API
|
|
33
|
+
* version rebuilds the outer `readArray`, the inner `readTupleNamed`, and the
|
|
34
|
+
* `readNullable` closures on every row (and the tuple reader iterates a keys
|
|
35
|
+
* array per element). Here both arrays are inlined loops, the tuple element is a
|
|
36
|
+
* flat object literal, and the nullable is an inline branch — no closures, no key
|
|
37
|
+
* iteration, at either nesting level.
|
|
38
|
+
*
|
|
39
|
+
* MEASURED (Node 24 / V8, `carts.bench.ts`): ~2x faster — nested combinators
|
|
40
|
+
* (outer `readArray`, inner `readTupleNamed` / `readNullable`) rebuilt per row in
|
|
41
|
+
* the API version, all flattened here.
|
|
42
|
+
*/
|
|
43
|
+
export const readCartRowFast: Reader<CartRow> = (s) => {
|
|
44
|
+
const { buf, view } = s;
|
|
45
|
+
|
|
46
|
+
// cart_id UInt32.
|
|
47
|
+
const cartId = view.getUint32(advance(s, 4), true);
|
|
48
|
+
|
|
49
|
+
// items Array(Tuple(sku String, qty UInt16)): count, then per element a
|
|
50
|
+
// length-prefixed string and a 2-byte int.
|
|
51
|
+
const itemsN = readUVarint(s);
|
|
52
|
+
const items = new Array<{ sku: string; qty: number }>(itemsN);
|
|
53
|
+
for (let i = 0; i < itemsN; i++) {
|
|
54
|
+
const len = readUVarint(s);
|
|
55
|
+
const start = advance(s, len);
|
|
56
|
+
const sku = buf.toString("utf8", start, start + len);
|
|
57
|
+
const qty = view.getUint16(advance(s, 2), true);
|
|
58
|
+
items[i] = { sku, qty };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// discounts Array(Nullable(Int32)): count, then per element a null-flag byte
|
|
62
|
+
// and, if non-null, a 4-byte int.
|
|
63
|
+
const discN = readUVarint(s);
|
|
64
|
+
const discounts = new Array<number | null>(discN);
|
|
65
|
+
for (let i = 0; i < discN; i++) {
|
|
66
|
+
discounts[i] =
|
|
67
|
+
buf[advance(s, 1)]! !== 0 ? null : view.getInt32(advance(s, 4), true);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { cartId, items, discounts };
|
|
71
|
+
};
|