@clickhouse/client 1.23.0-head.fae5998.1 → 1.23.0

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 (88) hide show
  1. package/CHANGELOG.md +1342 -0
  2. package/README.md +18 -6
  3. package/dist/common/index.d.ts +2 -2
  4. package/dist/common/index.js +2 -2
  5. package/dist/common/index.js.map +1 -1
  6. package/dist/common/parse/column_types.d.ts +30 -2
  7. package/dist/common/parse/column_types.js +8 -0
  8. package/dist/common/parse/column_types.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +2 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/version.d.ts +1 -1
  13. package/dist/version.js +1 -1
  14. package/dist/version.js.map +1 -1
  15. package/package.json +7 -6
  16. package/skills/AGENTS.md +8 -0
  17. package/skills/clickhouse-js-node-rowbinary/AGENTS.md +44 -0
  18. package/skills/clickhouse-js-node-rowbinary/CHANGELOG.md +49 -0
  19. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/README.md +85 -14
  20. package/skills/clickhouse-js-node-rowbinary/SKILL.md +111 -0
  21. package/skills/{clickhouse-js-node-rowbinary-parser/SKILL.md → clickhouse-js-node-rowbinary/reader.md} +59 -123
  22. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/carts.ts +9 -5
  23. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/events.ts +5 -5
  24. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/iot.ts +4 -4
  25. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/ledger.ts +3 -3
  26. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/logs.ts +4 -4
  27. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/observability.ts +9 -10
  28. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/orders.ts +10 -9
  29. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/profiles.ts +5 -5
  30. package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/telemetry.ts +6 -6
  31. package/skills/clickhouse-js-node-rowbinary/src/readers/compile.ts +328 -0
  32. package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/dynamic.ts +12 -8
  33. package/skills/clickhouse-js-node-rowbinary/src/readers/enums.ts +40 -0
  34. package/skills/clickhouse-js-node-rowbinary/src/readers/header.ts +29 -0
  35. package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/reader.ts +17 -0
  36. package/skills/clickhouse-js-node-rowbinary/src/readers/rowBinaryWithNamesAndTypes.ts +155 -0
  37. package/skills/clickhouse-js-node-rowbinary/src/writers/aggregateFunction.ts +18 -0
  38. package/skills/clickhouse-js-node-rowbinary/src/writers/bool.ts +10 -0
  39. package/skills/clickhouse-js-node-rowbinary/src/writers/composite.ts +140 -0
  40. package/skills/clickhouse-js-node-rowbinary/src/writers/core.ts +92 -0
  41. package/skills/clickhouse-js-node-rowbinary/src/writers/datetime.ts +123 -0
  42. package/skills/clickhouse-js-node-rowbinary/src/writers/decimals.ts +51 -0
  43. package/skills/clickhouse-js-node-rowbinary/src/writers/enums.ts +18 -0
  44. package/skills/clickhouse-js-node-rowbinary/src/writers/floats.ts +40 -0
  45. package/skills/clickhouse-js-node-rowbinary/src/writers/geo.ts +125 -0
  46. package/skills/clickhouse-js-node-rowbinary/src/writers/integers.ts +90 -0
  47. package/skills/clickhouse-js-node-rowbinary/src/writers/interval.ts +11 -0
  48. package/skills/clickhouse-js-node-rowbinary/src/writers/ip.ts +121 -0
  49. package/skills/clickhouse-js-node-rowbinary/src/writers/lowCardinality.ts +12 -0
  50. package/skills/clickhouse-js-node-rowbinary/src/writers/nested.ts +17 -0
  51. package/skills/clickhouse-js-node-rowbinary/src/writers/nothing.ts +21 -0
  52. package/skills/clickhouse-js-node-rowbinary/src/writers/rows.ts +144 -0
  53. package/skills/clickhouse-js-node-rowbinary/src/writers/simpleAggregateFunction.ts +12 -0
  54. package/skills/clickhouse-js-node-rowbinary/src/writers/strings.ts +77 -0
  55. package/skills/clickhouse-js-node-rowbinary/src/writers/time.ts +54 -0
  56. package/skills/clickhouse-js-node-rowbinary/src/writers/uuid.ts +60 -0
  57. package/skills/clickhouse-js-node-rowbinary/src/writers/varint.ts +64 -0
  58. package/skills/clickhouse-js-node-rowbinary/src/writers/writer.ts +101 -0
  59. package/skills/clickhouse-js-node-rowbinary/writer.md +96 -0
  60. package/skills/clickhouse-js-node-rowbinary-parser/src/enums.ts +0 -28
  61. /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/EXAMPLES.md +0 -0
  62. /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/case-studies/iot-rowbinary-vs-json.md +0 -0
  63. /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/case-studies/ledger-rowbinary-vs-json.md +0 -0
  64. /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/case-studies/logs-json-wins.md +0 -0
  65. /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/case-studies/wasm-vs-js.md +0 -0
  66. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/aggregateFunction.ts +0 -0
  67. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/bool.ts +0 -0
  68. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/columnar.ts +0 -0
  69. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/composite.ts +0 -0
  70. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/core.ts +0 -0
  71. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/datetime.ts +0 -0
  72. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/decimals.ts +0 -0
  73. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/floats.ts +0 -0
  74. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/geo.ts +0 -0
  75. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/integers.ts +0 -0
  76. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/interval.ts +0 -0
  77. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/ip.ts +0 -0
  78. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/json.ts +0 -0
  79. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/lowCardinality.ts +0 -0
  80. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/nested.ts +0 -0
  81. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/nothing.ts +0 -0
  82. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/rows.ts +0 -0
  83. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/simpleAggregateFunction.ts +0 -0
  84. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/stream.ts +0 -0
  85. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/strings.ts +0 -0
  86. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/time.ts +0 -0
  87. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/uuid.ts +0 -0
  88. /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/varint.ts +0 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The `RowBinaryWithNamesAndTypes` entry point: read the header off a cursor,
3
+ * compile each column's type string into a reader, and hand back a driver that
4
+ * decodes the rest of the stream.
5
+ *
6
+ * This ties together the pieces: {@link readHeader} (wire), the parser
7
+ * (`@clickhouse/datatype-parser`), and {@link astToReader} (the AST → reader
8
+ * fold in `compile.ts`), then assembles a named-tuple row reader over the
9
+ * columns and a {@link readRows} driver for the row data.
10
+ */
11
+
12
+ import { parseDataType } from "@clickhouse/datatype-parser";
13
+
14
+ import type { Reader, Cursor } from "./core.js";
15
+ import { readHeader } from "./header.js";
16
+ import { readRows } from "./rows.js";
17
+ import { astToReader, RowBinaryTypeError } from "./compile.js";
18
+
19
+ /** One decoded row, keyed by column name. */
20
+ export type Row = Record<string, unknown>;
21
+
22
+ /**
23
+ * The product of compiling a `RowBinaryWithNamesAndTypes` header: the column
24
+ * metadata, the per-column readers, and — the headline — `readRows`, the
25
+ * {@link Reader} that decodes every remaining row of the stream.
26
+ */
27
+ export interface CompiledStream {
28
+ /** Column names, in stream order (from the header). */
29
+ names: string[];
30
+ /** Column type strings, in stream order (from the header). */
31
+ types: string[];
32
+ /** One folded reader per column, in stream order. */
33
+ columnReaders: Reader<unknown>[];
34
+ /** Reads exactly one row into a `{ [name]: value }` object. */
35
+ readRow: Reader<Row>;
36
+ /**
37
+ * Reads the REST of the stream (all rows after the header) into an array.
38
+ * Streaming-aware via {@link readRows}: on a partial trailing row it rewinds
39
+ * to the last complete row and returns what it has.
40
+ */
41
+ readRows: Reader<Row[]>;
42
+ }
43
+
44
+ /**
45
+ * Parse one ClickHouse type string and fold it into a {@link Reader}. Throws a
46
+ * {@link RowBinaryTypeError} if the parser rejects the string (e.g. the
47
+ * deliberately unsupported `AggregateFunction` / `SimpleAggregateFunction`) —
48
+ * carrying the `typeString` and the parse `position`.
49
+ */
50
+ export function typeStringToReader(typeStr: string): Reader<unknown> {
51
+ const result = parseDataType(typeStr);
52
+ if (!result.ok()) {
53
+ const err = result.error!;
54
+ throw new RowBinaryTypeError(
55
+ `cannot compile type ${JSON.stringify(typeStr)}: ${err.message}`,
56
+ { typeString: typeStr, position: err.position },
57
+ );
58
+ }
59
+ return astToReader(result.ast!);
60
+ }
61
+
62
+ /** Resolves a ClickHouse type string to a reader — `typeStringToReader` or a cache wrapping it. */
63
+ export type TypeReaderResolver = (typeStr: string) => Reader<unknown>;
64
+
65
+ /**
66
+ * Build an LRU-cached {@link typeStringToReader}. The full ClickHouse type
67
+ * STRING is a perfect cache key: two columns of the same type compile to the
68
+ * same reader, and a reader is stateless (it only ever touches the cursor it is
69
+ * handed), so one instance is safe to share across columns and across streams —
70
+ * a cache hit skips the parse + AST fold entirely.
71
+ *
72
+ * Worth it when you decode many `RowBinaryWithNamesAndTypes` responses whose
73
+ * schemas overlap (e.g. the same query run repeatedly): keep one cache and pass
74
+ * it to {@link compileRowBinaryWithNamesAndTypes}, so a recurring type is
75
+ * compiled once rather than once per response. A single response rarely repeats
76
+ * a type across its own columns, so the win is across calls, not within one.
77
+ *
78
+ * Classic Map-based LRU: a `Map` iterates in insertion order, so on a HIT we
79
+ * delete + re-set the entry to move it to the most-recently-used end, and on
80
+ * overflow we evict the oldest key (the first the `Map` yields). `maxSize` caps
81
+ * memory. A parse FAILURE is never cached — {@link typeStringToReader} throws
82
+ * before anything is stored — so fixing a bad type is not shadowed by a cached
83
+ * error.
84
+ */
85
+ export function createTypeReaderCache(maxSize = 256): TypeReaderResolver {
86
+ const cache = new Map<string, Reader<unknown>>();
87
+ return (typeStr) => {
88
+ const cached = cache.get(typeStr);
89
+ if (cached !== undefined) {
90
+ // Touch on hit. A Map iterates in INSERTION order, not usage order — so on
91
+ // its own `keys().next()` would give the oldest-added key, not the
92
+ // least-recently-USED one. Deleting and re-inserting moves this key to the
93
+ // tail, which is what turns insertion order INTO recency order: every
94
+ // access (hit here, or miss below) lands the key at the tail, leaving the
95
+ // head as the genuine least-recently-used entry.
96
+ cache.delete(typeStr);
97
+ cache.set(typeStr, cached);
98
+ return cached;
99
+ }
100
+ const reader = typeStringToReader(typeStr); // may throw — then nothing is cached
101
+ cache.set(typeStr, reader);
102
+ if (cache.size > maxSize) {
103
+ // The head is the least-recently-used key (see touch-on-hit above), so it
104
+ // is the correct one to evict.
105
+ const lru = cache.keys().next().value;
106
+ if (lru !== undefined) cache.delete(lru);
107
+ }
108
+ return reader;
109
+ };
110
+ }
111
+
112
+ /**
113
+ * The headline entry point. Reads the `RowBinaryWithNamesAndTypes` header off
114
+ * `state`, compiles each column type into a combinator reader, and returns the
115
+ * column metadata plus the readers — including `readRows`, the reader for the
116
+ * REST of the stream. After this call the cursor sits at the first row, so:
117
+ *
118
+ * const s = new Cursor(buf);
119
+ * const { names, readRows } = compileRowBinaryWithNamesAndTypes(s);
120
+ * const rows = readRows(s); // decode every remaining row
121
+ *
122
+ * Pass `resolveType` to reuse readers across calls — e.g. a shared
123
+ * {@link createTypeReaderCache}. It defaults to {@link typeStringToReader}
124
+ * (compile every column afresh).
125
+ */
126
+ export function compileRowBinaryWithNamesAndTypes(
127
+ state: Cursor,
128
+ resolveType: TypeReaderResolver = typeStringToReader,
129
+ ): CompiledStream {
130
+ const { names, types } = readHeader(state);
131
+ const columnReaders = types.map((t) => resolveType(t));
132
+
133
+ // Build the row reader POSITIONALLY — by column index, NOT by keying the
134
+ // readers on column name and handing them to `readTupleNamed`. The header is
135
+ // an ordered list and RowBinary has no row delimiter, so every row MUST read
136
+ // exactly these readers, in exactly this order. Keying readers by name first
137
+ // would corrupt the stream on legal-but-awkward headers:
138
+ // - duplicate column names (e.g. two `SELECT 1 AS x, 2 AS x`) collapse to a
139
+ // single entry in a `Record`, so fewer readers run than there are columns;
140
+ // - integer-like names (`0`, `1`, …) are reordered ahead of string keys by
141
+ // `Object.keys()`, so the readers would run out of header order.
142
+ // Either desyncs the cursor and misreads every subsequent row. Reading by
143
+ // index sidesteps both. The row OBJECT is still keyed by name; on a duplicate
144
+ // name the last column with that name wins in the object, but every column is
145
+ // still consumed off the wire in order, so the cursor stays in sync.
146
+ const readRow: Reader<Row> = (s) => {
147
+ const row: Row = {};
148
+ for (let i = 0; i < columnReaders.length; i++) {
149
+ row[names[i]!] = columnReaders[i]!(s);
150
+ }
151
+ return row;
152
+ };
153
+
154
+ return { names, types, columnReaders, readRow, readRows: readRows(readRow) };
155
+ }
@@ -0,0 +1,18 @@
1
+ import { type Writer } from "./core.js";
2
+
3
+ /**
4
+ * Inverse of `readAggregateFunction`: an `AggregateFunction(func, T…)` column is
5
+ * OPAQUE, unframed aggregation state with a layout specific to `func` and the
6
+ * server version, so it cannot be produced generically from a value. Build the
7
+ * state server-side (the `-State` combinators) rather than encoding it on the
8
+ * client.
9
+ *
10
+ * This writer throws to stop a generic encoder from emitting a misaligned row.
11
+ */
12
+ export const writeAggregateFunction: Writer<never> = () => {
13
+ throw new Error(
14
+ "RowBinary: AggregateFunction is opaque, unframed aggregation state with no " +
15
+ "length prefix — not generically encodable. Produce the state server-side " +
16
+ "(the -State combinators) instead of encoding it on the client.",
17
+ );
18
+ };
@@ -0,0 +1,10 @@
1
+ import { Sink } from "./core.js";
2
+ import { writeUInt8 } from "./integers.js";
3
+
4
+ /**
5
+ * Write a `Bool`: 1 byte, stored as `UInt8` (`false` -> 0, `true` -> 1). Mirror
6
+ * of `readBool`.
7
+ */
8
+ export function writeBool(sink: Sink, value: boolean): void {
9
+ writeUInt8(sink, value ? 1 : 0);
10
+ }
@@ -0,0 +1,140 @@
1
+ import { type Writer } from "./core.js";
2
+ import { writeUInt8 } from "./integers.js";
3
+ import { writeUVarint } from "./varint.js";
4
+
5
+ // --- Writers: the encode mirror of the combinators in `composite.ts`. Each takes
6
+ // sub-WRITERS (instead of sub-readers) and returns a Writer; MONOMORPHIZE when
7
+ // generating code, exactly as noted for the readers.
8
+
9
+ /**
10
+ * Write a `Nullable(T)`: a 1-byte null flag (0 = present, 1 = NULL), then the
11
+ * inner value ONLY when present. The inverse of `readNullable`; curried — pass the
12
+ * inner writer, get a `Writer<T | null>`.
13
+ */
14
+ export function writeNullable<T>(writeValue: Writer<T>): Writer<T | null> {
15
+ return (sink, value) => {
16
+ if (value === null) {
17
+ writeUInt8(sink, 1);
18
+ } else {
19
+ writeUInt8(sink, 0);
20
+ writeValue(sink, value);
21
+ }
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Write an `Array(T)`: a LEB128 element count, then each element back-to-back. The
27
+ * inverse of `readArray`; curried — pass the element writer, get a `Writer<T[]>`.
28
+ */
29
+ export function writeArray<T>(writeElement: Writer<T>): Writer<readonly T[]> {
30
+ return (sink, values) => {
31
+ writeUVarint(sink, values.length);
32
+ // C-style loop, not for-of: this is a hot path and we don't want the
33
+ // iterator protocol overhead on a plain array.
34
+ for (let i = 0; i < values.length; i++) writeElement(sink, values[i]!);
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Write a `QBit(element_type, dimension)`: in RowBinary it is byte-for-byte an
40
+ * `Array(element_type)`, so this is just {@link writeArray}. The inverse of
41
+ * `readQBit`.
42
+ */
43
+ export function writeQBit<T>(writeElement: Writer<T>): Writer<readonly T[]> {
44
+ return writeArray(writeElement);
45
+ }
46
+
47
+ /**
48
+ * Write a `Tuple(...)` from a positional array: each element's value
49
+ * back-to-back, with NO count and NO delimiter. The inverse of `readTuple`;
50
+ * curried — pass one writer per element (in order), get a `Writer` of the tuple.
51
+ */
52
+ export function writeTuple<T extends readonly unknown[]>(writers: {
53
+ [K in keyof T]: Writer<T[K]>;
54
+ }): Writer<T> {
55
+ const fns = writers as ReadonlyArray<Writer<unknown>>;
56
+ return (sink, value) => {
57
+ for (let i = 0; i < fns.length; i++) fns[i]!(sink, value[i]);
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Write a named `Tuple(name1 T1, ...)` from an object. The wire is identical to an
63
+ * unnamed tuple — values back-to-back, no count or delimiter — so the writers run
64
+ * in the `writers` object's key order, which MUST match the tuple's declared field
65
+ * order. The inverse of `readTupleNamed`; curried.
66
+ */
67
+ export function writeTupleNamed<T extends Record<string, unknown>>(writers: {
68
+ [K in keyof T]: Writer<T[K]>;
69
+ }): Writer<T> {
70
+ const fns = writers as Record<string, Writer<unknown>>;
71
+ const keys = Object.keys(fns);
72
+ return (sink, value) => {
73
+ // C-style loop, not for-of: hot path, plain array of keys.
74
+ for (let i = 0; i < keys.length; i++) {
75
+ const key = keys[i]!;
76
+ fns[key]!(sink, value[key]);
77
+ }
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Write a `Map(K, V)`: a LEB128 pair count, then key/value interleaved
83
+ * (k, v, k, v, ...). The inverse of `readMap`; curried — pass the key and value
84
+ * writers, get a `Writer<Map<K, V>>` (`Map` iteration order is preserved).
85
+ */
86
+ export function writeMap<K, V>(
87
+ writeKey: Writer<K>,
88
+ writeValue: Writer<V>,
89
+ ): Writer<ReadonlyMap<K, V>> {
90
+ return (sink, map) => {
91
+ writeUVarint(sink, map.size);
92
+ // for-of is intentional here: it is the fastest way to iterate a `Map`
93
+ // (unlike plain arrays, where a C-style index loop wins).
94
+ for (const [key, value] of map) {
95
+ writeKey(sink, key);
96
+ writeValue(sink, value);
97
+ }
98
+ };
99
+ }
100
+
101
+ /**
102
+ * A tagged `Variant` value for {@link writeVariant}: the active alternative's
103
+ * `discriminant` (its index in the sorted-type-name order) paired with its value,
104
+ * or `null` for a NULL.
105
+ *
106
+ * WHY TAGGED: `readVariant` returns only the decoded VALUE — the discriminant is
107
+ * consumed from the wire and not surfaced — so encode cannot recover which
108
+ * alternative a bare value belongs to (e.g. is `5` the `UInt8` or the `Int32`
109
+ * alternative?). The discriminant must therefore be supplied explicitly, the
110
+ * encode-side analog of the `readGeometry` switch.
111
+ */
112
+ export type VariantValue =
113
+ | readonly [discriminant: number, value: unknown]
114
+ | null;
115
+
116
+ /**
117
+ * Write a `Variant(T1, ..., Tn)`: a 1-byte discriminant then the chosen
118
+ * alternative's value (discriminant `0xFF` = NULL, no value). The inverse of
119
+ * `readVariant`; curried — pass the alternative writers in sorted-type-name order
120
+ * (same order the reader expects), get a `Writer<VariantValue>`.
121
+ */
122
+ export function writeVariant(
123
+ writers: ReadonlyArray<Writer<never>>,
124
+ ): Writer<VariantValue> {
125
+ return (sink, value) => {
126
+ if (value === null) {
127
+ writeUInt8(sink, 0xff);
128
+ return;
129
+ }
130
+ const [discriminant, inner] = value;
131
+ const fn = writers[discriminant] as Writer<unknown> | undefined;
132
+ if (fn === undefined) {
133
+ throw new RangeError(
134
+ `RowBinary Variant: discriminant ${discriminant} out of range (${writers.length} alternatives)`,
135
+ );
136
+ }
137
+ writeUInt8(sink, discriminant);
138
+ fn(sink, inner);
139
+ };
140
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Thrown by {@link reserve} when the sink's buffer is full — i.e. lacks the bytes
3
+ * a write needs. The encode-side mirror of the reader's `NeedMoreData`. Like the
4
+ * reader, the `Sink` treats its buffer as a FIXED-length window: when a write
5
+ * would overflow it, `reserve` throws this sentinel WITHOUT moving the position,
6
+ * so a driver can flush the bytes written so far down the connection (a transport
7
+ * filter can glue successive buffers back together) and continue into a fresh
8
+ * buffer.
9
+ *
10
+ * A bare sentinel, NOT an `Error` subclass, on purpose — exactly as `NeedMoreData`
11
+ * on the read side: constructing an `Error` captures a stack trace (the expensive
12
+ * part of throwing), pure waste on a path that fires once per buffer boundary.
13
+ */
14
+ export const BufferFull = Symbol("RowBinary.BufferFull");
15
+
16
+ /**
17
+ * The write-side mirror of the reader's `Cursor`: the cursor every writer threads
18
+ * through. A `Buffer` to write into, the current write position, and a
19
+ * `DataView` over the same bytes. The encode counterpart of decode's `Cursor`.
20
+ *
21
+ * Deliberately STATE only — no write methods. Encoding lives in the free
22
+ * `writeX(sink, value)` functions in the sibling modules, so a generated encoder
23
+ * pulls in only the per-type writers a result needs (exactly like the reader
24
+ * side). `view`/`buf` are public so those free functions can reach them.
25
+ *
26
+ * Like a `Cursor`, a `Sink` wraps a FIXED-length buffer (supplied by the caller):
27
+ * it never reallocates. {@link reserve} throws {@link BufferFull} when the next
28
+ * write would overflow, the encode mirror of the reader's `advance` throwing
29
+ * `NeedMoreData` on underflow. Size the buffer to a chunk you intend to flush, and
30
+ * pull the written bytes with {@link Sink.bytes}.
31
+ */
32
+ export class Sink {
33
+ pos = 0;
34
+
35
+ /**
36
+ * The buffer being written into. Only `buf.subarray(0, pos)` (see
37
+ * {@link Sink.bytes}) holds written bytes; the tail is unwritten headroom.
38
+ * Built with the buffer's own `byteOffset`/`byteLength` view in
39
+ * {@link Sink.view}, exactly like the reader's `Cursor`.
40
+ */
41
+ readonly buf: Buffer;
42
+
43
+ /**
44
+ * `DataView` over {@link Sink.buf}, for fixed-width integer/float writes. Built
45
+ * with the buffer's own `byteOffset`/`byteLength`: a `Buffer` is often a window
46
+ * into a larger pooled `ArrayBuffer`, so `new DataView(buf.buffer)` alone would
47
+ * point at the wrong bytes.
48
+ */
49
+ readonly view: DataView;
50
+
51
+ constructor(buf: Buffer) {
52
+ this.buf = buf;
53
+ this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
54
+ }
55
+
56
+ /**
57
+ * The written bytes — `buf.subarray(0, pos)`. A zero-copy VIEW into the sink's
58
+ * buffer, so use `Buffer.from(sink.bytes())` if you need an independent copy.
59
+ */
60
+ bytes(): Buffer {
61
+ return this.buf.subarray(0, this.pos);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * A `Writer<T>` encodes one value of type `T` into the sink, advancing it — the
67
+ * mirror of the reader's `Reader<T>`. Leaf writers (e.g. `writeUInt32`) are
68
+ * `Writer`s directly; combinators (e.g. `writeArray`) take sub-`Writer`s and
69
+ * return a `Writer`, so types compose with no per-element closures.
70
+ */
71
+ export type Writer<T> = (sink: Sink, value: T) => void;
72
+
73
+ /**
74
+ * Reserve `n` bytes for the next write: bounds-check them, advance the position
75
+ * past them, and return the offset the write starts at (the value BEFORE
76
+ * advancing). The write-side mirror of the reader's `advance`: every fixed-width
77
+ * write goes through it, so the capacity check and position bookkeeping live in
78
+ * one place:
79
+ *
80
+ * function writeInt32(s, v) { s.view.setInt32(reserve(s, 4), v, true); }
81
+ *
82
+ * Throws {@link BufferFull} when fewer than `n` bytes remain, WITHOUT moving
83
+ * the position — the buffer is fixed-length, exactly as the reader's input is, so
84
+ * a driver flushes what is written and retries the row into a fresh buffer.
85
+ */
86
+ export function reserve(sink: Sink, n: number): number {
87
+ const start = sink.pos;
88
+ const next = start + n;
89
+ if (next > sink.buf.length) throw BufferFull;
90
+ sink.pos = next;
91
+ return start;
92
+ }
@@ -0,0 +1,123 @@
1
+ import { type Writer, Sink, reserve } from "./core.js";
2
+ import { type Microseconds, type Nanoseconds } from "../readers/datetime.js";
3
+
4
+ const MS_PER_DAY = 86_400_000;
5
+
6
+ /**
7
+ * Write a `Date`: 2-byte `UInt16` count of days since 1970-01-01 (UTC). The
8
+ * inverse of `readDate` — the `Date` it produced is at UTC midnight, so
9
+ * `getTime() / 86_400_000` recovers the whole-day count exactly. A non-midnight
10
+ * input is floored to its calendar day (matching ClickHouse's truncation),
11
+ * never rounded up into the next day.
12
+ *
13
+ * PRECONDITION: a valid `Date` whose day count fits the `UInt16` range
14
+ * (1970-01-01 … ~2149-06-06). Like every leaf writer (see `writeUVarint`) this
15
+ * is not range-checked — an invalid or out-of-range `Date` (e.g. a pre-1970 one,
16
+ * which belongs in {@link writeDate32}) is a programming error; the resulting
17
+ * bytes are rejected server-side.
18
+ */
19
+ export function writeDate(sink: Sink, value: Date): void {
20
+ sink.view.setUint16(
21
+ reserve(sink, 2),
22
+ Math.floor(value.getTime() / MS_PER_DAY),
23
+ true,
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Write a `Date32`: 4-byte signed `Int32` count of days since 1970-01-01 (UTC),
29
+ * negative for pre-1970 dates. The inverse of `readDate32`. A non-midnight input
30
+ * is floored toward -inf to its calendar day, so pre-1970 instants land on the
31
+ * correct (more negative) day rather than rounding toward the epoch.
32
+ *
33
+ * PRECONDITION: a valid `Date` whose day count fits `Int32`. Not range-checked
34
+ * (as elsewhere) — an invalid `Date` is a programming error, rejected server-side.
35
+ */
36
+ export function writeDate32(sink: Sink, value: Date): void {
37
+ sink.view.setInt32(
38
+ reserve(sink, 4),
39
+ Math.floor(value.getTime() / MS_PER_DAY),
40
+ true,
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Write a `DateTime`: 4-byte `UInt32` Unix seconds. The inverse of `readDateTime`;
46
+ * the column timezone is metadata, not in the bytes. Sub-second components are
47
+ * floored away (matching the reader and `writeDateTime64`'s `Math.floor`), never
48
+ * rounded up to the next second.
49
+ *
50
+ * PRECONDITION: a valid `Date` whose Unix-seconds fit the `UInt32` range
51
+ * (1970-01-01 … 2106-02-07). Not range-checked (as elsewhere) — an invalid or
52
+ * out-of-range `Date` is a programming error, rejected server-side.
53
+ */
54
+ export function writeDateTime(sink: Sink, value: Date): void {
55
+ sink.view.setUint32(
56
+ reserve(sink, 4),
57
+ Math.floor(value.getTime() / 1000),
58
+ true,
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Write a `DateTime64(P)`: 8-byte signed `Int64` count of `10^-P`-second ticks.
64
+ * Curried: `writeDateTime64(P)` returns the writer. The inverse of
65
+ * `readDateTime64`, which returns `[date, nanoseconds]` (date truncated to whole
66
+ * seconds, nanoseconds the sub-second remainder regardless of P). This recombines
67
+ * them: `ticks = seconds * 10^P + nanoseconds / 10^(9 - P)`. The reader floors
68
+ * seconds toward -inf with a non-negative remainder, so the seconds are computed
69
+ * with `Math.floor` on the cheap JS-number millisecond value (not bigint division,
70
+ * which truncates toward zero) — exact for negative instants too.
71
+ */
72
+ export function writeDateTime64(
73
+ precision: number,
74
+ ): Writer<[Date, Nanoseconds]> {
75
+ const scale = 10n ** BigInt(precision);
76
+ const nsPerTick = 10n ** BigInt(9 - precision);
77
+ return (sink, [date, nanoseconds]) => {
78
+ const seconds = BigInt(Math.floor(date.getTime() / 1000));
79
+ sink.buf.writeBigInt64LE(
80
+ seconds * scale + BigInt(nanoseconds) / nsPerTick,
81
+ reserve(sink, 8),
82
+ );
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Write a `DateTime64(3)` (milliseconds) from a plain `Date` — the inverse of
88
+ * `readDateTime64P3`. P=3 is a `Date`'s own resolution, so the tick count is
89
+ * exactly `getTime()` in milliseconds.
90
+ */
91
+ export function writeDateTime64P3(sink: Sink, value: Date): void {
92
+ sink.buf.writeBigInt64LE(BigInt(value.getTime()), reserve(sink, 8));
93
+ }
94
+
95
+ /**
96
+ * Write a `DateTime64(6)` (microseconds) from `[date, microseconds]` — the
97
+ * inverse of `readDateTime64P6`. `ticks = seconds * 1_000_000 + micros`.
98
+ */
99
+ export function writeDateTime64P6(
100
+ sink: Sink,
101
+ [date, microseconds]: [Date, Microseconds],
102
+ ): void {
103
+ const seconds = BigInt(Math.floor(date.getTime() / 1000));
104
+ sink.buf.writeBigInt64LE(
105
+ seconds * 1_000_000n + BigInt(microseconds),
106
+ reserve(sink, 8),
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Write a `DateTime64(9)` (nanoseconds) from `[date, nanoseconds]` — the inverse
112
+ * of `readDateTime64P9`. `ticks = seconds * 1_000_000_000 + nanos`.
113
+ */
114
+ export function writeDateTime64P9(
115
+ sink: Sink,
116
+ [date, nanoseconds]: [Date, Nanoseconds],
117
+ ): void {
118
+ const seconds = BigInt(Math.floor(date.getTime() / 1000));
119
+ sink.buf.writeBigInt64LE(
120
+ seconds * 1_000_000_000n + BigInt(nanoseconds),
121
+ reserve(sink, 8),
122
+ );
123
+ }
@@ -0,0 +1,51 @@
1
+ import { type Writer } from "./core.js";
2
+ import { type DecimalValue } from "../readers/decimals.js";
3
+ import {
4
+ writeInt32,
5
+ writeInt64,
6
+ writeInt128,
7
+ writeInt256,
8
+ } from "./integers.js";
9
+
10
+ /**
11
+ * Parse a fixed-point decimal string into a {@link DecimalValue} at the given
12
+ * `scale` — the inverse of `formatDecimal`. `"1.5000"` with scale 4 ->
13
+ * `[15000n, 4]`. A shorter fraction is right-padded with zeros to `scale`; a
14
+ * longer one is truncated (not rounded). Plug in only when you start from a
15
+ * string; if you already have the unscaled bigint, build the pair directly.
16
+ */
17
+ export function parseDecimal(text: string, scale: number): DecimalValue {
18
+ const neg = text.startsWith("-");
19
+ const body = neg ? text.slice(1) : text;
20
+ const dot = body.indexOf(".");
21
+ const intPart = dot < 0 ? body : body.slice(0, dot);
22
+ const fracPart = dot < 0 ? "" : body.slice(dot + 1);
23
+ const frac = (fracPart + "0".repeat(scale)).slice(0, scale);
24
+ let unscaled = BigInt((intPart || "0") + frac);
25
+ if (neg) unscaled = -unscaled;
26
+ return [unscaled, scale];
27
+ }
28
+
29
+ /**
30
+ * Write a `Decimal32(P, S)`: the `unscaled` part of a {@link DecimalValue} as a
31
+ * 4-byte little-endian signed integer (same wire as `Int32`). The inverse of
32
+ * `readDecimal32`; the `scale` lives in the type, so only `unscaled` is written
33
+ * (it must fit in `Int32`).
34
+ *
35
+ * `Decimal(P, S)` picks the width by precision P, exactly as the readers: P<=9 ->
36
+ * Decimal32, <=18 -> Decimal64, <=38 -> Decimal128, <=76 -> Decimal256.
37
+ */
38
+ export const writeDecimal32: Writer<DecimalValue> = (sink, [unscaled]) =>
39
+ writeInt32(sink, Number(unscaled));
40
+
41
+ /** Write a `Decimal64(P, S)`: 8-byte LE signed integer. Inverse of `readDecimal64`. */
42
+ export const writeDecimal64: Writer<DecimalValue> = (sink, [unscaled]) =>
43
+ writeInt64(sink, unscaled);
44
+
45
+ /** Write a `Decimal128(P, S)`: 16-byte LE signed integer. Inverse of `readDecimal128`. */
46
+ export const writeDecimal128: Writer<DecimalValue> = (sink, [unscaled]) =>
47
+ writeInt128(sink, unscaled);
48
+
49
+ /** Write a `Decimal256(P, S)`: 32-byte LE signed integer. Inverse of `readDecimal256`. */
50
+ export const writeDecimal256: Writer<DecimalValue> = (sink, [unscaled]) =>
51
+ writeInt256(sink, unscaled);
@@ -0,0 +1,18 @@
1
+ import { Sink, reserve } from "./core.js";
2
+
3
+ /**
4
+ * Write an `Enum8`: the value's underlying signed `Int8` (1 byte). Mirror of
5
+ * `readEnum8` — the name<->value map lives in the column type, so take the raw
6
+ * numeric value.
7
+ */
8
+ export function writeEnum8(sink: Sink, value: number): void {
9
+ sink.view.setInt8(reserve(sink, 1), value);
10
+ }
11
+
12
+ /**
13
+ * Write an `Enum16`: the value's underlying signed `Int16` (2 bytes,
14
+ * little-endian). Mirror of `readEnum16`.
15
+ */
16
+ export function writeEnum16(sink: Sink, value: number): void {
17
+ sink.view.setInt16(reserve(sink, 2), value, true);
18
+ }
@@ -0,0 +1,40 @@
1
+ import { Sink, reserve } from "./core.js";
2
+
3
+ /** Write a `Float32`: 4 bytes, little-endian IEEE 754. Mirror of `readFloat32`. */
4
+ export function writeFloat32(sink: Sink, value: number): void {
5
+ sink.view.setFloat32(reserve(sink, 4), value, true);
6
+ }
7
+
8
+ /** Write a `Float64`: 8 bytes, little-endian IEEE 754. Mirror of `readFloat64`. */
9
+ export function writeFloat64(sink: Sink, value: number): void {
10
+ sink.view.setFloat64(reserve(sink, 8), value, true);
11
+ }
12
+
13
+ /**
14
+ * Scratch view for narrowing a float32 to a `BFloat16`: BFloat16's 16 bits are
15
+ * the top half of an IEEE 754 float32, so we stage the float32 and take its high
16
+ * 16 bits back out.
17
+ */
18
+ const bf16Scratch = new DataView(new ArrayBuffer(4));
19
+
20
+ /**
21
+ * Write a `BFloat16`: 2 bytes, little-endian — the high 16 bits of `value`'s
22
+ * float32 representation (same 8-bit exponent, 7-bit mantissa). Mirror of
23
+ * `readBFloat16`: it widens a BFloat16 to a float32 by placing the bits in the
24
+ * top half, so here we stage the float32 and take that top half back.
25
+ *
26
+ * NOTE: this TRUNCATES the float32 mantissa to BFloat16's 7 bits (no rounding),
27
+ * matching the reader's exact inverse for values that originated as BFloat16. An
28
+ * arbitrary float32 loses precision, exactly as ClickHouse's own BFloat16 cast.
29
+ *
30
+ * NOTE: `bf16Scratch` is module-level shared state written-then-read; safe
31
+ * because the body is synchronous (do NOT introduce an `await`/`yield` between
32
+ * the `setFloat32` and the `getUint16`).
33
+ */
34
+ export function writeBFloat16(sink: Sink, value: number): void {
35
+ bf16Scratch.setFloat32(0, value, true);
36
+ // The float32's little-endian bytes are [lo16, hi16]; the high 16 bits at byte
37
+ // offset 2 are the BFloat16 payload.
38
+ const bits = bf16Scratch.getUint16(2, true);
39
+ sink.view.setUint16(reserve(sink, 2), bits, true);
40
+ }