Setting the file. One moment. Framing Nested Test · Clickhouse Js Node Rowbinary · ClickHouse/agent-skills · Skills Docs(opens in a new tab)
tests/framing-nested.test.ts
TypeScript·344 lines·12 KB
import
{ readDynamic }
from
"../src/readers/dynamic.js"
;
12import { readInt32, readUInt8 } from "../src/readers/integers.js";
13import { readJSON } from "../src/readers/json.js";
14import { readString } from "../src/readers/strings.js";
15
16/**
17 * Framing tests for NESTED self-describing / variable-length types — the place
18 * where a buggy reader is most likely to desync. Each value combines two types
19 * that carry an internal type description or a variable length (`Dynamic`,
20 * `Variant`, `JSON`, `Array`, `Map`, `Tuple`, `Nullable`), so the boundary
21 * between inner readers is "blurry": a reader that miscounts one element's bytes
22 * silently shifts everything after it.
23 *
24 * Same harness as framing.test.ts: the value sits as the MIDDLE column between
25 * two distinct Int32 sentinels `i32(LEAD), X, i32(TRAIL)`. Reading TRAIL back
26 * correctly is only possible if X consumed EXACTLY its bytes. The adjacency
27 * cases (Tuple of two variable things, NULL/empty inners) are the sharpest.
28 */
29const LEAD = 123456789;
30const TRAIL = 987654321;
31
32// Every experimental / suspicious flag on, as in framing.test.ts.
33const SETTINGS = [
34 "enable_time_time64_type = 1",
35 "allow_experimental_variant_type = 1",
36 "allow_suspicious_variant_types = 1",
37 "allow_experimental_dynamic_type = 1",
38 "allow_experimental_json_type = 1",
39 "enable_json_type = 1",
40 "allow_experimental_qbit_type = 1",
41 "allow_suspicious_low_cardinality_types = 1",
42].join(", ");
43
44async function framed(expr: string): Promise<Cursor> {
45 const sql =
46 `SELECT toInt32(${LEAD}) AS a, ${expr} AS x, toInt32(${TRAIL}) AS b` +
47 ` SETTINGS ${SETTINGS} FORMAT RowBinary`;
48 return new Cursor(await query(sql));
49}
50
51// Shared inner readers, written once so the nesting reads cleanly below.
52const u8 = readUInt8;
53const str = readString;
54// Variant(UInt8, String) sorts to [String(0), UInt8(1)].
55const variantU8Str = readVariant([readString, readUInt8]);
56
57describe("framing (nested): two self-describing / variable types — boundaries stay exact", () => {
58 describe("Array of a variable / self-describing inner", () => {
59 it("Array(Variant(UInt8, String)) with a NULL element", async () => {
60 const r = await framed(
61 "[42::Variant(UInt8, String), 'hi'::Variant(UInt8, String), NULL::Variant(UInt8, String)]::Array(Variant(UInt8, String))",
62 );
63 expect(readInt32(r)).toBe(LEAD);
64 expect(readArray(variantU8Str)(r)).toEqual([42, "hi", null]);
65 expect(readInt32(r)).toBe(TRAIL);
66 });
67
68 it("Array(Dynamic)", async () => {
69 const r = await framed("['x'::Dynamic, 'y'::Dynamic]::Array(Dynamic)");
70 expect(readInt32(r)).toBe(LEAD);
71 expect(readArray(readDynamic)(r)).toEqual(["x", "y"]);
72 expect(readInt32(r)).toBe(TRAIL);
73 });
74
75 it("Array(Nullable(String)) with a hole", async () => {
76 const r = await framed("['a', NULL, 'b']::Array(Nullable(String))");
77 expect(readInt32(r)).toBe(LEAD);
78 expect(readArray(readNullable(str))(r)).toEqual(["a", null, "b"]);
79 expect(readInt32(r)).toBe(TRAIL);
80 });
81
82 it("Array(Array(String)) — variable-length inner arrays", async () => {
83 const r = await framed("[['a', 'b'], ['c']]::Array(Array(String))");
84 expect(readInt32(r)).toBe(LEAD);
85 expect(readArray(readArray(str))(r)).toEqual([["a", "b"], ["c"]]);
86 expect(readInt32(r)).toBe(TRAIL);
87 });
88
89 it("Array(Map(String, UInt8))", async () => {
90 const r = await framed(
91 "[map('a', 1), map('b', 2)]::Array(Map(String, UInt8))",
92 );
93 expect(readInt32(r)).toBe(LEAD);
94 expect(readArray(readMap(str, u8))(r)).toEqual([
95 new Map([["a", 1]]),
96 new Map([["b", 2]]),
97 ]);
98 expect(readInt32(r)).toBe(TRAIL);
99 });
100
101 it("Array(Tuple(UInt8, String))", async () => {
102 const r = await framed(
103 "[(1, 'x'), (2, 'y')]::Array(Tuple(UInt8, String))",
104 );
105 expect(readInt32(r)).toBe(LEAD);
106 expect(readArray(readTuple([u8, str]))(r)).toEqual([
107 [1, "x"],
108 [2, "y"],
109 ]);
110 expect(readInt32(r)).toBe(TRAIL);
111 });
112 });
113
114 describe("Map(String, variable value)", () => {
115 it("Map(String, Variant(UInt8, String))", async () => {
116 const r = await framed(
117 "map('a', 42::Variant(UInt8, String), 'b', 'hi'::Variant(UInt8, String))",
118 );
119 expect(readInt32(r)).toBe(LEAD);
120 expect(readMap(str, variantU8Str)(r)).toEqual(
121 new Map<string, unknown>([
122 ["a", 42],
123 ["b", "hi"],
124 ]),
125 );
126 expect(readInt32(r)).toBe(TRAIL);
127 });
128
129 it("Map(String, Dynamic) with mixed value types", async () => {
130 const r = await framed(
131 "map('a', toInt32(7)::Dynamic, 'b', 'hi'::Dynamic)::Map(String, Dynamic)",
132 );
133 expect(readInt32(r)).toBe(LEAD);
134 expect(readMap(str, readDynamic)(r)).toEqual(
135 new Map<string, unknown>([
136 ["a", 7],
137 ["b", "hi"],
138 ]),
139 );
140 expect(readInt32(r)).toBe(TRAIL);
141 });
142
143 it("Map(String, Array(UInt8))", async () => {
144 const r = await framed(
145 "map('x', [1, 2], 'y', [3])::Map(String, Array(UInt8))",
146 );
147 expect(readInt32(r)).toBe(LEAD);
148 expect(readMap(str, readArray(u8))(r)).toEqual(
149 new Map([
150 ["x", [1, 2]],
151 ["y", [3]],
152 ]),
153 );
154 expect(readInt32(r)).toBe(TRAIL);
155 });
156
157 it("Map(String, Nullable(UInt8)) with a NULL value", async () => {
158 const r = await framed(
159 "map('a', 1, 'b', NULL)::Map(String, Nullable(UInt8))",
160 );
161 expect(readInt32(r)).toBe(LEAD);
162 expect(readMap(str, readNullable(u8))(r)).toEqual(
163 new Map<string, number | null>([
164 ["a", 1],
165 ["b", null],
166 ]),
167 );
168 expect(readInt32(r)).toBe(TRAIL);
169 });
170 });
171
172 describe("Tuple adjacency — two variable things back-to-back", () => {
173 it("Tuple(Dynamic, Dynamic)", async () => {
174 const r = await framed(
175 "(toInt32(7)::Dynamic, 'hi'::Dynamic)::Tuple(Dynamic, Dynamic)",
176 );
177 expect(readInt32(r)).toBe(LEAD);
178 expect(readTuple([readDynamic, readDynamic])(r)).toEqual([7, "hi"]);
179 expect(readInt32(r)).toBe(TRAIL);
180 });
181
182 it("Tuple(Dynamic, Dynamic) — first is a 1-byte NULL", async () => {
183 const r = await framed(
184 "(NULL::Dynamic, toInt32(9)::Dynamic)::Tuple(Dynamic, Dynamic)",
185 );
186 expect(readInt32(r)).toBe(LEAD);
187 expect(readTuple([readDynamic, readDynamic])(r)).toEqual([null, 9]);
188 expect(readInt32(r)).toBe(TRAIL);
189 });
190
191 it("Tuple(Variant, Variant)", async () => {
192 const r = await framed(
193 "(42::Variant(UInt8, String), 'x'::Variant(UInt8, String))::Tuple(Variant(UInt8, String), Variant(UInt8, String))",
194 );
195 expect(readInt32(r)).toBe(LEAD);
196 expect(readTuple([variantU8Str, variantU8Str])(r)).toEqual([42, "x"]);
197 expect(readInt32(r)).toBe(TRAIL);
198 });
199
200 it("Tuple(Array(UInt8), Array(UInt8)) — two adjacent length-prefixed arrays", async () => {
201 const r = await framed(
202 "([1, 2], [3, 4])::Tuple(Array(UInt8), Array(UInt8))",
203 );
204 expect(readInt32(r)).toBe(LEAD);
205 expect(readTuple([readArray(u8), readArray(u8)])(r)).toEqual([
206 [1, 2],
207 [3, 4],
208 ]);
209 expect(readInt32(r)).toBe(TRAIL);
210 });
211
212 it("Tuple(Array(UInt8), String)", async () => {
213 const r = await framed("([1, 2], 'x')::Tuple(Array(UInt8), String)");
214 expect(readInt32(r)).toBe(LEAD);
215 expect(readTuple([readArray(u8), str])(r)).toEqual([[1, 2], "x"]);
216 expect(readInt32(r)).toBe(TRAIL);
217 });
218
219 it("Tuple(Nullable(String), Nullable(String)) — NULL then value", async () => {
220 const r = await framed(
221 "(NULL, 'x')::Tuple(Nullable(String), Nullable(String))",
222 );
223 expect(readInt32(r)).toBe(LEAD);
224 expect(readTuple([readNullable(str), readNullable(str)])(r)).toEqual([
225 null,
226 "x",
227 ]);
228 expect(readInt32(r)).toBe(TRAIL);
229 });
230 });
231
232 describe("Variant whose active alternative is variable-length", () => {
233 it("Variant(Array(UInt8), String) holding the Array (discriminant 0)", async () => {
234 const r = await framed("[1, 2, 3]::Variant(Array(UInt8), String)");
235 expect(readInt32(r)).toBe(LEAD);
236 // sorted [Array(UInt8)(0), String(1)]
237 expect(readVariant([readArray(u8), readString])(r)).toEqual([1, 2, 3]);
238 expect(readInt32(r)).toBe(TRAIL);
239 });
240
241 it("Variant(Array(UInt8), String) holding the String (discriminant 1)", async () => {
242 const r = await framed("'hi'::Variant(Array(UInt8), String)");
243 expect(readInt32(r)).toBe(LEAD);
244 expect(readVariant([readArray(u8), readString])(r)).toBe("hi");
245 expect(readInt32(r)).toBe(TRAIL);
246 });
247
248 it("Variant(Map(String, UInt8), UInt8) holding the Map (discriminant 0)", async () => {
249 const r = await framed("map('a', 1)::Variant(Map(String, UInt8), UInt8)");
250 expect(readInt32(r)).toBe(LEAD);
251 // sorted [Map(String, UInt8)(0), UInt8(1)]
252 expect(readVariant([readMap(str, u8), readUInt8])(r)).toEqual(
253 new Map([["a", 1]]),
254 );
255 expect(readInt32(r)).toBe(TRAIL);
256 });
257
258 it("Variant(Tuple(UInt8, String), UInt8) holding the Tuple (discriminant 0)", async () => {
259 const r = await framed("(1, 'x')::Variant(Tuple(UInt8, String), UInt8)");
260 expect(readInt32(r)).toBe(LEAD);
261 // sorted [Tuple(UInt8, String)(0), UInt8(1)]
262 expect(readVariant([readTuple([u8, str]), readUInt8])(r)).toEqual([
263 1,
264 "x",
265 ]);
266 expect(readInt32(r)).toBe(TRAIL);
267 });
268 });
269
270 describe("Dynamic wrapping a nested self-describing type", () => {
271 it("Dynamic(Array(Variant(UInt8, String)))", async () => {
272 const r = await framed(
273 "[42::Variant(UInt8, String), 'hi'::Variant(UInt8, String)]::Array(Variant(UInt8, String))::Dynamic",
274 );
275 expect(readInt32(r)).toBe(LEAD);
276 expect(readDynamic(r)).toEqual([42, "hi"]);
277 expect(readInt32(r)).toBe(TRAIL);
278 });
279
280 it("Dynamic(Map(String, Variant(UInt8, String)))", async () => {
281 const r = await framed(
282 "map('a', 42::Variant(UInt8, String))::Map(String, Variant(UInt8, String))::Dynamic",
283 );
284 expect(readInt32(r)).toBe(LEAD);
285 expect(readDynamic(r)).toEqual(new Map([["a", 42]]));
286 expect(readInt32(r)).toBe(TRAIL);
287 });
288
289 it("Dynamic(Array(Dynamic))", async () => {
290 const r = await framed(
291 "['x'::Dynamic, 'y'::Dynamic]::Array(Dynamic)::Dynamic",
292 );
293 expect(readInt32(r)).toBe(LEAD);
294 expect(readDynamic(r)).toEqual(["x", "y"]);
295 expect(readInt32(r)).toBe(TRAIL);
296 });
297
298 it("Dynamic(JSON)", async () => {
299 const r = await framed(`'{"a":1}'::JSON::Dynamic`);
300 expect(readInt32(r)).toBe(LEAD);
301 expect(readDynamic(r)).toEqual(new Map([["a", 1n]]));
302 expect(readInt32(r)).toBe(TRAIL);
303 });
304
305 it("Dynamic(Nested)", async () => {
306 const r = await framed(
307 "[(1, 'x'), (2, 'y')]::Nested(a UInt8, b String)::Dynamic",
308 );
309 expect(readInt32(r)).toBe(LEAD);
310 expect(readDynamic(r)).toEqual([
311 { a: 1, b: "x" },
312 { a: 2, b: "y" },
313 ]);
314 expect(readInt32(r)).toBe(TRAIL);
315 });
316 });
317
318 describe("JSON with several self-describing paths", () => {
319 it("mixed value types (Int64, String, Array)", async () => {
320 const r = await framed(`'{"i":1,"s":"hi","arr":[1,2]}'::JSON`);
321 expect(readInt32(r)).toBe(LEAD);
322 expect(readJSON(r)).toEqual(
323 new Map<string, unknown>([
324 ["arr", [1n, 2n]],
325 ["s", "hi"],
326 ["i", 1n],
327 ]),
328 );
329 expect(readInt32(r)).toBe(TRAIL);
330 });
331
332 it("nested object flattened to dotted paths", async () => {
333 const r = await framed(`'{"a":{"b":2},"c":3}'::JSON`);
334 expect(readInt32(r)).toBe(LEAD);
335 expect(readJSON(r)).toEqual(
336 new Map<string, unknown>([
337 ["a.b", 2n],
338 ["c", 3n],
339 ]),
340 );
341 expect(readInt32(r)).toBe(TRAIL);
342 });
343 });
344});