@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.
Files changed (163) hide show
  1. package/README.md +2 -1
  2. package/dist/client.d.ts +2 -2
  3. package/dist/client.js +11 -4
  4. package/dist/client.js.map +1 -1
  5. package/dist/common/clickhouse_types.d.ts +98 -0
  6. package/dist/common/clickhouse_types.js +30 -0
  7. package/dist/common/clickhouse_types.js.map +1 -0
  8. package/dist/common/client.d.ts +233 -0
  9. package/dist/common/client.js +414 -0
  10. package/dist/common/client.js.map +1 -0
  11. package/dist/common/config.d.ts +234 -0
  12. package/dist/common/config.js +364 -0
  13. package/dist/common/config.js.map +1 -0
  14. package/dist/common/connection.d.ts +124 -0
  15. package/dist/common/connection.js +3 -0
  16. package/dist/common/connection.js.map +1 -0
  17. package/dist/common/data_formatter/format_query_params.d.ts +11 -0
  18. package/dist/common/data_formatter/format_query_params.js +128 -0
  19. package/dist/common/data_formatter/format_query_params.js.map +1 -0
  20. package/dist/common/data_formatter/format_query_settings.d.ts +2 -0
  21. package/dist/common/data_formatter/format_query_settings.js +20 -0
  22. package/dist/common/data_formatter/format_query_settings.js.map +1 -0
  23. package/dist/common/data_formatter/formatter.d.ts +41 -0
  24. package/dist/common/data_formatter/formatter.js +78 -0
  25. package/dist/common/data_formatter/formatter.js.map +1 -0
  26. package/dist/common/data_formatter/index.d.ts +3 -0
  27. package/dist/common/data_formatter/index.js +24 -0
  28. package/dist/common/data_formatter/index.js.map +1 -0
  29. package/dist/common/error/error.d.ts +20 -0
  30. package/dist/common/error/error.js +73 -0
  31. package/dist/common/error/error.js.map +1 -0
  32. package/dist/common/error/index.d.ts +1 -0
  33. package/dist/common/error/index.js +18 -0
  34. package/dist/common/error/index.js.map +1 -0
  35. package/dist/common/index.d.ts +67 -0
  36. package/dist/common/index.js +97 -0
  37. package/dist/common/index.js.map +1 -0
  38. package/dist/common/logger.d.ts +80 -0
  39. package/dist/common/logger.js +154 -0
  40. package/dist/common/logger.js.map +1 -0
  41. package/dist/common/parse/column_types.d.ts +155 -0
  42. package/dist/common/parse/column_types.js +594 -0
  43. package/dist/common/parse/column_types.js.map +1 -0
  44. package/dist/common/parse/index.d.ts +2 -0
  45. package/dist/common/parse/index.js +19 -0
  46. package/dist/common/parse/index.js.map +1 -0
  47. package/dist/common/parse/json_handling.d.ts +19 -0
  48. package/dist/common/parse/json_handling.js +8 -0
  49. package/dist/common/parse/json_handling.js.map +1 -0
  50. package/dist/common/result.d.ts +90 -0
  51. package/dist/common/result.js +3 -0
  52. package/dist/common/result.js.map +1 -0
  53. package/dist/common/settings.d.ts +2007 -0
  54. package/dist/common/settings.js +19 -0
  55. package/dist/common/settings.js.map +1 -0
  56. package/dist/common/tracing.d.ts +146 -0
  57. package/dist/common/tracing.js +76 -0
  58. package/dist/common/tracing.js.map +1 -0
  59. package/dist/common/ts_utils.d.ts +4 -0
  60. package/dist/common/ts_utils.js +3 -0
  61. package/dist/common/ts_utils.js.map +1 -0
  62. package/dist/common/utils/connection.d.ts +21 -0
  63. package/dist/common/utils/connection.js +43 -0
  64. package/dist/common/utils/connection.js.map +1 -0
  65. package/dist/common/utils/index.d.ts +5 -0
  66. package/dist/common/utils/index.js +22 -0
  67. package/dist/common/utils/index.js.map +1 -0
  68. package/dist/common/utils/multipart.d.ts +34 -0
  69. package/dist/common/utils/multipart.js +81 -0
  70. package/dist/common/utils/multipart.js.map +1 -0
  71. package/dist/common/utils/sleep.d.ts +4 -0
  72. package/dist/common/utils/sleep.js +12 -0
  73. package/dist/common/utils/sleep.js.map +1 -0
  74. package/dist/common/utils/stream.d.ts +15 -0
  75. package/dist/common/utils/stream.js +50 -0
  76. package/dist/common/utils/stream.js.map +1 -0
  77. package/dist/common/utils/url.d.ts +20 -0
  78. package/dist/common/utils/url.js +67 -0
  79. package/dist/common/utils/url.js.map +1 -0
  80. package/dist/common/version.d.ts +2 -0
  81. package/dist/common/version.js +4 -0
  82. package/dist/common/version.js.map +1 -0
  83. package/dist/config.d.ts +22 -2
  84. package/dist/config.js +2 -2
  85. package/dist/config.js.map +1 -1
  86. package/dist/connection/compression.d.ts +2 -2
  87. package/dist/connection/compression.js +4 -4
  88. package/dist/connection/compression.js.map +1 -1
  89. package/dist/connection/create_connection.d.ts +1 -1
  90. package/dist/connection/node_base_connection.d.ts +3 -3
  91. package/dist/connection/node_base_connection.js +22 -22
  92. package/dist/connection/node_base_connection.js.map +1 -1
  93. package/dist/connection/node_custom_agent_connection.js +2 -2
  94. package/dist/connection/node_custom_agent_connection.js.map +1 -1
  95. package/dist/connection/node_http_connection.js +2 -2
  96. package/dist/connection/node_http_connection.js.map +1 -1
  97. package/dist/connection/node_https_connection.d.ts +1 -1
  98. package/dist/connection/node_https_connection.js +3 -3
  99. package/dist/connection/node_https_connection.js.map +1 -1
  100. package/dist/connection/socket_pool.d.ts +1 -1
  101. package/dist/connection/socket_pool.js +30 -30
  102. package/dist/connection/socket_pool.js.map +1 -1
  103. package/dist/connection/stream.d.ts +1 -1
  104. package/dist/connection/stream.js +9 -9
  105. package/dist/connection/stream.js.map +1 -1
  106. package/dist/index.d.ts +9 -7
  107. package/dist/index.js +26 -24
  108. package/dist/index.js.map +1 -1
  109. package/dist/result_set.d.ts +1 -1
  110. package/dist/result_set.js +10 -10
  111. package/dist/result_set.js.map +1 -1
  112. package/dist/utils/encoder.d.ts +1 -1
  113. package/dist/utils/encoder.js +5 -5
  114. package/dist/utils/encoder.js.map +1 -1
  115. package/dist/version.d.ts +1 -1
  116. package/dist/version.js +1 -1
  117. package/dist/version.js.map +1 -1
  118. package/package.json +7 -5
  119. package/skills/clickhouse-js-node-rowbinary-parser/EXAMPLES.md +48 -0
  120. package/skills/clickhouse-js-node-rowbinary-parser/README.md +255 -0
  121. package/skills/clickhouse-js-node-rowbinary-parser/SKILL.md +206 -0
  122. package/skills/clickhouse-js-node-rowbinary-parser/case-studies/iot-rowbinary-vs-json.md +83 -0
  123. package/skills/clickhouse-js-node-rowbinary-parser/case-studies/ledger-rowbinary-vs-json.md +103 -0
  124. package/skills/clickhouse-js-node-rowbinary-parser/case-studies/logs-json-wins.md +86 -0
  125. package/skills/clickhouse-js-node-rowbinary-parser/case-studies/wasm-vs-js.md +172 -0
  126. package/skills/clickhouse-js-node-rowbinary-parser/src/aggregateFunction.ts +34 -0
  127. package/skills/clickhouse-js-node-rowbinary-parser/src/bool.ts +10 -0
  128. package/skills/clickhouse-js-node-rowbinary-parser/src/columnar.ts +125 -0
  129. package/skills/clickhouse-js-node-rowbinary-parser/src/compile.ts +318 -0
  130. package/skills/clickhouse-js-node-rowbinary-parser/src/composite.ts +181 -0
  131. package/skills/clickhouse-js-node-rowbinary-parser/src/core.ts +77 -0
  132. package/skills/clickhouse-js-node-rowbinary-parser/src/datetime.ts +113 -0
  133. package/skills/clickhouse-js-node-rowbinary-parser/src/decimals.ts +57 -0
  134. package/skills/clickhouse-js-node-rowbinary-parser/src/dynamic.ts +328 -0
  135. package/skills/clickhouse-js-node-rowbinary-parser/src/enums.ts +28 -0
  136. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/carts.ts +71 -0
  137. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/events.ts +51 -0
  138. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/iot.ts +158 -0
  139. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/ledger.ts +98 -0
  140. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/logs.ts +73 -0
  141. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/observability.ts +142 -0
  142. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/orders.ts +65 -0
  143. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/profiles.ts +60 -0
  144. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/telemetry.ts +102 -0
  145. package/skills/clickhouse-js-node-rowbinary-parser/src/floats.ts +32 -0
  146. package/skills/clickhouse-js-node-rowbinary-parser/src/geo.ts +109 -0
  147. package/skills/clickhouse-js-node-rowbinary-parser/src/header.ts +29 -0
  148. package/skills/clickhouse-js-node-rowbinary-parser/src/integers.ts +95 -0
  149. package/skills/clickhouse-js-node-rowbinary-parser/src/interval.ts +54 -0
  150. package/skills/clickhouse-js-node-rowbinary-parser/src/ip.ts +93 -0
  151. package/skills/clickhouse-js-node-rowbinary-parser/src/json.ts +33 -0
  152. package/skills/clickhouse-js-node-rowbinary-parser/src/lowCardinality.ts +18 -0
  153. package/skills/clickhouse-js-node-rowbinary-parser/src/nested.ts +23 -0
  154. package/skills/clickhouse-js-node-rowbinary-parser/src/nothing.ts +29 -0
  155. package/skills/clickhouse-js-node-rowbinary-parser/src/reader.ts +68 -0
  156. package/skills/clickhouse-js-node-rowbinary-parser/src/rowBinaryWithNamesAndTypes.ts +155 -0
  157. package/skills/clickhouse-js-node-rowbinary-parser/src/rows.ts +58 -0
  158. package/skills/clickhouse-js-node-rowbinary-parser/src/simpleAggregateFunction.ts +20 -0
  159. package/skills/clickhouse-js-node-rowbinary-parser/src/stream.ts +276 -0
  160. package/skills/clickhouse-js-node-rowbinary-parser/src/strings.ts +55 -0
  161. package/skills/clickhouse-js-node-rowbinary-parser/src/time.ts +61 -0
  162. package/skills/clickhouse-js-node-rowbinary-parser/src/uuid.ts +153 -0
  163. 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
+ };