@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,125 @@
|
|
|
1
|
+
import { type Writer, Sink, reserve } from "./core.js";
|
|
2
|
+
import { type Point } from "../readers/geo.js";
|
|
3
|
+
import { writeUInt8 } from "./integers.js";
|
|
4
|
+
import { writeUVarint } from "./varint.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Write a `Point`: `Tuple(Float64, Float64)` -> `[x, y]`. Inverse of `readPoint`.
|
|
8
|
+
* A single `reserve(16)` then two inlined `setFloat64`s — no per-coordinate
|
|
9
|
+
* `writeFloat64` call and only one bounds check.
|
|
10
|
+
*/
|
|
11
|
+
export function writePoint(sink: Sink, [x, y]: Point): void {
|
|
12
|
+
const o = reserve(sink, 16);
|
|
13
|
+
sink.view.setFloat64(o, x, true);
|
|
14
|
+
sink.view.setFloat64(o + 8, y, true);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Write a `Ring`: `Array(Point)` — a LEB128 point count, then each point. The
|
|
19
|
+
* inverse of `readRing`; a SINGLE `reserve(16 * length)` covers the whole point
|
|
20
|
+
* block (one bounds check per ring, not per point), then the coordinates are
|
|
21
|
+
* written into it inline, mirroring the reader.
|
|
22
|
+
*/
|
|
23
|
+
export function writeRing(sink: Sink, points: readonly Point[]): void {
|
|
24
|
+
writeUVarint(sink, points.length);
|
|
25
|
+
let p = reserve(sink, points.length * 16);
|
|
26
|
+
for (let i = 0; i < points.length; i++) {
|
|
27
|
+
const [x, y] = points[i]!;
|
|
28
|
+
sink.view.setFloat64(p, x, true);
|
|
29
|
+
sink.view.setFloat64(p + 8, y, true);
|
|
30
|
+
p += 16;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Write a `LineString`: `Array(Point)` (identical wire to a `Ring`). Inverse of `readLineString`. */
|
|
35
|
+
export function writeLineString(sink: Sink, points: readonly Point[]): void {
|
|
36
|
+
writeUVarint(sink, points.length);
|
|
37
|
+
let p = reserve(sink, points.length * 16);
|
|
38
|
+
for (let i = 0; i < points.length; i++) {
|
|
39
|
+
const [x, y] = points[i]!;
|
|
40
|
+
sink.view.setFloat64(p, x, true);
|
|
41
|
+
sink.view.setFloat64(p + 8, y, true);
|
|
42
|
+
p += 16;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Write a `Polygon`: `Array(Ring)` — outer ring first, then holes. Inverse of `readPolygon`. */
|
|
47
|
+
export function writePolygon(sink: Sink, rings: readonly Point[][]): void {
|
|
48
|
+
writeUVarint(sink, rings.length);
|
|
49
|
+
for (let i = 0; i < rings.length; i++) writeRing(sink, rings[i]!);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Write a `MultiLineString`: `Array(LineString)`. Inverse of `readMultiLineString`. */
|
|
53
|
+
export function writeMultiLineString(
|
|
54
|
+
sink: Sink,
|
|
55
|
+
lines: readonly Point[][],
|
|
56
|
+
): void {
|
|
57
|
+
writeUVarint(sink, lines.length);
|
|
58
|
+
for (let i = 0; i < lines.length; i++) writeLineString(sink, lines[i]!);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Write a `MultiPolygon`: `Array(Polygon)`. Inverse of `readMultiPolygon`. */
|
|
62
|
+
export function writeMultiPolygon(
|
|
63
|
+
sink: Sink,
|
|
64
|
+
polygons: readonly Point[][][],
|
|
65
|
+
): void {
|
|
66
|
+
writeUVarint(sink, polygons.length);
|
|
67
|
+
for (let i = 0; i < polygons.length; i++) writePolygon(sink, polygons[i]!);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A tagged `Geometry` value for {@link writeGeometry}: the alternative's
|
|
72
|
+
* `discriminant` paired with its value, or `null` for NULL. Like
|
|
73
|
+
* `readVariant`/`writeVariant`, `readGeometry` returns only the value — and the
|
|
74
|
+
* geo value shapes overlap (LineString and Ring are both `Point[]`,
|
|
75
|
+
* MultiLineString and Polygon both `Point[][]`) — so encode must be told which geo
|
|
76
|
+
* type it is via the discriminant.
|
|
77
|
+
*
|
|
78
|
+
* Discriminants (sorted by type name): LineString(0), MultiLineString(1),
|
|
79
|
+
* MultiPolygon(2), Point(3), Polygon(4), Ring(5); `0xFF` = NULL.
|
|
80
|
+
*/
|
|
81
|
+
export type GeometryValue =
|
|
82
|
+
| readonly [discriminant: number, value: unknown]
|
|
83
|
+
| null;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Write a `Geometry`: a 1-byte discriminant then the chosen geo type's value. The
|
|
87
|
+
* inverse of `readGeometry` (a switch over the discriminant with each branch
|
|
88
|
+
* inlined). Takes a tagged {@link GeometryValue} because the value shapes are
|
|
89
|
+
* ambiguous on their own.
|
|
90
|
+
*/
|
|
91
|
+
export const writeGeometry: Writer<GeometryValue> = (sink, value) => {
|
|
92
|
+
if (value === null) {
|
|
93
|
+
writeUInt8(sink, 0xff);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// The discriminant byte is written inside each case, only after the switch
|
|
97
|
+
// has accepted it — an out-of-range value throws from `default` before the
|
|
98
|
+
// sink is advanced, so it never leaves a partially-written payload behind
|
|
99
|
+
// (mirrors `writeVariant`).
|
|
100
|
+
const [discriminant, geo] = value;
|
|
101
|
+
switch (discriminant) {
|
|
102
|
+
case 0:
|
|
103
|
+
writeUInt8(sink, discriminant);
|
|
104
|
+
return writeLineString(sink, geo as Point[]);
|
|
105
|
+
case 1:
|
|
106
|
+
writeUInt8(sink, discriminant);
|
|
107
|
+
return writeMultiLineString(sink, geo as Point[][]);
|
|
108
|
+
case 2:
|
|
109
|
+
writeUInt8(sink, discriminant);
|
|
110
|
+
return writeMultiPolygon(sink, geo as Point[][][]);
|
|
111
|
+
case 3:
|
|
112
|
+
writeUInt8(sink, discriminant);
|
|
113
|
+
return writePoint(sink, geo as Point);
|
|
114
|
+
case 4:
|
|
115
|
+
writeUInt8(sink, discriminant);
|
|
116
|
+
return writePolygon(sink, geo as Point[][]);
|
|
117
|
+
case 5:
|
|
118
|
+
writeUInt8(sink, discriminant);
|
|
119
|
+
return writeRing(sink, geo as Point[]);
|
|
120
|
+
default:
|
|
121
|
+
throw new RangeError(
|
|
122
|
+
`RowBinary: unknown Geometry discriminant ${discriminant}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Sink, reserve } from "./core.js";
|
|
2
|
+
|
|
3
|
+
// --- Writers: the encode mirror of the readers in `integers.ts`. Each writeX is
|
|
4
|
+
// the inverse of the matching readX.
|
|
5
|
+
|
|
6
|
+
/** Write a `UInt8`: 1 byte (0 .. 255). Mirror of `readUInt8`. */
|
|
7
|
+
export function writeUInt8(sink: Sink, value: number): void {
|
|
8
|
+
sink.buf[reserve(sink, 1)] = value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Write an `Int8`: 1 byte, two's-complement signed (-128 .. 127). Mirror of `readInt8`. */
|
|
12
|
+
export function writeInt8(sink: Sink, value: number): void {
|
|
13
|
+
sink.view.setInt8(reserve(sink, 1), value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Write a `UInt16`: 2 bytes, little-endian. Mirror of `readUInt16`. */
|
|
17
|
+
export function writeUInt16(sink: Sink, value: number): void {
|
|
18
|
+
sink.view.setUint16(reserve(sink, 2), value, true);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Write an `Int16`: 2 bytes, little-endian, two's-complement. Mirror of `readInt16`. */
|
|
22
|
+
export function writeInt16(sink: Sink, value: number): void {
|
|
23
|
+
sink.view.setInt16(reserve(sink, 2), value, true);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Write a `UInt32`: 4 bytes, little-endian. Mirror of `readUInt32`. */
|
|
27
|
+
export function writeUInt32(sink: Sink, value: number): void {
|
|
28
|
+
sink.view.setUint32(reserve(sink, 4), value, true);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Write an `Int32`: 4 bytes, little-endian, two's-complement. Mirror of `readInt32`. */
|
|
32
|
+
export function writeInt32(sink: Sink, value: number): void {
|
|
33
|
+
sink.view.setInt32(reserve(sink, 4), value, true);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Write a `UInt64`: 8 bytes, little-endian. Takes a `bigint` (mirror of
|
|
38
|
+
* `readUInt64`). Uses Node's `Buffer.writeBigUInt64LE`, which writes the 64-bit
|
|
39
|
+
* value straight from the bigint — no narrowing to a JS number. The value must be
|
|
40
|
+
* in `[0, 2^64)`.
|
|
41
|
+
*/
|
|
42
|
+
export function writeUInt64(sink: Sink, value: bigint): void {
|
|
43
|
+
sink.buf.writeBigUInt64LE(value, reserve(sink, 8));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Write an `Int64`: 8 bytes, little-endian, two's-complement. Takes a `bigint`
|
|
48
|
+
* (mirror of `readInt64`). Uses Node's `Buffer.writeBigInt64LE`, writing the
|
|
49
|
+
* signed 64-bit value straight from the bigint (range `[-2^63, 2^63)`).
|
|
50
|
+
*/
|
|
51
|
+
export function writeInt64(sink: Sink, value: bigint): void {
|
|
52
|
+
sink.buf.writeBigInt64LE(value, reserve(sink, 8));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Mask reducing a bigint word to its unsigned 64-bit (two's-complement) value. */
|
|
56
|
+
const MASK64 = (1n << 64n) - 1n;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Write a `UInt128`/`Int128`: 16 bytes, little-endian, as two 64-bit words (low
|
|
60
|
+
* then high). Each word is masked to 64 bits with `& MASK64` — a pure bigint
|
|
61
|
+
* operation that yields the correct unsigned (two's-complement) representation for
|
|
62
|
+
* negatives too — and written with Node's `Buffer.writeBigUInt64LE`, so this one
|
|
63
|
+
* function serves both `readUInt128` and `readInt128` without narrowing to a JS
|
|
64
|
+
* number.
|
|
65
|
+
*/
|
|
66
|
+
export function writeUInt128(sink: Sink, value: bigint): void {
|
|
67
|
+
const o = reserve(sink, 16);
|
|
68
|
+
sink.buf.writeBigUInt64LE(value & MASK64, o);
|
|
69
|
+
sink.buf.writeBigUInt64LE((value >> 64n) & MASK64, o + 8);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Write an `Int128`: 16 bytes LE two's-complement. Same word layout as {@link writeUInt128}. */
|
|
73
|
+
export const writeInt128 = writeUInt128;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Write a `UInt256`/`Int256`: 32 bytes, little-endian, as four 64-bit words
|
|
77
|
+
* (least-significant first). Like {@link writeUInt128}, each word is masked with
|
|
78
|
+
* `& MASK64` and written via `Buffer.writeBigUInt64LE`, handling both unsigned and
|
|
79
|
+
* signed (two's complement) values straight from the bigint.
|
|
80
|
+
*/
|
|
81
|
+
export function writeUInt256(sink: Sink, value: bigint): void {
|
|
82
|
+
const o = reserve(sink, 32);
|
|
83
|
+
sink.buf.writeBigUInt64LE(value & MASK64, o);
|
|
84
|
+
sink.buf.writeBigUInt64LE((value >> 64n) & MASK64, o + 8);
|
|
85
|
+
sink.buf.writeBigUInt64LE((value >> 128n) & MASK64, o + 16);
|
|
86
|
+
sink.buf.writeBigUInt64LE((value >> 192n) & MASK64, o + 24);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Write an `Int256`: 32 bytes LE two's-complement. Same word layout as {@link writeUInt256}. */
|
|
90
|
+
export const writeInt256 = writeUInt256;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { writeInt64 } from "./integers.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write an `Interval` — any of `IntervalNanosecond` ... `IntervalYear`: a signed
|
|
5
|
+
* `Int64` count of the unit. The inverse of `readInterval`; the unit lives in the
|
|
6
|
+
* column type, not the bytes, so all 11 interval types share this writer.
|
|
7
|
+
*
|
|
8
|
+
* It IS `writeInt64` — assigned directly rather than wrapped, so there is no extra
|
|
9
|
+
* call frame on the wire-write path.
|
|
10
|
+
*/
|
|
11
|
+
export const writeInterval = writeInt64;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Sink, reserve } from "./core.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write an `IPv4`: the raw 32-bit value (as produced by `readIPv4`) as a 4-byte
|
|
5
|
+
* little-endian `UInt32`. The inverse of `readIPv4`; pair with {@link parseIPv4}
|
|
6
|
+
* to start from a dotted-quad string.
|
|
7
|
+
*/
|
|
8
|
+
export function writeIPv4(sink: Sink, value: number): void {
|
|
9
|
+
sink.view.setUint32(reserve(sink, 4), value, true);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Write an `IPv6`: the raw 16 bytes (network order, as produced by `readIPv6`)
|
|
14
|
+
* copied verbatim. The inverse of `readIPv6`; pair with {@link parseIPv6} to
|
|
15
|
+
* start from a string. Throws unless exactly 16 bytes.
|
|
16
|
+
*/
|
|
17
|
+
export function writeIPv6(sink: Sink, value: Uint8Array): void {
|
|
18
|
+
if (value.length !== 16) {
|
|
19
|
+
throw new RangeError(
|
|
20
|
+
`RowBinary: IPv6 must be 16 bytes, got ${value.length}`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const o = reserve(sink, 16);
|
|
24
|
+
sink.buf.set(value, o);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Parse one dotted-quad field into a 0..255 octet, throwing on anything else. */
|
|
28
|
+
function parseOctet(part: string): number {
|
|
29
|
+
const octet = Number(part);
|
|
30
|
+
if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
|
|
31
|
+
throw new RangeError(
|
|
32
|
+
`RowBinary: invalid IPv4 octet ${JSON.stringify(part)}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return octet;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse a dotted-quad IPv4 string into the raw 32-bit value — the inverse of
|
|
40
|
+
* `formatIPv4`. `"1.2.3.4"` -> `0x01020304`. Pair with {@link writeIPv4}.
|
|
41
|
+
*
|
|
42
|
+
* The four octets are read explicitly rather than in a loop (only ever four);
|
|
43
|
+
* `>>> 0` coerces the assembled value back to an unsigned 32-bit number.
|
|
44
|
+
*/
|
|
45
|
+
export function parseIPv4(text: string): number {
|
|
46
|
+
const parts = text.split(".");
|
|
47
|
+
if (parts.length !== 4) {
|
|
48
|
+
throw new RangeError(
|
|
49
|
+
`RowBinary: invalid IPv4 string ${JSON.stringify(text)}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const a = parseOctet(parts[0]!);
|
|
53
|
+
const b = parseOctet(parts[1]!);
|
|
54
|
+
const c = parseOctet(parts[2]!);
|
|
55
|
+
const d = parseOctet(parts[3]!);
|
|
56
|
+
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse an IPv6 string into its raw 16 bytes (network order) — the inverse of
|
|
61
|
+
* `formatIPv6`, accepting the canonical forms it emits (`::` zero-run compression
|
|
62
|
+
* and the `::ffff:a.b.c.d` IPv4-mapped form) as well as the fully expanded form.
|
|
63
|
+
* Pair with {@link writeIPv6}.
|
|
64
|
+
*
|
|
65
|
+
* Rejects malformed input (throws): a parse-time helper validates because it
|
|
66
|
+
* must produce exactly 16 well-defined bytes and there is no hot loop or server
|
|
67
|
+
* to fall back on — see the "No defensive validation" exceptions in AGENTS.md.
|
|
68
|
+
*/
|
|
69
|
+
export function parseIPv6(text: string): Buffer {
|
|
70
|
+
const halves = text.split("::");
|
|
71
|
+
if (halves.length > 2) {
|
|
72
|
+
throw new RangeError(
|
|
73
|
+
`RowBinary: invalid IPv6 string ${JSON.stringify(text)}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Expand a colon-separated side into 16-bit groups, splitting a trailing
|
|
78
|
+
// embedded IPv4 (a.b.c.d) into its two groups.
|
|
79
|
+
const toGroups = (side: string): number[] => {
|
|
80
|
+
if (side === "") return [];
|
|
81
|
+
const groups: number[] = [];
|
|
82
|
+
for (const part of side.split(":")) {
|
|
83
|
+
if (part.includes(".")) {
|
|
84
|
+
const v4 = parseIPv4(part);
|
|
85
|
+
groups.push((v4 >>> 16) & 0xffff, v4 & 0xffff);
|
|
86
|
+
} else {
|
|
87
|
+
// 1–4 hex digits only. Parsing strictly rather than `parseInt(...) &
|
|
88
|
+
// 0xffff` rejects malformed groups instead of silently turning them
|
|
89
|
+
// into 0 (`NaN & 0xffff`) or wrapping negatives like "-1" to 0xffff.
|
|
90
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(part)) {
|
|
91
|
+
throw new RangeError(
|
|
92
|
+
`RowBinary: invalid IPv6 group ${JSON.stringify(part)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
groups.push(parseInt(part, 16));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return groups;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const head = toGroups(halves[0]!);
|
|
102
|
+
const tail = halves.length === 2 ? toGroups(halves[1]!) : [];
|
|
103
|
+
const groups =
|
|
104
|
+
halves.length === 2
|
|
105
|
+
? [...head, ...new Array(8 - head.length - tail.length).fill(0), ...tail]
|
|
106
|
+
: head;
|
|
107
|
+
if (groups.length !== 8) {
|
|
108
|
+
throw new RangeError(
|
|
109
|
+
`RowBinary: invalid IPv6 string ${JSON.stringify(text)}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
// SAFE: allocUnsafe — the loop below writes all 16 bytes (out[0..15] for the
|
|
113
|
+
// 8 groups), and `out` is only allocated here, past every throw, so an
|
|
114
|
+
// uninitialized buffer is never returned.
|
|
115
|
+
const out = Buffer.allocUnsafe(16);
|
|
116
|
+
for (let i = 0; i < 8; i++) {
|
|
117
|
+
out[2 * i] = (groups[i]! >>> 8) & 0xff;
|
|
118
|
+
out[2 * i + 1] = groups[i]! & 0xff;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Writer } from "./core.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `LowCardinality(T)` is TRANSPARENT in RowBinary (no dictionary layer on the
|
|
5
|
+
* wire), so there is nothing extra to encode: use `T`'s own writer directly.
|
|
6
|
+
* This identity combinator mirrors `readLowCardinality` and returns the inner
|
|
7
|
+
* writer unchanged:
|
|
8
|
+
*
|
|
9
|
+
* writeLowCardinality(writeString) === writeString
|
|
10
|
+
*/
|
|
11
|
+
export const writeLowCardinality = <T>(writeValue: Writer<T>): Writer<T> =>
|
|
12
|
+
writeValue;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Writer } from "./core.js";
|
|
2
|
+
import { writeArray, writeTupleNamed } from "./composite.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Inverse of `readNested`: `Nested(...)` has no wire format of its own, so for the
|
|
6
|
+
* `flatten_nested = 0` shape it is simply `Array(Tuple(a T1, b T2, …))`. This thin
|
|
7
|
+
* alias composes the existing array + named-tuple writers, mirroring the reader:
|
|
8
|
+
*
|
|
9
|
+
* writeNested({ a: writeUInt8, b: writeString })
|
|
10
|
+
* === writeArray(writeTupleNamed({ a: writeUInt8, b: writeString }))
|
|
11
|
+
*
|
|
12
|
+
* When generating code, prefer inlining (monomorphize the array + tuple) over this
|
|
13
|
+
* generic composition.
|
|
14
|
+
*/
|
|
15
|
+
export const writeNested = <T extends Record<string, unknown>>(writers: {
|
|
16
|
+
[K in keyof T]: Writer<T[K]>;
|
|
17
|
+
}): Writer<readonly T[]> => writeArray(writeTupleNamed(writers));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Writer } from "./core.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inverse of `readNothing`: a `Nothing` value is NEVER written either. It only
|
|
5
|
+
* appears wrapped, where the wrapper short-circuits before reaching it:
|
|
6
|
+
*
|
|
7
|
+
* writeArray(writeNothing) // only the empty array [] (length 0x00)
|
|
8
|
+
* writeNullable(writeNothing) // only null (lone flag byte 0x01)
|
|
9
|
+
*
|
|
10
|
+
* In both cases the element/inner writer is not invoked. This writer throws if it
|
|
11
|
+
* is ever actually called, which would mean a `Nothing` writer was placed where a
|
|
12
|
+
* real element/inner type was expected.
|
|
13
|
+
*/
|
|
14
|
+
export const writeNothing: Writer<never> = () => {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"RowBinary: Nothing is zero-width and is never encoded — it only appears as " +
|
|
17
|
+
"an empty Array(Nothing) or a NULL Nullable(Nothing), where the inner writer " +
|
|
18
|
+
"is not called. Reaching here means a Nothing writer was wired where a real " +
|
|
19
|
+
"element/inner type was expected.",
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { channel } from "node:diagnostics_channel";
|
|
2
|
+
import { BufferFull, Sink, type Writer } from "./core.js";
|
|
3
|
+
|
|
4
|
+
/** Default sink size when `writeRows` isn't given one — a typical flush chunk. */
|
|
5
|
+
const DEFAULT_BUFFER_SIZE = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Payload of {@link FLUSH_CHANNEL_NAME}, published once per buffer `writeRows`
|
|
9
|
+
* flushes — the hook for buffer-capacity-utilization metrics (e.g. an OTEL
|
|
10
|
+
* `used_bytes` / `capacity_bytes` counter pair, divided in the backend for a
|
|
11
|
+
* byte-weighted average fill). Identity (table, `query_id`, …) is deliberately
|
|
12
|
+
* NOT here: carry it in `AsyncLocalStorage` and read it in the subscriber, which
|
|
13
|
+
* runs synchronously on the publisher's call stack, so its async context is live.
|
|
14
|
+
*/
|
|
15
|
+
export interface WriteRowsFlush {
|
|
16
|
+
/** Bytes actually written into the buffer just flushed (`<= capacityBytes`). */
|
|
17
|
+
usedBytes: number;
|
|
18
|
+
/** That buffer's capacity; doubles from `bufferSize` to fit an oversized row. */
|
|
19
|
+
capacityBytes: number;
|
|
20
|
+
/**
|
|
21
|
+
* The configured initial buffer size for this run. `capacityBytes > bufferSize`
|
|
22
|
+
* means the buffer had to grow to fit an oversized row, and `usedBytes /
|
|
23
|
+
* bufferSize` is the overflow magnitude — the signal that `bufferSize` is too
|
|
24
|
+
* small. (Growth is sticky: once grown, every later flush in the run reports the
|
|
25
|
+
* larger `capacityBytes`, so compare against `bufferSize`, not a prior capacity.)
|
|
26
|
+
*/
|
|
27
|
+
bufferSize: number;
|
|
28
|
+
/** Why it flushed: `"full"` mid-stream (next row overflowed) or `"end"` (rows ran out). */
|
|
29
|
+
reason: "full" | "end";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `node:diagnostics_channel` name {@link writeRows} publishes a {@link WriteRowsFlush}
|
|
34
|
+
* once per flushed buffer. Subscribe to observe buffer-capacity utilization; with no
|
|
35
|
+
* subscriber `writeRows` skips the publish entirely (a single `hasSubscribers`
|
|
36
|
+
* check per buffer, off the per-row path), so it's free when unused.
|
|
37
|
+
*/
|
|
38
|
+
export const FLUSH_CHANNEL_NAME = "@clickhouse/rowbinary:writeRows.flush";
|
|
39
|
+
|
|
40
|
+
/** Created once — `channel()` is idempotent (same name → same object). */
|
|
41
|
+
const flushChannel = channel(FLUSH_CHANNEL_NAME);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Drive `writeRow` over every row of an iterable into a plain `RowBinary` payload
|
|
45
|
+
* — the encode mirror of `readRows`. Rows are concatenated with NO count, length
|
|
46
|
+
* prefix, or delimiter (just as the reader expects), so `writeRow` must emit
|
|
47
|
+
* EXACTLY one row's bytes. Curried: `writeRows(writeRow)` returns the driver.
|
|
48
|
+
*
|
|
49
|
+
* Returns a GENERATOR rather than a `Writer<readonly T[]>` on purpose. A `Sink`
|
|
50
|
+
* wraps a FIXED-length buffer, so a large (or unbounded) result won't fit in one
|
|
51
|
+
* pass — when a row would overflow, `writeRow` throws {@link BufferFull} from
|
|
52
|
+
* `reserve`, and a plain `(sink, rows) => void` would let that escape mid-row,
|
|
53
|
+
* leaving the caller unable to tell how many rows actually made it in. Instead
|
|
54
|
+
* this owns the sink: it catches `BufferFull`, rewinds to the last COMPLETE row
|
|
55
|
+
* boundary (never a half-written row), `yield`s that batch, and starts a FRESH
|
|
56
|
+
* `Sink` for the rows that didn't fit. Because each flush gets its own buffer,
|
|
57
|
+
* every yielded `Buffer` stays valid after the generator resumes — safe to retain
|
|
58
|
+
* or hand to an async sink, no copy needed. The caller supplies a `bufferSize`,
|
|
59
|
+
* not a sink, and `rows` is an `Iterable<T>` (not a fixed array), so the same
|
|
60
|
+
* driver handles a future infinite/streaming row source unchanged.
|
|
61
|
+
*
|
|
62
|
+
* The driver loop is just a `for...of` — the generator yields each batch of whole
|
|
63
|
+
* rows whenever it stops accumulating (on overflow, or when the rows run out), so
|
|
64
|
+
* the final batch comes through the same channel and there's nothing to flush
|
|
65
|
+
* afterwards:
|
|
66
|
+
*
|
|
67
|
+
* const drive = writeRows(writeRow);
|
|
68
|
+
* for (const chunk of drive(rows, 64 * 1024)) send(chunk);
|
|
69
|
+
*
|
|
70
|
+
* OVERSIZED ROWS: when a single row won't fit even an empty buffer, the buffer is
|
|
71
|
+
* GROWN (doubled) and the row retried — never dropped, never thrown. The first
|
|
72
|
+
* time this happens `writeRows` `console.warn`s ONCE (the buffer may keep doubling
|
|
73
|
+
* after that) so an under-sized `bufferSize` or a pathologically large row doesn't
|
|
74
|
+
* pass unnoticed.
|
|
75
|
+
*
|
|
76
|
+
* METRICS: each flushed buffer is published as a {@link WriteRowsFlush} on the
|
|
77
|
+
* {@link FLUSH_CHANNEL_NAME} diagnostics channel — wire it to a utilization metric.
|
|
78
|
+
* No subscriber means no publish (one `hasSubscribers` check per buffer).
|
|
79
|
+
*
|
|
80
|
+
* When generating code, inline the per-column writes into the loop body,
|
|
81
|
+
* mirroring the reader.
|
|
82
|
+
*/
|
|
83
|
+
export function writeRows<T>(
|
|
84
|
+
writeRow: Writer<T>,
|
|
85
|
+
): (rows: Iterable<T>, bufferSize?: number) => Generator<Buffer, void, void> {
|
|
86
|
+
return function* (rows, bufferSize = DEFAULT_BUFFER_SIZE) {
|
|
87
|
+
if (!Number.isSafeInteger(bufferSize) || bufferSize <= 0)
|
|
88
|
+
// Guard the growth loop: a 0 / NaN / negative size makes the first row
|
|
89
|
+
// overflow forever (`size *= 2` never escapes 0/NaN), so fail fast instead.
|
|
90
|
+
throw new RangeError(
|
|
91
|
+
`RowBinary writeRows: bufferSize must be a positive integer, got ${bufferSize}`,
|
|
92
|
+
);
|
|
93
|
+
let size = bufferSize;
|
|
94
|
+
let warned = false;
|
|
95
|
+
let sink = new Sink(Buffer.allocUnsafe(size));
|
|
96
|
+
for (const row of rows) {
|
|
97
|
+
while (true) {
|
|
98
|
+
const committed = sink.pos; // start of this row — the last clean boundary
|
|
99
|
+
try {
|
|
100
|
+
writeRow(sink, row);
|
|
101
|
+
break; // row written cleanly — on to the next
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (e !== BufferFull) throw e;
|
|
104
|
+
if (committed === 0) {
|
|
105
|
+
// An empty buffer couldn't hold even this one row: double it and
|
|
106
|
+
// retry the SAME row — never drop it. Nothing was written, so the
|
|
107
|
+
// discarded sink had no bytes to flush.
|
|
108
|
+
size *= 2;
|
|
109
|
+
if (!warned) {
|
|
110
|
+
warned = true;
|
|
111
|
+
console.warn(
|
|
112
|
+
`RowBinary writeRows: a row didn't fit bufferSize=${bufferSize}; ` +
|
|
113
|
+
`growing the buffer beyond it (possibly more than once). Raise bufferSize.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
sink = new Sink(Buffer.allocUnsafe(size));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
sink.pos = committed; // drop the partially written row, then flush
|
|
120
|
+
if (flushChannel.hasSubscribers)
|
|
121
|
+
flushChannel.publish({
|
|
122
|
+
usedBytes: committed,
|
|
123
|
+
capacityBytes: size,
|
|
124
|
+
bufferSize,
|
|
125
|
+
reason: "full",
|
|
126
|
+
} satisfies WriteRowsFlush);
|
|
127
|
+
yield sink.bytes();
|
|
128
|
+
sink = new Sink(Buffer.allocUnsafe(size)); // fresh buffer; retry the row
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (sink.pos > 0) {
|
|
133
|
+
// the final batch
|
|
134
|
+
if (flushChannel.hasSubscribers)
|
|
135
|
+
flushChannel.publish({
|
|
136
|
+
usedBytes: sink.pos,
|
|
137
|
+
capacityBytes: size,
|
|
138
|
+
bufferSize,
|
|
139
|
+
reason: "end",
|
|
140
|
+
} satisfies WriteRowsFlush);
|
|
141
|
+
yield sink.bytes();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Writer } from "./core.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `SimpleAggregateFunction(func, T)` is TRANSPARENT in RowBinary — the column
|
|
5
|
+
* holds a finished value of `T` — so encode the inner `T` directly. Identity
|
|
6
|
+
* combinator mirroring `readSimpleAggregateFunction`:
|
|
7
|
+
*
|
|
8
|
+
* writeSimpleAggregateFunction(writeUInt64) === writeUInt64
|
|
9
|
+
*/
|
|
10
|
+
export const writeSimpleAggregateFunction = <T>(
|
|
11
|
+
writeValue: Writer<T>,
|
|
12
|
+
): Writer<T> => writeValue;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type Writer, Sink, reserve } from "./core.js";
|
|
2
|
+
import { writeUVarint } from "./varint.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Write a `String` from a JS string: a varint byte-length prefix followed by the
|
|
6
|
+
* UTF-8 bytes. The inverse of `readString`.
|
|
7
|
+
*
|
|
8
|
+
* Split from {@link writeStringBytes} (rather than one function branching on the
|
|
9
|
+
* argument type) so each is monomorphic — V8 keeps a single shape per call site
|
|
10
|
+
* instead of going megamorphic on a `string | Uint8Array` parameter.
|
|
11
|
+
*/
|
|
12
|
+
export function writeString(sink: Sink, value: string): void {
|
|
13
|
+
const len = Buffer.byteLength(value, "utf8");
|
|
14
|
+
writeUVarint(sink, len);
|
|
15
|
+
const o = reserve(sink, len);
|
|
16
|
+
sink.buf.write(value, o, len, "utf8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write a `String` from raw bytes: a varint byte-length prefix followed by the
|
|
21
|
+
* bytes verbatim. Use this for ClickHouse `String` columns holding arbitrary
|
|
22
|
+
* (non-UTF-8) bytes, mirroring the reader's note that `String` is not guaranteed
|
|
23
|
+
* UTF-8. The bytes counterpart of {@link writeString} (see the note there on why
|
|
24
|
+
* they are kept as two monomorphic functions).
|
|
25
|
+
*/
|
|
26
|
+
export function writeStringBytes(sink: Sink, value: Uint8Array): void {
|
|
27
|
+
writeUVarint(sink, value.length);
|
|
28
|
+
const o = reserve(sink, value.length);
|
|
29
|
+
sink.buf.set(value, o);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Write a `FixedString(N)` from a string: exactly `size` bytes, UTF-8 encoded and
|
|
34
|
+
* right-padded with NUL bytes (`\x00`) to `size`. Curried: `writeFixedString(N)`
|
|
35
|
+
* returns the writer. The inverse of `readFixedString` — which preserves the
|
|
36
|
+
* trailing NULs, so re-encoding a value it produced is byte-exact.
|
|
37
|
+
*
|
|
38
|
+
* Throws if the UTF-8 encoding exceeds `size` bytes (it would not fit the column).
|
|
39
|
+
*/
|
|
40
|
+
export function writeFixedString(size: number): Writer<string> {
|
|
41
|
+
return (sink, value) => {
|
|
42
|
+
const len = Buffer.byteLength(value, "utf8");
|
|
43
|
+
if (len > size) {
|
|
44
|
+
throw new RangeError(
|
|
45
|
+
`RowBinary: FixedString value is ${len} bytes, exceeds FixedString(${size})`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const o = reserve(sink, size);
|
|
49
|
+
sink.buf.write(value, o, len, "utf8");
|
|
50
|
+
// The sink's buffer may be uninitialized (allocUnsafe); zero the padding.
|
|
51
|
+
// POTENTIAL OPTIMIZATION: drop this fill when the buffer is known to be
|
|
52
|
+
// zero-initialized. Kept by default — relying on a zeroed buffer is a footgun
|
|
53
|
+
// (a pooled/reused sink would leak stale bytes into the column).
|
|
54
|
+
sink.buf.fill(0, o + len, o + size);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Write a `FixedString(N)` from raw bytes: exactly `size` bytes, the value copied
|
|
60
|
+
* verbatim and right-padded with NUL bytes if shorter. Curried:
|
|
61
|
+
* `writeFixedStringBytes(N)` returns the writer. The inverse of
|
|
62
|
+
* `readFixedStringBytes` (binary columns).
|
|
63
|
+
*
|
|
64
|
+
* Throws if the value is longer than `size`.
|
|
65
|
+
*/
|
|
66
|
+
export function writeFixedStringBytes(size: number): Writer<Uint8Array> {
|
|
67
|
+
return (sink, value) => {
|
|
68
|
+
if (value.length > size) {
|
|
69
|
+
throw new RangeError(
|
|
70
|
+
`RowBinary: FixedString value is ${value.length} bytes, exceeds FixedString(${size})`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const o = reserve(sink, size);
|
|
74
|
+
sink.buf.set(value, o);
|
|
75
|
+
sink.buf.fill(0, o + value.length, o + size);
|
|
76
|
+
};
|
|
77
|
+
}
|