@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,276 @@
1
+ import { type Reader, Cursor } from "./core.js";
2
+ import { readRows } from "./rows.js";
3
+
4
+ /** Empty buffer reused as the "no carry" sentinel between chunks. */
5
+ const EMPTY_CHUNK = Buffer.alloc(0);
6
+
7
+ /** Stats captured at the moment the small-chunk warning fires. */
8
+ export interface SmallChunkStats {
9
+ /** Chunks consumed so far. */
10
+ chunks: number;
11
+ /** Rows decoded so far. */
12
+ rows: number;
13
+ /** `rows / chunks` — the ratio that tripped the threshold. */
14
+ rowsPerChunk: number;
15
+ }
16
+
17
+ /**
18
+ * Tuning for {@link streamRowBatches}'s small-chunk warning. Pass `false` to
19
+ * disable it, `true` / omit for the defaults, or an object to tune.
20
+ */
21
+ export type WarnOnSmallChunks =
22
+ | boolean
23
+ | {
24
+ /**
25
+ * Warn when the running `rows / chunks` average drops below this. Default
26
+ * `2`: throw + restart re-decodes the partial trailing row on EVERY chunk,
27
+ * so once a chunk barely covers a row or two the re-scan dominates — the
28
+ * regime where `streamingRow.bench.ts` shows throw+restart losing to a lean
29
+ * generator. Keep it low so the warning only fires when chunks are
30
+ * genuinely too small, never on a healthy hundreds-of-rows-per-chunk stream.
31
+ */
32
+ minRowsPerChunk?: number;
33
+ /**
34
+ * Don't evaluate until this many chunks have been seen. Default `16`:
35
+ * lets the average settle and suppresses the warning on small results,
36
+ * where the gotcha doesn't bite (it only matters at megabytes / millions
37
+ * of rows). A stream that ends before this never warns.
38
+ */
39
+ warmupChunks?: number;
40
+ /** Where the warning goes. Default `console.warn`. */
41
+ warn?: (message: string, stats: SmallChunkStats) => void;
42
+ };
43
+
44
+ /** Options for {@link streamRowBatches}. */
45
+ export interface StreamRowBatchesOptions {
46
+ /**
47
+ * Diagnostic that catches a silent throughput killer: chunks so small that the
48
+ * throw+restart streaming strategy spends most of its time re-decoding the
49
+ * partial trailing row instead of making progress. Fires AT MOST ONCE per
50
+ * stream. On by default; see {@link WarnOnSmallChunks} to tune or disable.
51
+ *
52
+ * The fix it points at is usually upstream — raise the HTTP response's read
53
+ * size (Node sets the socket/stream `highWaterMark`; a fetch `Response.body`
54
+ * reader delivers larger chunks than a hand-rolled tiny read) into the
55
+ * tens–hundreds of KB range — or, when chunk size isn't yours to control,
56
+ * compose {@link coalesceChunks} in front to merge small chunks first.
57
+ */
58
+ warnOnSmallChunks?: WarnOnSmallChunks;
59
+ }
60
+
61
+ /**
62
+ * Stream a chunked `RowBinary` response into batches of decoded rows. This is
63
+ * the async front door built on {@link readRows}: feed it the byte chunks of an
64
+ * HTTP response (anything async-iterable — a Node `Readable`, `response.body`,
65
+ * etc.) and a per-row `Reader`, and `for await` the batches.
66
+ *
67
+ * One batch is yielded per incoming chunk — exactly the rows that completed
68
+ * within it — so batch size tracks chunk size, which the caller controls. A
69
+ * chunk that doesn't complete a new row yields nothing; its bytes are carried
70
+ * into the next chunk. Empty batches are never yielded.
71
+ *
72
+ * How it works (the carry-buffer driver):
73
+ * - Join the leftover `carry` from the previous chunk to the new chunk, build a
74
+ * state over the join, and run `readRows`. It decodes whole rows, stops cleanly
75
+ * on the partial trailing row (catching `NeedMoreData`), and leaves `pos` at
76
+ * that row's start.
77
+ * - The unread tail `pos..end` becomes the next `carry` as a `subarray` VIEW,
78
+ * NOT a copy. The joined buffer is owned entirely by this generator — it is
79
+ * never yielded to the caller — so there is no aliasing hazard in keeping a
80
+ * view into it, and we skip a per-chunk copy of the tail. The view is also
81
+ * short-lived: the next chunk's `Buffer.concat` copies these bytes into a
82
+ * fresh buffer, after which the old one is released.
83
+ * - When the stream ends, any non-empty carry means the response was truncated
84
+ * mid-row — a malformed stream — so it throws rather than silently dropping
85
+ * bytes.
86
+ *
87
+ * `readRow` is a `Reader<T>` — write it as `(s) => ({ id: readUInt64(s),
88
+ * name: readString(s) })`. Build any configured/combinator readers ONCE (e.g.
89
+ * `const readRow = readTupleNamed({...})`) and reuse, rather than rebuilding them
90
+ * per chunk.
91
+ *
92
+ * ZERO-COPY NOTE: raw-bytes readers (`readUUID`/`readIPv6`/`readFixedStringBytes`
93
+ * and binary `String`) return views into the current chunk's joined buffer. Those
94
+ * stay valid as long as you hold the row objects, but are NOT views into one
95
+ * stable buffer across batches. If you retain them long-term, copy in `readRow`.
96
+ *
97
+ * BACKPRESSURE: this is a pull stream — the next chunk is only requested when the
98
+ * consumer asks for the next batch, so a slow consumer naturally throttles reading.
99
+ *
100
+ * The per-chunk bookkeeping for the small-chunk warning (two integer adds and a
101
+ * compare) runs once per CHUNK, not per row, so it is off every hot path; the
102
+ * default-on warning is documented in {@link StreamRowBatchesOptions}.
103
+ */
104
+ export async function* streamRowBatches<T>(
105
+ chunks: AsyncIterable<Uint8Array>,
106
+ readRow: Reader<T>,
107
+ options?: StreamRowBatchesOptions,
108
+ ): AsyncGenerator<T[], void, undefined> {
109
+ const drive = readRows(readRow);
110
+ let carry: Buffer<ArrayBufferLike> = EMPTY_CHUNK;
111
+
112
+ // Resolve the warning config once, outside the loop.
113
+ const warnCfg = options?.warnOnSmallChunks;
114
+ const warnEnabled = warnCfg !== false;
115
+ const warnObj = typeof warnCfg === "object" ? warnCfg : undefined;
116
+ const minRowsPerChunk = warnObj?.minRowsPerChunk ?? 2;
117
+ const warmupChunks = warnObj?.warmupChunks ?? 16;
118
+ const warn = warnObj?.warn ?? ((message: string) => console.warn(message));
119
+ let chunkCount = 0;
120
+ let rowCount = 0;
121
+ let warned = false;
122
+
123
+ for await (const chunk of chunks) {
124
+ // Normalize to a Buffer without copying (a Uint8Array shares its ArrayBuffer).
125
+ const incoming = Buffer.isBuffer(chunk)
126
+ ? chunk
127
+ : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
128
+ const work =
129
+ carry.length === 0 ? incoming : Buffer.concat([carry, incoming]);
130
+
131
+ const state = new Cursor(work);
132
+ const rows = drive(state);
133
+ if (rows.length > 0) yield rows;
134
+
135
+ // Carry the unread tail (the partial trailing row, if any) to the next
136
+ // chunk. A view, not a copy: we own `work` and never expose it, so keeping a
137
+ // subarray into it is safe; the next concat copies these bytes out.
138
+ carry = state.pos < work.length ? work.subarray(state.pos) : EMPTY_CHUNK;
139
+
140
+ if (warnEnabled && !warned) {
141
+ chunkCount++;
142
+ rowCount += rows.length;
143
+ const rowsPerChunk = rowCount / chunkCount;
144
+ if (chunkCount >= warmupChunks && rowsPerChunk < minRowsPerChunk) {
145
+ warned = true;
146
+ warn(
147
+ `RowBinary stream: chunks look too small — ${rowsPerChunk.toFixed(2)} rows/chunk over ${chunkCount} chunks. ` +
148
+ `Streaming throws + restarts the partial trailing row on every chunk, so tiny chunks spend most of their ` +
149
+ `time re-decoding instead of advancing. Increase the upstream read/highWaterMark to tens–hundreds of KB, ` +
150
+ `or compose coalesceChunks() in front of this stream to merge small chunks first.`,
151
+ { chunks: chunkCount, rows: rowCount, rowsPerChunk },
152
+ );
153
+ }
154
+ }
155
+ }
156
+ if (carry.length > 0) {
157
+ throw new Error(
158
+ `RowBinary stream ended mid-row: ${carry.length} trailing byte(s) left undecoded`,
159
+ );
160
+ }
161
+ }
162
+
163
+ /** A timeout result distinct from any `IteratorResult`. */
164
+ const TIMED_OUT = Symbol("coalesceChunks.timeout");
165
+
166
+ /**
167
+ * Coalesce (debounce) a chunk stream so each emitted chunk is at least `minSize`
168
+ * bytes — a filter you compose IN FRONT of {@link streamRowBatches} when the
169
+ * source delivers chunks too small to stream efficiently and you can't enlarge
170
+ * them upstream:
171
+ *
172
+ * streamRowBatches(coalesceChunks(httpChunks, { minSize: 64 * 1024, timeoutMs: 50 }), readRow)
173
+ *
174
+ * WHY: the throw+restart streaming strategy re-decodes the partial trailing row
175
+ * on every chunk boundary, so the smaller the chunks the more time is wasted
176
+ * re-scanning (see `streamingRow.bench.ts`). Merging small chunks up front cuts
177
+ * the number of boundaries — and the backtracking with it.
178
+ *
179
+ * THE TRADE-OFF (latency vs. reallocation vs. backtracking): merging holds bytes
180
+ * back until enough accumulate, so it ADDS up to `timeoutMs` of latency to data
181
+ * that arrives in a trickle, and it COPIES via `Buffer.concat` to join the parts
182
+ * (one extra allocation per emitted chunk). In return the downstream parser
183
+ * backtracks far less. Tune `minSize` to the downstream sweet spot (tens–hundreds
184
+ * of KB) and `timeoutMs` to the latency you can spare.
185
+ *
186
+ * SEMANTICS:
187
+ * - Accumulates incoming chunks until their total reaches `minSize`, then emits
188
+ * the join immediately.
189
+ * - A batch below `minSize` is flushed early when `timeoutMs` elapses from the
190
+ * moment its FIRST byte arrived (the deadline is anchored, not reset per
191
+ * chunk — a steady trickle of tiny chunks can't defer the flush forever).
192
+ * - While nothing is buffered it blocks indefinitely for the next chunk: an idle
193
+ * or finished stream is never charged the timeout.
194
+ * - End of stream flushes whatever remains (possibly below `minSize`); a single
195
+ * already-large-enough chunk passes straight through with no copy.
196
+ *
197
+ * It keeps exactly ONE outstanding pull on the source at a time (never calls
198
+ * `next()` while a prior result is still in flight), reads one chunk ahead so it
199
+ * can race arrival against the timer, and releases the source via `return()` if
200
+ * the consumer abandons it early.
201
+ */
202
+ export async function* coalesceChunks(
203
+ source: AsyncIterable<Uint8Array>,
204
+ { minSize, timeoutMs }: { minSize: number; timeoutMs: number },
205
+ ): AsyncGenerator<Buffer, void, undefined> {
206
+ const it = source[Symbol.asyncIterator]();
207
+ // The single in-flight pull. Read one ahead so we always have a promise to
208
+ // race the timer against; never start a second next() before this resolves.
209
+ let pull = it.next();
210
+ let parts: Buffer[] = [];
211
+ let buffered = 0;
212
+ let deadline = 0; // ms timestamp; armed when the first byte enters an empty batch
213
+
214
+ const asBuffer = (u8: Uint8Array): Buffer =>
215
+ Buffer.isBuffer(u8)
216
+ ? u8
217
+ : Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength);
218
+
219
+ const flush = (): Buffer => {
220
+ // One part: hand it back as-is (no concat, no copy). Many: join them.
221
+ const out = parts.length === 1 ? parts[0]! : Buffer.concat(parts, buffered);
222
+ parts = [];
223
+ buffered = 0;
224
+ return out;
225
+ };
226
+
227
+ const take = (u8: Uint8Array): void => {
228
+ const b = asBuffer(u8);
229
+ parts.push(b);
230
+ buffered += b.length;
231
+ };
232
+
233
+ try {
234
+ while (true) {
235
+ if (buffered === 0) {
236
+ // Nothing buffered: block for the next chunk with no timeout.
237
+ const r = await pull;
238
+ if (r.done) return;
239
+ take(r.value);
240
+ deadline = Date.now() + timeoutMs;
241
+ pull = it.next();
242
+ if (buffered >= minSize) yield flush();
243
+ continue;
244
+ }
245
+
246
+ // Below minSize with bytes in hand: race the next chunk against the time
247
+ // left on this batch's anchored deadline.
248
+ const remaining = deadline - Date.now();
249
+ if (remaining <= 0) {
250
+ yield flush();
251
+ continue;
252
+ }
253
+ let timer: ReturnType<typeof setTimeout> | undefined;
254
+ const timeout = new Promise<typeof TIMED_OUT>((resolve) => {
255
+ timer = setTimeout(() => resolve(TIMED_OUT), remaining);
256
+ });
257
+ const r = await Promise.race([pull, timeout]);
258
+ clearTimeout(timer); // no-op if it already fired; frees the loop otherwise
259
+ if (r === TIMED_OUT) {
260
+ // pull is STILL outstanding — keep it; just flush what we have so far.
261
+ yield flush();
262
+ continue;
263
+ }
264
+ if (r.done) {
265
+ yield flush(); // emit the tail; stream is over
266
+ return;
267
+ }
268
+ take(r.value);
269
+ pull = it.next();
270
+ if (buffered >= minSize) yield flush();
271
+ }
272
+ } finally {
273
+ // Consumer broke out early (break/throw): let the source clean up.
274
+ if (typeof it.return === "function") await it.return();
275
+ }
276
+ }
@@ -0,0 +1,55 @@
1
+ import { type Reader, Cursor, advance } from "./core.js";
2
+ import { readUVarint } from "./varint.js";
3
+
4
+ /**
5
+ * Read a `String`: a varint byte-length prefix followed by that many bytes,
6
+ * decoded as UTF-8.
7
+ *
8
+ * NOTE: ClickHouse `String` is arbitrary bytes, not guaranteed UTF-8. For binary
9
+ * columns, read `state.buf.subarray(start, start + len)` and skip the decode to
10
+ * keep the raw bytes.
11
+ */
12
+ export function readString(state: Cursor): string {
13
+ const len = readUVarint(state);
14
+ const start = advance(state, len);
15
+ return state.buf.toString("utf8", start, start + len);
16
+ }
17
+
18
+ /**
19
+ * Read a `FixedString(N)`: exactly `size` raw bytes, decoded as UTF-8. Curried:
20
+ * `readFixedString(N)` returns the reader.
21
+ *
22
+ * The value is right-padded with NUL bytes to `size`; those trailing `\x00` are
23
+ * part of the stored value and are preserved here. Trim them
24
+ * (`.replace(/\x00+$/, "")`) only if your column holds NUL-terminated text.
25
+ *
26
+ * ClickHouse server returns `FixedString`s in JSON with the trailing NULs,
27
+ * therefore this reader preserves them as well.
28
+ */
29
+ export function readFixedString(size: number): Reader<string> {
30
+ return (state) => {
31
+ const start = advance(state, size);
32
+ return state.buf.toString("utf8", start, start + size);
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Read a `FixedString(N)` as raw bytes (no UTF-8 decode) — for binary columns.
38
+ * Curried: `readFixedStringBytes(N)` returns the reader. Returns a zero-copy
39
+ * view: no allocation, but the slice shares memory with the response, so
40
+ * retaining any one slice pins the entire chunk buffer in memory.
41
+ *
42
+ * SAFE TO TOGGLE — if the bytes outlive the row/response, return an independent
43
+ * copy instead so the chunk can be freed:
44
+ *
45
+ * // return Buffer.from(state.buf.subarray(start, start + size));
46
+ *
47
+ * Make an educated tradeoff: view (default) when consumed immediately, a copy
48
+ * when retained.
49
+ */
50
+ export function readFixedStringBytes(size: number): Reader<Buffer> {
51
+ return (state) => {
52
+ const start = advance(state, size);
53
+ return state.buf.subarray(start, start + size);
54
+ };
55
+ }
@@ -0,0 +1,61 @@
1
+ import { type Reader, Cursor } from "./core.js";
2
+ import { readInt32, readInt64 } from "./integers.js";
3
+
4
+ /** Semantic alias for `number` marking a seconds value (see {@link readTime}). */
5
+ export type Seconds = number;
6
+
7
+ /**
8
+ * A signed sub-second duration kept lossless as its raw parts: the value is
9
+ * `ticks / 10 ** precision` seconds. Used by `Time64` (a time-of-day duration,
10
+ * which has no natural JS type), carrying the precision so nothing is lost.
11
+ */
12
+ export type ScaledTicks = readonly [ticks: bigint, precision: number];
13
+
14
+ /**
15
+ * Read a `Time`: 4-byte signed `Int32` seconds-of-day (range ±999:59:59).
16
+ * Returns the raw seconds; pass it to {@link formatTime}.
17
+ */
18
+ export function readTime(state: Cursor): Seconds {
19
+ return readInt32(state);
20
+ }
21
+
22
+ /**
23
+ * Read a `Time64(P)`: 8-byte signed `Int64` count of `10^-P`-second ticks.
24
+ * Curried: `readTime64(P)` returns the reader. Returns `[ticks, precision]` (a
25
+ * {@link ScaledTicks}); pass it to {@link formatTime64}.
26
+ */
27
+ export function readTime64(precision: number): Reader<ScaledTicks> {
28
+ return (state) => [readInt64(state), precision];
29
+ }
30
+
31
+ /**
32
+ * Format a `Time` value (signed seconds-of-day) as "[-]HH:MM:SS". The hour
33
+ * field can exceed two digits (the range is ±999:59:59).
34
+ */
35
+ export function formatTime(seconds: Seconds): string {
36
+ const sign = seconds < 0 ? "-" : "";
37
+ const s = Math.abs(seconds);
38
+ const hh = Math.floor(s / 3600);
39
+ const mm = Math.floor((s % 3600) / 60);
40
+ const ss = s % 60;
41
+ return `${sign}${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
42
+ }
43
+
44
+ /**
45
+ * Format a `Time64` [ticks, precision] (signed sub-second time-of-day) as
46
+ * "[-]HH:MM:SS[.fff]".
47
+ */
48
+ export function formatTime64([ticks, precision]: ScaledTicks): string {
49
+ const sign = ticks < 0n ? "-" : "";
50
+ const t = ticks < 0n ? -ticks : ticks;
51
+ const scale = 10n ** BigInt(precision);
52
+ const totalSec = Number(t / scale);
53
+ const frac = t % scale;
54
+ const hh = Math.floor(totalSec / 3600);
55
+ const mm = Math.floor((totalSec % 3600) / 60);
56
+ const ss = totalSec % 60;
57
+ const base = `${sign}${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
58
+ return precision > 0
59
+ ? `${base}.${frac.toString().padStart(precision, "0")}`
60
+ : base;
61
+ }
@@ -0,0 +1,153 @@
1
+ import { Cursor, advance } from "./core.js";
2
+
3
+ /**
4
+ * `UUID_HEX16[b]` packs the two lowercase ASCII hex chars of byte `b`, low char
5
+ * in the low byte. Drives the lookup-table UUID formatter {@link formatUUIDTable}.
6
+ */
7
+ const UUID_HEX16 = new Uint16Array(256);
8
+ for (let b = 0; b < 256; b++) {
9
+ const hex = b.toString(16).padStart(2, "0");
10
+ UUID_HEX16[b] = hex.charCodeAt(0) | (hex.charCodeAt(1) << 8);
11
+ }
12
+
13
+ /**
14
+ * Reusable 36-byte scratch for {@link formatUUIDTable}. The four `-` separators
15
+ * are written once and never touched again; each call overwrites only the 32
16
+ * hex slots, then copies the bytes out as a string.
17
+ */
18
+ const UUID_OUT = Buffer.alloc(36);
19
+ UUID_OUT[8] = UUID_OUT[13] = UUID_OUT[18] = UUID_OUT[23] = 0x2d; // '-'
20
+
21
+ /**
22
+ * Read a `UUID`: 16 raw bytes (two little-endian `UInt64` halves on the wire).
23
+ * Returns a zero-copy view; pass it to {@link formatUUID} for the canonical
24
+ * `xxxxxxxx-...` string.
25
+ *
26
+ * The view shares memory with the response buffer, so keeping it alive pins the
27
+ * whole chunk; copy with `Buffer.from(...)` if it must outlive the row.
28
+ *
29
+ * FAST ALTERNATIVE: if you stringify every UUID, use {@link formatUUIDTable}
30
+ * (lookup table, no BigInt, ~1.6x faster).
31
+ */
32
+ export function readUUID(state: Cursor): Buffer {
33
+ const start = advance(state, 16);
34
+ return state.buf.subarray(start, start + 16);
35
+ }
36
+
37
+ /**
38
+ * Read a `UUID` as a single 128-bit `bigint` (`hi << 64 | lo`) — useful for
39
+ * numeric storage, comparison, or de-duplication without a string.
40
+ *
41
+ * Reads the halves with `DataView.getBigUint64` rather than
42
+ * `Buffer.readBigUInt64LE`: V8 inlines the DataView accessors, measurably faster
43
+ * for 8-byte reads. For the canonical string, use {@link readUUID} + {@link formatUUID}.
44
+ */
45
+ export function readUUIDBigInt(state: Cursor): bigint {
46
+ const start = advance(state, 16);
47
+ const hi = state.view.getBigUint64(start, true);
48
+ const lo = state.view.getBigUint64(start + 8, true);
49
+ return (hi << 64n) | lo;
50
+ }
51
+
52
+ /**
53
+ * Read a `UUID` as its two raw little-endian `UInt64` halves, `[hi, lo]` — the
54
+ * faithful wire split with no combining work. Cheaper than {@link readUUIDBigInt}
55
+ * (skips `hi << 64 | lo`) and a compact two-value key for comparison/dedup. For
56
+ * the canonical string, use {@link readUUID} + {@link formatUUID}.
57
+ */
58
+ export function readUUIDHiLo(state: Cursor): [hi: bigint, lo: bigint] {
59
+ const start = advance(state, 16);
60
+ const hi = state.view.getBigUint64(start, true);
61
+ const lo = state.view.getBigUint64(start + 8, true);
62
+ return [hi, lo];
63
+ }
64
+
65
+ /**
66
+ * Format a `UUID` (raw 16 bytes from {@link readUUID}) as the canonical
67
+ * `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` string.
68
+ *
69
+ * THE TRAP: ClickHouse stores a UUID as two little-endian `UInt64` halves (high
70
+ * then low), so each half is byte-reversed vs the text form. Reading each half
71
+ * with `readBigUInt64LE` undoes that; concatenating high then low gives the 32
72
+ * canonical hex digits. (Hexing the 16 bytes in wire order scrambles the value.)
73
+ * Kept aside from the read so the hot path can skip stringifying when raw bytes
74
+ * suffice.
75
+ *
76
+ * FAST ALTERNATIVE: to format every value, {@link formatUUIDTable} does the same
77
+ * via a byte->hex lookup table with no BigInt (~1.6x faster).
78
+ */
79
+ export function formatUUID(b: Buffer): string {
80
+ const hex = ((b.readBigUInt64LE(0) << 64n) | b.readBigUInt64LE(8))
81
+ .toString(16)
82
+ .padStart(32, "0");
83
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
84
+ }
85
+
86
+ /**
87
+ * Fast {@link formatUUID}: same canonical string via a byte -> two-hex-char
88
+ * lookup table (`UUID_HEX16`) written into a reused 36-byte buffer (`UUID_OUT`,
89
+ * dashes preset), no BigInt, no slicing. ~1.6x faster (see `readUUID.bench.ts`).
90
+ * Takes the raw 16 bytes from {@link readUUID}.
91
+ *
92
+ * Same byte-reversal as formatUUID: emit the high half in reverse (`b[7]..b[0]`)
93
+ * then the low half (`b[15]..b[8]`).
94
+ *
95
+ * SAFE TO TOGGLE — opt-in fast formatter, not the default. `UUID_OUT` is shared
96
+ * scratch, so NOT reentrant; safe for synchronous formatting because the bytes
97
+ * are copied into the returned string before the next call (don't alias
98
+ * `UUID_OUT`). Worth it only when you stringify every UUID.
99
+ */
100
+ export function formatUUIDTable(b: Buffer): string {
101
+ let p: number;
102
+ // High half: bytes b[7]..b[0] -> hex positions 0..7 (chars 0..15).
103
+ p = UUID_HEX16[b[7]!]!;
104
+ UUID_OUT[0] = p & 0xff;
105
+ UUID_OUT[1] = p >>> 8;
106
+ p = UUID_HEX16[b[6]!]!;
107
+ UUID_OUT[2] = p & 0xff;
108
+ UUID_OUT[3] = p >>> 8;
109
+ p = UUID_HEX16[b[5]!]!;
110
+ UUID_OUT[4] = p & 0xff;
111
+ UUID_OUT[5] = p >>> 8;
112
+ p = UUID_HEX16[b[4]!]!;
113
+ UUID_OUT[6] = p & 0xff;
114
+ UUID_OUT[7] = p >>> 8;
115
+ p = UUID_HEX16[b[3]!]!;
116
+ UUID_OUT[9] = p & 0xff;
117
+ UUID_OUT[10] = p >>> 8;
118
+ p = UUID_HEX16[b[2]!]!;
119
+ UUID_OUT[11] = p & 0xff;
120
+ UUID_OUT[12] = p >>> 8;
121
+ p = UUID_HEX16[b[1]!]!;
122
+ UUID_OUT[14] = p & 0xff;
123
+ UUID_OUT[15] = p >>> 8;
124
+ p = UUID_HEX16[b[0]!]!;
125
+ UUID_OUT[16] = p & 0xff;
126
+ UUID_OUT[17] = p >>> 8;
127
+ // Low half: bytes b[15]..b[8] -> hex positions 8..15 (chars 19..35).
128
+ p = UUID_HEX16[b[15]!]!;
129
+ UUID_OUT[19] = p & 0xff;
130
+ UUID_OUT[20] = p >>> 8;
131
+ p = UUID_HEX16[b[14]!]!;
132
+ UUID_OUT[21] = p & 0xff;
133
+ UUID_OUT[22] = p >>> 8;
134
+ p = UUID_HEX16[b[13]!]!;
135
+ UUID_OUT[24] = p & 0xff;
136
+ UUID_OUT[25] = p >>> 8;
137
+ p = UUID_HEX16[b[12]!]!;
138
+ UUID_OUT[26] = p & 0xff;
139
+ UUID_OUT[27] = p >>> 8;
140
+ p = UUID_HEX16[b[11]!]!;
141
+ UUID_OUT[28] = p & 0xff;
142
+ UUID_OUT[29] = p >>> 8;
143
+ p = UUID_HEX16[b[10]!]!;
144
+ UUID_OUT[30] = p & 0xff;
145
+ UUID_OUT[31] = p >>> 8;
146
+ p = UUID_HEX16[b[9]!]!;
147
+ UUID_OUT[32] = p & 0xff;
148
+ UUID_OUT[33] = p >>> 8;
149
+ p = UUID_HEX16[b[8]!]!;
150
+ UUID_OUT[34] = p & 0xff;
151
+ UUID_OUT[35] = p >>> 8;
152
+ return UUID_OUT.toString("latin1");
153
+ }
@@ -0,0 +1,70 @@
1
+ import { Cursor, advance } from "./core.js";
2
+
3
+ /**
4
+ * Read a LEB128 unsigned varint (used for string/array lengths).
5
+ *
6
+ * Returns a JS `number`, so it is NOT bigint-friendly: only values up to
7
+ * `Number.MAX_SAFE_INTEGER` (2^53 - 1) are representable exactly. A varint
8
+ * larger than that throws rather than silently losing precision. RowBinary
9
+ * lengths never approach this in practice.
10
+ *
11
+ * The loop is unrolled: each byte carries 7 bits, so its place value is the
12
+ * constant 2^(7*k). The overwhelmingly common 1–2 byte case costs one or two
13
+ * reads and a compare.
14
+ *
15
+ * Multipliers must stay as `*` (not `<<`): JS bitwise shift is 32-bit and would wrap past bit 31.
16
+ *
17
+ * SAFE TO TOGGLE — how many bytes to handle:
18
+ * - If you know the maximum blob/array size, keep only the steps you need and
19
+ * delete the rest along with the overflow guard. E.g. lengths < 2^28 fit in
20
+ * 4 bytes, so everything below the `* 268435456` step can go.
21
+ * - Keep all eight steps (the default) when lengths are untrusted.
22
+ * If you genuinely need lengths beyond 2^53, create a bigint version of this
23
+ * function with a bigint accumulator instead of removing the guard.
24
+ *
25
+ * OPTIMIZATION HINT — for a known invariant, emit a dedicated named variant
26
+ * rather than toggling here. E.g. a `readUVarint32` for lengths guaranteed to be
27
+ * 32-bit would unroll only the first five bytes and throw past 2^32 - 1.
28
+ */
29
+ export function readUVarint(state: Cursor): number {
30
+ // Each byte reserves its space through `advance(1)` (the bounds check), but
31
+ // the read itself stays inlined as `state.buf[...]` rather than calling
32
+ // readUInt8 — this is the hottest loop in the reader.
33
+ let byte = state.buf[advance(state, 1)]!;
34
+ if (byte < 0x80) return byte; // 1 byte -> 2^0
35
+ let result = byte & 0x7f;
36
+
37
+ byte = state.buf[advance(state, 1)]!;
38
+ if (byte < 0x80) return result + byte * 128; // 2^7
39
+ result += (byte & 0x7f) * 128;
40
+
41
+ byte = state.buf[advance(state, 1)]!;
42
+ if (byte < 0x80) return result + byte * 16384; // 2^14
43
+ result += (byte & 0x7f) * 16384;
44
+
45
+ byte = state.buf[advance(state, 1)]!;
46
+ if (byte < 0x80) return result + byte * 2097152; // 2^21
47
+ result += (byte & 0x7f) * 2097152;
48
+
49
+ byte = state.buf[advance(state, 1)]!;
50
+ if (byte < 0x80) return result + byte * 268435456; // 2^28
51
+ result += (byte & 0x7f) * 268435456;
52
+
53
+ byte = state.buf[advance(state, 1)]!;
54
+ if (byte < 0x80) return result + byte * 34359738368; // 2^35
55
+ result += (byte & 0x7f) * 34359738368;
56
+
57
+ byte = state.buf[advance(state, 1)]!;
58
+ if (byte < 0x80) return result + byte * 4398046511104; // 2^42
59
+ result += (byte & 0x7f) * 4398046511104;
60
+
61
+ // 8th byte: only its low 4 payload bits (bits 49..52) fit under 2^53. A larger
62
+ // payload, or a continuation bit signalling a 9th byte, overflows MAX_SAFE_INTEGER.
63
+ byte = state.buf[advance(state, 1)]!;
64
+ if (byte > 0x0f) {
65
+ throw new RangeError(
66
+ "RowBinary: varint exceeds Number.MAX_SAFE_INTEGER (2^53 - 1)",
67
+ );
68
+ }
69
+ return result + byte * 562949953421312; // 2^49
70
+ }