@clickhouse/client 1.22.0 → 1.23.0-head.c8dc8d8.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 (160) hide show
  1. package/README.md +2 -1
  2. package/dist/client.d.ts +2 -2
  3. package/dist/client.js +3 -3
  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 +127 -0
  42. package/dist/common/parse/column_types.js +586 -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 +1990 -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 +2 -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 +7 -7
  107. package/dist/index.js +24 -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 +248 -0
  121. package/skills/clickhouse-js-node-rowbinary-parser/SKILL.md +190 -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/composite.ts +181 -0
  130. package/skills/clickhouse-js-node-rowbinary-parser/src/core.ts +77 -0
  131. package/skills/clickhouse-js-node-rowbinary-parser/src/datetime.ts +113 -0
  132. package/skills/clickhouse-js-node-rowbinary-parser/src/decimals.ts +57 -0
  133. package/skills/clickhouse-js-node-rowbinary-parser/src/dynamic.ts +328 -0
  134. package/skills/clickhouse-js-node-rowbinary-parser/src/enums.ts +28 -0
  135. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/carts.ts +71 -0
  136. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/events.ts +51 -0
  137. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/iot.ts +158 -0
  138. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/ledger.ts +98 -0
  139. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/logs.ts +73 -0
  140. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/observability.ts +142 -0
  141. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/orders.ts +65 -0
  142. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/profiles.ts +60 -0
  143. package/skills/clickhouse-js-node-rowbinary-parser/src/examples/telemetry.ts +102 -0
  144. package/skills/clickhouse-js-node-rowbinary-parser/src/floats.ts +32 -0
  145. package/skills/clickhouse-js-node-rowbinary-parser/src/geo.ts +109 -0
  146. package/skills/clickhouse-js-node-rowbinary-parser/src/integers.ts +95 -0
  147. package/skills/clickhouse-js-node-rowbinary-parser/src/interval.ts +54 -0
  148. package/skills/clickhouse-js-node-rowbinary-parser/src/ip.ts +93 -0
  149. package/skills/clickhouse-js-node-rowbinary-parser/src/json.ts +33 -0
  150. package/skills/clickhouse-js-node-rowbinary-parser/src/lowCardinality.ts +18 -0
  151. package/skills/clickhouse-js-node-rowbinary-parser/src/nested.ts +23 -0
  152. package/skills/clickhouse-js-node-rowbinary-parser/src/nothing.ts +29 -0
  153. package/skills/clickhouse-js-node-rowbinary-parser/src/reader.ts +51 -0
  154. package/skills/clickhouse-js-node-rowbinary-parser/src/rows.ts +58 -0
  155. package/skills/clickhouse-js-node-rowbinary-parser/src/simpleAggregateFunction.ts +20 -0
  156. package/skills/clickhouse-js-node-rowbinary-parser/src/stream.ts +276 -0
  157. package/skills/clickhouse-js-node-rowbinary-parser/src/strings.ts +55 -0
  158. package/skills/clickhouse-js-node-rowbinary-parser/src/time.ts +61 -0
  159. package/skills/clickhouse-js-node-rowbinary-parser/src/uuid.ts +153 -0
  160. package/skills/clickhouse-js-node-rowbinary-parser/src/varint.ts +70 -0
@@ -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
+ };
@@ -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
+ }