@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.
- package/CHANGELOG.md +1342 -0
- package/README.md +18 -6
- package/dist/common/index.d.ts +2 -2
- package/dist/common/index.js +2 -2
- package/dist/common/index.js.map +1 -1
- package/dist/common/parse/column_types.d.ts +30 -2
- package/dist/common/parse/column_types.js +8 -0
- package/dist/common/parse/column_types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +7 -6
- package/skills/AGENTS.md +8 -0
- package/skills/clickhouse-js-node-rowbinary/AGENTS.md +44 -0
- package/skills/clickhouse-js-node-rowbinary/CHANGELOG.md +49 -0
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/README.md +85 -14
- package/skills/clickhouse-js-node-rowbinary/SKILL.md +111 -0
- package/skills/{clickhouse-js-node-rowbinary-parser/SKILL.md → clickhouse-js-node-rowbinary/reader.md} +59 -123
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/carts.ts +9 -5
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/events.ts +5 -5
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/iot.ts +4 -4
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/ledger.ts +3 -3
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/logs.ts +4 -4
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/observability.ts +9 -10
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/orders.ts +10 -9
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/profiles.ts +5 -5
- package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/src/examples/telemetry.ts +6 -6
- package/skills/clickhouse-js-node-rowbinary/src/readers/compile.ts +328 -0
- package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/dynamic.ts +12 -8
- package/skills/clickhouse-js-node-rowbinary/src/readers/enums.ts +40 -0
- package/skills/clickhouse-js-node-rowbinary/src/readers/header.ts +29 -0
- package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/reader.ts +17 -0
- package/skills/clickhouse-js-node-rowbinary/src/readers/rowBinaryWithNamesAndTypes.ts +155 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/aggregateFunction.ts +18 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/bool.ts +10 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/composite.ts +140 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/core.ts +92 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/datetime.ts +123 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/decimals.ts +51 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/enums.ts +18 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/floats.ts +40 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/geo.ts +125 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/integers.ts +90 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/interval.ts +11 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/ip.ts +121 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/lowCardinality.ts +12 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/nested.ts +17 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/nothing.ts +21 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/rows.ts +144 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/simpleAggregateFunction.ts +12 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/strings.ts +77 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/time.ts +54 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/uuid.ts +60 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/varint.ts +64 -0
- package/skills/clickhouse-js-node-rowbinary/src/writers/writer.ts +101 -0
- package/skills/clickhouse-js-node-rowbinary/writer.md +96 -0
- package/skills/clickhouse-js-node-rowbinary-parser/src/enums.ts +0 -28
- /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/EXAMPLES.md +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/case-studies/iot-rowbinary-vs-json.md +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/case-studies/ledger-rowbinary-vs-json.md +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/case-studies/logs-json-wins.md +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser → clickhouse-js-node-rowbinary}/case-studies/wasm-vs-js.md +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/aggregateFunction.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/bool.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/columnar.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/composite.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/core.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/datetime.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/decimals.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/floats.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/geo.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/integers.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/interval.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/ip.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/json.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/lowCardinality.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/nested.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/nothing.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/rows.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/simpleAggregateFunction.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/stream.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/strings.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/time.ts +0 -0
- /package/skills/{clickhouse-js-node-rowbinary-parser/src → clickhouse-js-node-rowbinary/src/readers}/uuid.ts +0 -0
- /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
|
+
}
|