@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,51 @@
|
|
|
1
|
+
import { type Reader, advance } from "../core.js";
|
|
2
|
+
import { readDateTime } from "../datetime.js";
|
|
3
|
+
import { readUInt64 } from "../integers.js";
|
|
4
|
+
import { readString } from "../strings.js";
|
|
5
|
+
import { readUVarint } from "../varint.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Example: a plain events table — the scalar baseline.
|
|
9
|
+
*
|
|
10
|
+
* Columns (the trigger — generate this reader when a result has these types):
|
|
11
|
+
* id UInt64
|
|
12
|
+
* name String
|
|
13
|
+
* ts DateTime('UTC')
|
|
14
|
+
*
|
|
15
|
+
* A wide integer (returned as `bigint`, never a lossy `number`), an arbitrary
|
|
16
|
+
* `String`, and a `DateTime` rendered to an ISO-8601 string here for a stable,
|
|
17
|
+
* timezone-independent result (the raw reader returns a JS `Date`). Drive it over
|
|
18
|
+
* a whole result with `readRows(readEventRow)`.
|
|
19
|
+
*/
|
|
20
|
+
export type EventRow = { id: bigint; name: string; ts: string };
|
|
21
|
+
|
|
22
|
+
export const readEventRow: Reader<EventRow> = (s) => ({
|
|
23
|
+
id: readUInt64(s),
|
|
24
|
+
name: readString(s),
|
|
25
|
+
ts: readDateTime(s).toISOString(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Optimized {@link readEventRow}: the same three reads inlined into one function
|
|
30
|
+
* body — no per-field reader calls, the `String` length + slice and the
|
|
31
|
+
* `DateTime` math written out in place. All scalars, so there is little for the
|
|
32
|
+
* monomorphization to remove (the JIT already inlines the leaf readers); see
|
|
33
|
+
* `events.bench.ts`. Still goes through `advance()`, so it stays streaming-safe.
|
|
34
|
+
*
|
|
35
|
+
* MEASURED (Node 24 / V8, `events.bench.ts`): ~1.05x — essentially ON PAR, within
|
|
36
|
+
* run-to-run noise. A purely scalar row has no per-row closures to remove and V8
|
|
37
|
+
* already inlines the leaf readers, so there is no real win here: prefer the
|
|
38
|
+
* clearer API `readEventRow` unless your own profiling says otherwise. (Contrast
|
|
39
|
+
* the composite examples, where monomorphization removes per-row closures and
|
|
40
|
+
* wins 1.3x–2.7x.)
|
|
41
|
+
*/
|
|
42
|
+
export const readEventRowFast: Reader<EventRow> = (s) => {
|
|
43
|
+
const id = s.view.getBigUint64(advance(s, 8), true);
|
|
44
|
+
const len = readUVarint(s);
|
|
45
|
+
const start = advance(s, len);
|
|
46
|
+
const name = s.buf.toString("utf8", start, start + len);
|
|
47
|
+
const ts = new Date(
|
|
48
|
+
s.view.getUint32(advance(s, 4), true) * 1000,
|
|
49
|
+
).toISOString();
|
|
50
|
+
return { id, name, ts };
|
|
51
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { type Reader, advance } from "../core.js";
|
|
2
|
+
import { readDateTime64P3 } from "../datetime.js";
|
|
3
|
+
import { readFloat32, readFloat64 } from "../floats.js";
|
|
4
|
+
import { readUInt8, readUInt32 } from "../integers.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Example: a table of IoT sensor readings — the dense, fixed-width NUMERIC case
|
|
8
|
+
* that RowBinary is built for, and the headline of the RowBinary-vs-JSON
|
|
9
|
+
* comparison in `iot.bench.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Columns (the trigger — generate this reader when a result has these types):
|
|
12
|
+
* sensor_id UInt32
|
|
13
|
+
* ts DateTime64(3)
|
|
14
|
+
* temperature Float64
|
|
15
|
+
* humidity Float64
|
|
16
|
+
* pressure Float64
|
|
17
|
+
* battery Float32
|
|
18
|
+
* status UInt8
|
|
19
|
+
*
|
|
20
|
+
* Every column is fixed-width and there is not a single string or composite in
|
|
21
|
+
* the row, so the whole record is a flat 4 + 8 + 8 + 8 + 8 + 4 + 1 = 41-byte
|
|
22
|
+
* run. This is the shape where a JS RowBinary decoder beats `JSON.parse`: the
|
|
23
|
+
* wire is ~1/3 the size and each field is one `DataView` read, versus JSON's
|
|
24
|
+
* tokenize-and-number-parse over a much larger, key-repeating text.
|
|
25
|
+
*/
|
|
26
|
+
export type IotRow = {
|
|
27
|
+
sensor_id: number;
|
|
28
|
+
ts: Date;
|
|
29
|
+
temperature: number;
|
|
30
|
+
humidity: number;
|
|
31
|
+
pressure: number;
|
|
32
|
+
battery: number;
|
|
33
|
+
status: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* API-combinator reader: correct and clear, one leaf reader per column. A fine
|
|
38
|
+
* default; `readIotRowFast` is the monomorphized form `iot.bench.ts` measures.
|
|
39
|
+
*/
|
|
40
|
+
export const readIotRow: Reader<IotRow> = (s) => ({
|
|
41
|
+
sensor_id: readUInt32(s),
|
|
42
|
+
ts: readDateTime64P3(s),
|
|
43
|
+
temperature: readFloat64(s),
|
|
44
|
+
humidity: readFloat64(s),
|
|
45
|
+
pressure: readFloat64(s),
|
|
46
|
+
battery: readFloat32(s),
|
|
47
|
+
status: readUInt8(s),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Optimized {@link readIotRow}: every column is fixed-width, so the seven
|
|
52
|
+
* separate bounds checks coalesce into one `advance(s, 41)` and each field is
|
|
53
|
+
* read at a constant offset off that base — no per-field reader calls, no cursor
|
|
54
|
+
* write-back between fields. Stays streaming-safe (one `advance`), so a row that
|
|
55
|
+
* straddles a chunk boundary still rewinds and retries cleanly.
|
|
56
|
+
*
|
|
57
|
+
* sensor_id UInt32 @ o+0 getUint32
|
|
58
|
+
* ts DateTime64(3) @ o+4 getBigInt64 (ms ticks -> Date)
|
|
59
|
+
* temperature Float64 @ o+12 getFloat64
|
|
60
|
+
* humidity Float64 @ o+20 getFloat64
|
|
61
|
+
* pressure Float64 @ o+28 getFloat64
|
|
62
|
+
* battery Float32 @ o+36 getFloat32
|
|
63
|
+
* status UInt8 @ o+40 buf[o+40]
|
|
64
|
+
*/
|
|
65
|
+
export const readIotRowFast: Reader<IotRow> = (s) => {
|
|
66
|
+
const { buf, view } = s;
|
|
67
|
+
const o = advance(s, 41); // one bounds check for the whole 41-byte row
|
|
68
|
+
const sensor_id = view.getUint32(o, true);
|
|
69
|
+
// DateTime64(3): Int64 millisecond ticks; ms fits a JS number, so Number() is exact here.
|
|
70
|
+
const ts = new Date(Number(view.getBigInt64(o + 4, true)));
|
|
71
|
+
const temperature = view.getFloat64(o + 12, true);
|
|
72
|
+
const humidity = view.getFloat64(o + 20, true);
|
|
73
|
+
const pressure = view.getFloat64(o + 28, true);
|
|
74
|
+
const battery = view.getFloat32(o + 36, true);
|
|
75
|
+
const status = buf[o + 40]!;
|
|
76
|
+
return { sensor_id, ts, temperature, humidity, pressure, battery, status };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Byte width of one fixed-width IoT row: 4 + 8 + 8 + 8 + 8 + 4 + 1. */
|
|
80
|
+
export const IOT_ROW_BYTES = 41;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Columnar (struct-of-arrays) form of the IoT result: one typed array per
|
|
84
|
+
* column instead of one object per row. `ts` is kept as epoch milliseconds in a
|
|
85
|
+
* `Float64Array` (format the few you display; don't allocate 50k `Date`s).
|
|
86
|
+
*/
|
|
87
|
+
export type IotColumns = {
|
|
88
|
+
sensor_id: Uint32Array;
|
|
89
|
+
ts: Float64Array; // epoch ms
|
|
90
|
+
temperature: Float64Array;
|
|
91
|
+
humidity: Float64Array;
|
|
92
|
+
pressure: Float64Array;
|
|
93
|
+
battery: Float32Array;
|
|
94
|
+
status: Uint8Array;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decode the whole IoT result into columns (SoA) rather than row objects (AoS).
|
|
99
|
+
*
|
|
100
|
+
* MEASURED (`iot.columnar.bench.ts`): ~4x faster than `readIotRowFast` over the
|
|
101
|
+
* same buffer, and several times smaller in memory. The win is entirely from
|
|
102
|
+
* what it does NOT do — no per-row object, no `Date`, no number boxing — so the
|
|
103
|
+
* cost drops to one unboxed store per field. It is a NUMERIC win; it would not
|
|
104
|
+
* help a string column (a JS string must be allocated either way).
|
|
105
|
+
*
|
|
106
|
+
* WHOLE-BUFFER ONLY: this needs the complete response in one `Buffer`. Because
|
|
107
|
+
* every IoT column is fixed-width the row stride is known, so the exact row
|
|
108
|
+
* count is `buf.length / IOT_ROW_BYTES` — one exact allocation per column, no
|
|
109
|
+
* growth, no bounds check in the loop.
|
|
110
|
+
*
|
|
111
|
+
* Reach for this when the consumer is column-oriented (aggregate / filter /
|
|
112
|
+
* scan / plot / feed a Worker or WASM kernel via the transferable
|
|
113
|
+
* `ArrayBuffer`s). Prefer the row reader when downstream code is row-shaped.
|
|
114
|
+
*/
|
|
115
|
+
export function decodeIotColumnar(buf: Buffer): IotColumns {
|
|
116
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
117
|
+
const n = (buf.length / IOT_ROW_BYTES) | 0;
|
|
118
|
+
// Hold each column in a LOCAL so the fill loop writes through a register-held
|
|
119
|
+
// typed-array reference, not a property load off a result object every
|
|
120
|
+
// iteration; assemble the object once, on return.
|
|
121
|
+
const sensor_id = new Uint32Array(n);
|
|
122
|
+
const ts = new Float64Array(n);
|
|
123
|
+
const temperature = new Float64Array(n);
|
|
124
|
+
const humidity = new Float64Array(n);
|
|
125
|
+
const pressure = new Float64Array(n);
|
|
126
|
+
const battery = new Float32Array(n);
|
|
127
|
+
const status = new Uint8Array(n);
|
|
128
|
+
let o = 0;
|
|
129
|
+
for (let i = 0; i < n; i++) {
|
|
130
|
+
sensor_id[i] = view.getUint32(o, true); // UInt32
|
|
131
|
+
ts[i] = Number(view.getBigInt64(o + 4, true)); // DateTime64(3) ms ticks
|
|
132
|
+
temperature[i] = view.getFloat64(o + 12, true); // Float64
|
|
133
|
+
humidity[i] = view.getFloat64(o + 20, true); // Float64
|
|
134
|
+
pressure[i] = view.getFloat64(o + 28, true); // Float64
|
|
135
|
+
battery[i] = view.getFloat32(o + 36, true); // Float32
|
|
136
|
+
status[i] = buf[o + 40]!; // UInt8
|
|
137
|
+
o += IOT_ROW_BYTES;
|
|
138
|
+
}
|
|
139
|
+
return { sensor_id, ts, temperature, humidity, pressure, battery, status };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Hybrid accessor: reconstruct a single {@link IotRow} object from columns on
|
|
144
|
+
* demand (here is where `ts` becomes a `Date`). Store columnar, and pay the
|
|
145
|
+
* object/`Date` cost only for the rows a caller actually touches — best when
|
|
146
|
+
* row access is sparse.
|
|
147
|
+
*/
|
|
148
|
+
export function iotRowAt(c: IotColumns, i: number): IotRow {
|
|
149
|
+
return {
|
|
150
|
+
sensor_id: c.sensor_id[i]!,
|
|
151
|
+
ts: new Date(c.ts[i]!),
|
|
152
|
+
temperature: c.temperature[i]!,
|
|
153
|
+
humidity: c.humidity[i]!,
|
|
154
|
+
pressure: c.pressure[i]!,
|
|
155
|
+
battery: c.battery[i]!,
|
|
156
|
+
status: c.status[i]!,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type Reader, advance } from "../core.js";
|
|
2
|
+
import {
|
|
3
|
+
type DecimalValue,
|
|
4
|
+
readDecimal64,
|
|
5
|
+
readDecimal128,
|
|
6
|
+
} from "../decimals.js";
|
|
7
|
+
import { readInt64, readUInt128, readUInt256 } from "../integers.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Example: a financial ledger — the WIDE-NUMERIC case where RowBinary wins on
|
|
11
|
+
* *correctness*, not merely speed, and the headline of the wide-int/decimal
|
|
12
|
+
* comparison in `ledger.bench.ts`.
|
|
13
|
+
*
|
|
14
|
+
* Columns (the trigger — generate this reader when a result has these types):
|
|
15
|
+
* txn_id UInt128
|
|
16
|
+
* account Int64
|
|
17
|
+
* amount Decimal128(18)
|
|
18
|
+
* balance Decimal128(18)
|
|
19
|
+
* fee Decimal64(4)
|
|
20
|
+
* volume UInt256
|
|
21
|
+
*
|
|
22
|
+
* Every value here exceeds what a JS `number` (IEEE-754 double, 53-bit mantissa)
|
|
23
|
+
* can hold exactly, and ClickHouse emits them as **bare JSON numbers**. So
|
|
24
|
+
* `JSON.parse` silently rounds every field — the only correct JSON path is to
|
|
25
|
+
* quote the values server-side and re-parse each string into a `bigint` /
|
|
26
|
+
* decimal pair by hand. RowBinary reads each as an exact `bigint` straight off
|
|
27
|
+
* the wire. The whole row is fixed-width (16+8+16+16+8+32 = 96 bytes).
|
|
28
|
+
*/
|
|
29
|
+
export type LedgerRow = {
|
|
30
|
+
txn_id: bigint;
|
|
31
|
+
account: bigint;
|
|
32
|
+
amount: DecimalValue;
|
|
33
|
+
balance: DecimalValue;
|
|
34
|
+
fee: DecimalValue;
|
|
35
|
+
volume: bigint;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* API-combinator reader: correct and clear, one leaf reader per column. A fine
|
|
40
|
+
* default; `readLedgerRowFast` is the monomorphized form `ledger.bench.ts`
|
|
41
|
+
* measures. Both return identical values.
|
|
42
|
+
*/
|
|
43
|
+
export const readLedgerRow: Reader<LedgerRow> = (s) => ({
|
|
44
|
+
txn_id: readUInt128(s),
|
|
45
|
+
account: readInt64(s),
|
|
46
|
+
amount: readDecimal128(18)(s),
|
|
47
|
+
balance: readDecimal128(18)(s),
|
|
48
|
+
fee: readDecimal64(4)(s),
|
|
49
|
+
volume: readUInt256(s),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optimized, monomorphized reader: the six column bounds checks coalesce into
|
|
54
|
+
* one `advance(s, 96)`; each wide value is composed from 64-bit words read at
|
|
55
|
+
* constant offsets off that base, with the high word read **signed** for the
|
|
56
|
+
* signed types (`Int64`, the `Decimal128` unscaled value, no high word needed
|
|
57
|
+
* for the unsigned `UInt128`/`UInt256`). Stays streaming-safe (one `advance`).
|
|
58
|
+
*
|
|
59
|
+
* txn_id UInt128 @ o+0 lo + (hi<<64) unsigned
|
|
60
|
+
* account Int64 @ o+16 getBigInt64
|
|
61
|
+
* amount Decimal128(18) @ o+24 lo + (hiSigned<<64) -> [v, 18]
|
|
62
|
+
* balance Decimal128(18) @ o+40 lo + (hiSigned<<64) -> [v, 18]
|
|
63
|
+
* fee Decimal64(4) @ o+56 getBigInt64 -> [v, 4]
|
|
64
|
+
* volume UInt256 @ o+64 w0 + w1<<64 + w2<<128 + w3<<192 unsigned
|
|
65
|
+
*/
|
|
66
|
+
export const readLedgerRowFast: Reader<LedgerRow> = (s) => {
|
|
67
|
+
const { view } = s;
|
|
68
|
+
const o = advance(s, 96); // one bounds check for the whole 96-byte row
|
|
69
|
+
|
|
70
|
+
// UInt128 — unsigned, both words unsigned.
|
|
71
|
+
const txn_id =
|
|
72
|
+
view.getBigUint64(o, true) + (view.getBigUint64(o + 8, true) << 64n);
|
|
73
|
+
|
|
74
|
+
// Int64 — signed.
|
|
75
|
+
const account = view.getBigInt64(o + 16, true);
|
|
76
|
+
|
|
77
|
+
// Decimal128(18) — Int128 unscaled (low word unsigned, high word signed), scale 18.
|
|
78
|
+
const amount: DecimalValue = [
|
|
79
|
+
view.getBigUint64(o + 24, true) + (view.getBigInt64(o + 32, true) << 64n),
|
|
80
|
+
18,
|
|
81
|
+
];
|
|
82
|
+
const balance: DecimalValue = [
|
|
83
|
+
view.getBigUint64(o + 40, true) + (view.getBigInt64(o + 48, true) << 64n),
|
|
84
|
+
18,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// Decimal64(4) — Int64 unscaled, scale 4.
|
|
88
|
+
const fee: DecimalValue = [view.getBigInt64(o + 56, true), 4];
|
|
89
|
+
|
|
90
|
+
// UInt256 — unsigned, four unsigned words.
|
|
91
|
+
const volume =
|
|
92
|
+
view.getBigUint64(o + 64, true) +
|
|
93
|
+
(view.getBigUint64(o + 72, true) << 64n) +
|
|
94
|
+
(view.getBigUint64(o + 80, true) << 128n) +
|
|
95
|
+
(view.getBigUint64(o + 88, true) << 192n);
|
|
96
|
+
|
|
97
|
+
return { txn_id, account, amount, balance, fee, volume };
|
|
98
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type Reader, advance } from "../core.js";
|
|
2
|
+
import { readDateTime } from "../datetime.js";
|
|
3
|
+
import { readString } from "../strings.js";
|
|
4
|
+
import { readUVarint } from "../varint.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Example: an application log table — the STRING-HEAVY case where the skill
|
|
8
|
+
* steers you AWAY from RowBinary and toward a `JSON*` format. The honest
|
|
9
|
+
* counter-case to the IoT and ledger studies; see `logs.bench.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Columns (the trigger — generate this reader when a result has these types):
|
|
12
|
+
* ts DateTime
|
|
13
|
+
* level LowCardinality(String)
|
|
14
|
+
* service LowCardinality(String)
|
|
15
|
+
* message String
|
|
16
|
+
* trace_id String
|
|
17
|
+
*
|
|
18
|
+
* Four of the five columns are text consumed wholesale, and `LowCardinality(T)`
|
|
19
|
+
* is transparent in RowBinary (decodes as the inner `String`, no dictionary on
|
|
20
|
+
* the wire). A RowBinary string read is a varint length + `buf.toString("utf8",
|
|
21
|
+
* …)` per field in JS; V8's native `JSON.parse` builds the same JS strings in
|
|
22
|
+
* optimized C++ and tends to WIN here. This reader exists so the comparison is
|
|
23
|
+
* apples-to-apples — not because RowBinary is the right call for this shape.
|
|
24
|
+
*/
|
|
25
|
+
export type LogRow = {
|
|
26
|
+
ts: Date;
|
|
27
|
+
level: string;
|
|
28
|
+
service: string;
|
|
29
|
+
message: string;
|
|
30
|
+
trace_id: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* API-combinator reader: one leaf reader per column, the clear default. There is
|
|
35
|
+
* little to monomorphize on a mostly-string row — the only fixed-width field is
|
|
36
|
+
* `ts` — so `readLogRowFast` below is barely different and barely faster; the
|
|
37
|
+
* real lesson (see `logs.bench.ts`) is that JSON beats both.
|
|
38
|
+
*/
|
|
39
|
+
export const readLogRow: Reader<LogRow> = (s) => ({
|
|
40
|
+
ts: readDateTime(s),
|
|
41
|
+
level: readString(s), // LowCardinality(String) — transparent, decode as String
|
|
42
|
+
service: readString(s), // LowCardinality(String) — transparent, decode as String
|
|
43
|
+
message: readString(s),
|
|
44
|
+
trace_id: readString(s),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Optimized {@link readLogRow}: the four string reads inlined (varint length +
|
|
49
|
+
* `buf.toString` in place) and the `DateTime` read written out. Note how little
|
|
50
|
+
* monomorphization can do when the row is dominated by variable-length strings —
|
|
51
|
+
* there are no adjacent fixed-width columns to coalesce, so this stays close to
|
|
52
|
+
* the API version. Included to make `logs.bench.ts` a fair fight; the takeaway
|
|
53
|
+
* is to pick `JSONEachRow` for this shape, not to tune this reader.
|
|
54
|
+
*/
|
|
55
|
+
export const readLogRowFast: Reader<LogRow> = (s) => {
|
|
56
|
+
const { buf } = s;
|
|
57
|
+
// DateTime: 4-byte LE Unix seconds.
|
|
58
|
+
const ts = new Date(s.view.getUint32(advance(s, 4), true) * 1000);
|
|
59
|
+
// Four UTF-8 Strings (the two LowCardinality columns are plain String on the wire).
|
|
60
|
+
let len = readUVarint(s);
|
|
61
|
+
let o = advance(s, len);
|
|
62
|
+
const level = buf.toString("utf8", o, o + len);
|
|
63
|
+
len = readUVarint(s);
|
|
64
|
+
o = advance(s, len);
|
|
65
|
+
const service = buf.toString("utf8", o, o + len);
|
|
66
|
+
len = readUVarint(s);
|
|
67
|
+
o = advance(s, len);
|
|
68
|
+
const message = buf.toString("utf8", o, o + len);
|
|
69
|
+
len = readUVarint(s);
|
|
70
|
+
o = advance(s, len);
|
|
71
|
+
const trace_id = buf.toString("utf8", o, o + len);
|
|
72
|
+
return { ts, level, service, message, trace_id };
|
|
73
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readArray,
|
|
3
|
+
readMap,
|
|
4
|
+
readNullable,
|
|
5
|
+
readTupleNamed,
|
|
6
|
+
readVariant,
|
|
7
|
+
} from "../composite.js";
|
|
8
|
+
import { type Reader, advance } from "../core.js";
|
|
9
|
+
import { readDateTime64P3 } from "../datetime.js";
|
|
10
|
+
import { readEnum8 } from "../enums.js";
|
|
11
|
+
import { readFloat64 } from "../floats.js";
|
|
12
|
+
import { readInt64, readUInt64 } from "../integers.js";
|
|
13
|
+
import { readString } from "../strings.js";
|
|
14
|
+
import { formatUUID, formatUUIDTable, readUUID } from "../uuid.js";
|
|
15
|
+
import { readUVarint } from "../varint.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Example: an observability/events table — the gotcha-heavy one. It packs the
|
|
19
|
+
* traps that trip a from-scratch decoder; the skill's job is getting them right.
|
|
20
|
+
*
|
|
21
|
+
* Columns (the trigger):
|
|
22
|
+
* id UInt64
|
|
23
|
+
* ts DateTime64(3, 'UTC')
|
|
24
|
+
* level Enum8('debug'=1, 'info'=2, 'warn'=3, 'error'=4)
|
|
25
|
+
* trace_id UUID
|
|
26
|
+
* payload Variant(String, Int64, Float64)
|
|
27
|
+
* tags Map(LowCardinality(String), String)
|
|
28
|
+
* metrics Array(Tuple(name LowCardinality(String), value Float64))
|
|
29
|
+
* attrs Array(Nullable(Int64))
|
|
30
|
+
*
|
|
31
|
+
* Gotchas exercised, all in one row:
|
|
32
|
+
* - `Variant(String, Int64, Float64)`: the discriminant indexes the alternatives
|
|
33
|
+
* SORTED BY TYPE NAME — ["Float64", "Int64", "String"] → 0=Float64, 1=Int64,
|
|
34
|
+
* 2=String (NOT declaration order); `0xFF` = NULL. `readVariant` takes the
|
|
35
|
+
* readers in that sorted order.
|
|
36
|
+
* - `DateTime64(3)`: 8-byte Int64 of millisecond ticks; P=3 is exactly a `Date`'s
|
|
37
|
+
* resolution, so `readDateTime64P3` returns a plain `Date` (here ISO-stringed).
|
|
38
|
+
* - `LowCardinality(String)` (in the Map key and the Tuple field) is TRANSPARENT
|
|
39
|
+
* in RowBinary — decode as plain `String`, no dictionary layer.
|
|
40
|
+
* - `UUID` is two little-endian `UInt64` halves, byte-reversed vs the text form.
|
|
41
|
+
* - `Array(Nullable(Int64))`: per element a null flag then (if present) an Int64,
|
|
42
|
+
* kept as `bigint`.
|
|
43
|
+
*/
|
|
44
|
+
export type ObsRow = {
|
|
45
|
+
id: bigint;
|
|
46
|
+
ts: string;
|
|
47
|
+
level: number;
|
|
48
|
+
traceId: string;
|
|
49
|
+
payload: number | bigint | string | null;
|
|
50
|
+
tags: Map<string, string>;
|
|
51
|
+
metrics: { name: string; value: number }[];
|
|
52
|
+
attrs: (bigint | null)[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* API-combinator reader. Note the `Variant` readers are in sorted-type-name
|
|
57
|
+
* order (Float64, Int64, String), and `LowCardinality` columns just use the
|
|
58
|
+
* inner `String` reader.
|
|
59
|
+
*/
|
|
60
|
+
export const readObsRow: Reader<ObsRow> = (s) => ({
|
|
61
|
+
id: readUInt64(s),
|
|
62
|
+
ts: readDateTime64P3(s).toISOString(),
|
|
63
|
+
level: readEnum8(s),
|
|
64
|
+
traceId: formatUUID(readUUID(s)),
|
|
65
|
+
payload: readVariant([readFloat64, readInt64, readString])(s),
|
|
66
|
+
tags: readMap(readString, readString)(s),
|
|
67
|
+
metrics: readArray(readTupleNamed({ name: readString, value: readFloat64 }))(
|
|
68
|
+
s,
|
|
69
|
+
),
|
|
70
|
+
attrs: readArray(readNullable(readInt64))(s),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Optimized {@link readObsRow}, flattened per the SKILL.md guidance:
|
|
75
|
+
* - `buf`/`view` hoisted to locals.
|
|
76
|
+
* - the leading run of FIXED-WIDTH columns — `id` UInt64 (8) + `ts` DateTime64 (8)
|
|
77
|
+
* + `level` Enum8 (1) + `trace_id` UUID (16) = 33 bytes — is bounds-checked ONCE
|
|
78
|
+
* (`advance(s, 33)`) and read at constant offsets, instead of four `advance`s.
|
|
79
|
+
* - the `Variant` is an inlined `switch` over the discriminant (sorted order).
|
|
80
|
+
* - leaf reads inlined, `formatUUIDTable` for the UUID, pre-sized arrays.
|
|
81
|
+
* The variable-width columns (`payload`/`tags`/`metrics`/`attrs`) each start a new
|
|
82
|
+
* `advance` run because their size isn't known until decoded.
|
|
83
|
+
*/
|
|
84
|
+
export const readObsRowFast: Reader<ObsRow> = (s) => {
|
|
85
|
+
const { buf, view } = s;
|
|
86
|
+
|
|
87
|
+
// One bounds check for the 33-byte fixed-width head.
|
|
88
|
+
const o = advance(s, 33);
|
|
89
|
+
const id = view.getBigUint64(o, true);
|
|
90
|
+
const ts = new Date(Number(view.getBigInt64(o + 8, true))).toISOString();
|
|
91
|
+
const level = view.getInt8(o + 16);
|
|
92
|
+
const traceId = formatUUIDTable(buf.subarray(o + 17, o + 33));
|
|
93
|
+
|
|
94
|
+
// payload Variant(String, Int64, Float64): 1-byte discriminant (sorted names:
|
|
95
|
+
// 0=Float64, 1=Int64, 2=String), 0xFF = NULL.
|
|
96
|
+
let payload: number | bigint | string | null;
|
|
97
|
+
const disc = buf[advance(s, 1)]!;
|
|
98
|
+
if (disc === 0xff) {
|
|
99
|
+
payload = null;
|
|
100
|
+
} else if (disc === 0) {
|
|
101
|
+
payload = view.getFloat64(advance(s, 8), true);
|
|
102
|
+
} else if (disc === 1) {
|
|
103
|
+
payload = view.getBigInt64(advance(s, 8), true);
|
|
104
|
+
} else {
|
|
105
|
+
const len = readUVarint(s);
|
|
106
|
+
const st = advance(s, len);
|
|
107
|
+
payload = buf.toString("utf8", st, st + len);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// tags Map(LowCardinality(String) -> String): count, then key/value strings.
|
|
111
|
+
const tagN = readUVarint(s);
|
|
112
|
+
const tags = new Map<string, string>();
|
|
113
|
+
for (let i = 0; i < tagN; i++) {
|
|
114
|
+
let len = readUVarint(s);
|
|
115
|
+
let st = advance(s, len);
|
|
116
|
+
const k = buf.toString("utf8", st, st + len);
|
|
117
|
+
len = readUVarint(s);
|
|
118
|
+
st = advance(s, len);
|
|
119
|
+
tags.set(k, buf.toString("utf8", st, st + len));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// metrics Array(Tuple(name LowCardinality(String), value Float64)).
|
|
123
|
+
const mN = readUVarint(s);
|
|
124
|
+
const metrics = new Array<{ name: string; value: number }>(mN);
|
|
125
|
+
for (let i = 0; i < mN; i++) {
|
|
126
|
+
const len = readUVarint(s);
|
|
127
|
+
const st = advance(s, len);
|
|
128
|
+
const name = buf.toString("utf8", st, st + len);
|
|
129
|
+
const value = view.getFloat64(advance(s, 8), true);
|
|
130
|
+
metrics[i] = { name, value };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// attrs Array(Nullable(Int64)).
|
|
134
|
+
const aN = readUVarint(s);
|
|
135
|
+
const attrs = new Array<bigint | null>(aN);
|
|
136
|
+
for (let i = 0; i < aN; i++) {
|
|
137
|
+
attrs[i] =
|
|
138
|
+
buf[advance(s, 1)]! !== 0 ? null : view.getBigInt64(advance(s, 8), true);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { id, ts, level, traceId, payload, tags, metrics, attrs };
|
|
142
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type Reader, advance } from "../core.js";
|
|
2
|
+
import { type DecimalValue, readDecimal64 } from "../decimals.js";
|
|
3
|
+
import { readEnum8 } from "../enums.js";
|
|
4
|
+
import { readUInt8 } from "../integers.js";
|
|
5
|
+
import { formatUUID, formatUUIDTable, readUUID } from "../uuid.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Example: an orders table — UUID, Decimal, and Enum (awkward-as-JSON types).
|
|
9
|
+
*
|
|
10
|
+
* Columns (the trigger):
|
|
11
|
+
* id UInt8
|
|
12
|
+
* uid UUID
|
|
13
|
+
* price Decimal64(2)
|
|
14
|
+
* status Enum8('new' = 1, 'shipped' = 2, 'done' = 3)
|
|
15
|
+
*
|
|
16
|
+
* Shows the parse/format split and faithful values: `uid` is read as raw bytes
|
|
17
|
+
* then formatted with `formatUUID`; `price` stays the exact `[unscaled, scale]`
|
|
18
|
+
* pair (`[1234n, 2]` == 12.34), not a lossy float; `status` decodes to the
|
|
19
|
+
* underlying `Int8` value (1/2/3), the name<->value map being type metadata, not
|
|
20
|
+
* on the wire. The declared scale `2` is baked into `readDecimal64(2)`.
|
|
21
|
+
*/
|
|
22
|
+
export type OrderRow = {
|
|
23
|
+
id: number;
|
|
24
|
+
uid: string;
|
|
25
|
+
price: DecimalValue;
|
|
26
|
+
status: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const readOrderRow: Reader<OrderRow> = (s) => ({
|
|
30
|
+
id: readUInt8(s),
|
|
31
|
+
uid: formatUUID(readUUID(s)),
|
|
32
|
+
price: readDecimal64(2)(s),
|
|
33
|
+
status: readEnum8(s),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Optimized {@link readOrderRow}, flattened per the SKILL.md guidance:
|
|
38
|
+
* - `buf`/`view` hoisted to locals (one property load, not one per field).
|
|
39
|
+
* - every column here is FIXED-WIDTH, so the whole row — `id` UInt8 (1) + `uid`
|
|
40
|
+
* UUID (16) + `price` Decimal64 (8) + `status` Enum8 (1) = 26 bytes — is
|
|
41
|
+
* bounds-checked ONCE (`advance(s, 26)`) and read at constant offsets, instead
|
|
42
|
+
* of four separate `advance`s. (This is the exact worked example in SKILL.md.)
|
|
43
|
+
* - the four leaf reads are inlined, and the BigInt `formatUUID` is swapped for
|
|
44
|
+
* the lookup-table `formatUUIDTable` (~1.6x on its own; see `readUUID.bench.ts`).
|
|
45
|
+
* Since this example formats every UUID to a string, that swap is the dominant
|
|
46
|
+
* win — the `readDecimal64(2)` closure (rebuilt per row above) is inlined too.
|
|
47
|
+
*
|
|
48
|
+
* MEASURED (Node 24 / V8, `orders.bench.ts`): ~2.6x faster — the largest win of
|
|
49
|
+
* the examples, dominated by the `formatUUIDTable` swap (every row stringifies a
|
|
50
|
+
* UUID; the table formatter is ~1.7x on its own and the row is otherwise cheap).
|
|
51
|
+
*
|
|
52
|
+
* `formatUUIDTable` uses a shared scratch buffer, so it is non-reentrant — fine
|
|
53
|
+
* here because the bytes are copied into the returned string synchronously before
|
|
54
|
+
* the next call.
|
|
55
|
+
*/
|
|
56
|
+
export const readOrderRowFast: Reader<OrderRow> = (s) => {
|
|
57
|
+
const { buf, view } = s;
|
|
58
|
+
// One bounds check for the whole 26-byte fixed-width row.
|
|
59
|
+
const o = advance(s, 26);
|
|
60
|
+
const id = buf[o]!;
|
|
61
|
+
const uid = formatUUIDTable(buf.subarray(o + 1, o + 17));
|
|
62
|
+
const price: DecimalValue = [view.getBigInt64(o + 17, true), 2];
|
|
63
|
+
const status = view.getInt8(o + 25);
|
|
64
|
+
return { id, uid, price, status };
|
|
65
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readArray, readNullable } from "../composite.js";
|
|
2
|
+
import { type Reader, advance } from "../core.js";
|
|
3
|
+
import { readInt32, readUInt32 } from "../integers.js";
|
|
4
|
+
import { readString } from "../strings.js";
|
|
5
|
+
import { readUVarint } from "../varint.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Example: a profiles table — Array and Nullable wrappers.
|
|
9
|
+
*
|
|
10
|
+
* Columns (the trigger):
|
|
11
|
+
* id UInt32
|
|
12
|
+
* tags Array(String)
|
|
13
|
+
* score Nullable(Int32)
|
|
14
|
+
*
|
|
15
|
+
* `readArray(elem)` reads a LEB128 length then that many elements; `readNullable`
|
|
16
|
+
* reads a 1-byte present/NULL flag then the value. Both are combinators: pass the
|
|
17
|
+
* inner reader and they return a `Reader`. Empty array and NULL are the sharp
|
|
18
|
+
* cases (a single byte each).
|
|
19
|
+
*/
|
|
20
|
+
export type ProfileRow = { id: number; tags: string[]; score: number | null };
|
|
21
|
+
|
|
22
|
+
export const readProfileRow: Reader<ProfileRow> = (s) => ({
|
|
23
|
+
id: readUInt32(s),
|
|
24
|
+
tags: readArray(readString)(s),
|
|
25
|
+
score: readNullable(readInt32)(s),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Optimized {@link readProfileRow}, monomorphized: `readArray(readString)` and
|
|
30
|
+
* `readNullable(readInt32)` each allocate a fresh combinator closure on EVERY
|
|
31
|
+
* row in the version above; here the array loop and the null-flag branch are
|
|
32
|
+
* inlined, so no per-row closures are created and the element/inner reads are
|
|
33
|
+
* straight-line. This is the kind of win the SKILL's "monomorphize" step targets;
|
|
34
|
+
* see `profiles.bench.ts`.
|
|
35
|
+
*
|
|
36
|
+
* MEASURED (Node 24 / V8, `profiles.bench.ts`): ~1.3x faster — removing the two
|
|
37
|
+
* per-row combinator closures (`readArray(readString)`, `readNullable(readInt32)`)
|
|
38
|
+
* is the win.
|
|
39
|
+
*/
|
|
40
|
+
export const readProfileRowFast: Reader<ProfileRow> = (s) => {
|
|
41
|
+
const { buf, view } = s;
|
|
42
|
+
|
|
43
|
+
// id UInt32.
|
|
44
|
+
const id = view.getUint32(advance(s, 4), true);
|
|
45
|
+
|
|
46
|
+
// tags Array(String): count, then each a length-prefixed UTF-8 string.
|
|
47
|
+
const n = readUVarint(s);
|
|
48
|
+
const tags = new Array<string>(n);
|
|
49
|
+
for (let i = 0; i < n; i++) {
|
|
50
|
+
const len = readUVarint(s);
|
|
51
|
+
const start = advance(s, len);
|
|
52
|
+
tags[i] = buf.toString("utf8", start, start + len);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// score Nullable(Int32): null-flag byte, then if non-null a 4-byte int.
|
|
56
|
+
const score =
|
|
57
|
+
buf[advance(s, 1)]! !== 0 ? null : view.getInt32(advance(s, 4), true);
|
|
58
|
+
|
|
59
|
+
return { id, tags, score };
|
|
60
|
+
};
|