Setting the file. One moment.
Row Binary With Names And Types Test · Clickhouse Js Node Rowbinary · ClickHouse/agent-skills · Skills Docs
ContentsBack to the top of the page tests/ rowBinaryWithNamesAndTypes.test.ts
TypeScript · 588 lines · 19 KB
{ RowBinaryTypeError }
from
"../src/readers/compile.js"
;
11
12 /** Raw value bytes for one expression (`FORMAT RowBinary`, no header). */
13 async function rowBinary ( expr : string ) : Promise < Cursor > {
14 return new Cursor ( await query ( `SELECT ${ expr } FORMAT RowBinary` ));
15 }
16
17 /** A full `RowBinaryWithNamesAndTypes` response (header + rows) as a cursor. */
18 async function withNamesAndTypes ( select : string ) : Promise < Cursor > {
19 return new Cursor ( await query ( `${ select } FORMAT RowBinaryWithNamesAndTypes` ));
20 }
21
22 /**
23 * Compile a `SELECT ... FORMAT RowBinaryWithNamesAndTypes` response and decode
24 * every row. Asserts the stream is consumed EXACTLY — the strongest single
25 * check that the fold picked the right reader for each column and framed the
26 * bytes correctly. Returns the header type strings and the decoded rows.
27 */
28 async function decode (
29 select : string ,
30 ) : Promise <{ types : string []; rows : Row [] }> {
31 const s = await withNamesAndTypes (select);
32 const compiled = compileRowBinaryWithNamesAndTypes (s);
33 const rows = compiled. readRows (s);
34 expect (s.pos). toBe (s.buf. length ); // consumed exactly — no under/over-read
35 return { types: compiled.types, rows };
36 }
37
38 /** Decode a single-column, single-row `... AS v` SELECT down to its value. */
39 async function value ( select : string ) : Promise < unknown > {
40 const { rows } = await decode (select);
41 expect (rows). toHaveLength ( 1 );
42 return rows[ 0 ] ! .v;
43 }
44
45 describe ( "typeStringToReader (AST -> combinator fold)" , () => {
46 it ( "folds a nested composite type into a working reader" , async () => {
47 const s = await rowBinary ( "CAST([1, NULL, 3] AS Array(Nullable(UInt32)))" );
48 expect ( typeStringToReader ( "Array(Nullable(UInt32))" )(s)). toEqual ([
49 1 ,
50 null ,
51 3 ,
52 ]);
53 });
54
55 it ( "throws a typed RowBinaryTypeError (with typeString + position) for an unsupported type" , () => {
56 const type = "AggregateFunction(sum, UInt64)" ;
57 let err : unknown ;
58 try {
59 typeStringToReader (type);
60 } catch (e) {
61 err = e;
62 }
63 expect (err). toBeInstanceOf (RowBinaryTypeError);
64 const typed = err as RowBinaryTypeError ;
65 expect (typed.message). toMatch ( /cannot compile type/ );
66 expect (typed.typeString). toBe (type);
67 expect ( typeof typed.position). toBe ( "number" );
68 });
69 });
70
71 // The corpus below mirrors the parser's test/cases.txt, one it() per type or
72 // combination. Each value is the actual decode of a server-produced stream.
73 describe ( "scalars" , () => {
74 it ( "UInt8" , async () => {
75 expect ( await value ( "SELECT toUInt8(255) AS v" )). toEqual ( 255 );
76 });
77 it ( "UInt16" , async () => {
78 expect ( await value ( "SELECT toUInt16(65535) AS v" )). toEqual ( 65535 );
79 });
80 it ( "UInt32" , async () => {
81 expect ( await value ( "SELECT toUInt32(4294967295) AS v" )). toEqual ( 4294967295 );
82 });
83 it ( "UInt64" , async () => {
84 expect ( await value ( "SELECT toUInt64('18446744073709551615') AS v" )). toEqual (
85 18446744073709551615 n ,
86 );
87 });
88 it ( "UInt128" , async () => {
89 expect (
90 await value (
91 "SELECT toUInt128('340282366920938463463374607431768211455') AS v" ,
92 ),
93 ). toEqual ( 340282366920938463463374607431768211455 n );
94 });
95 it ( "UInt256" , async () => {
96 expect (
97 await value ( "SELECT toUInt256('123456789012345678901234567890') AS v" ),
98 ). toEqual ( 123456789012345678901234567890 n );
99 });
100 it ( "Int8" , async () => {
101 expect ( await value ( "SELECT toInt8(-128) AS v" )). toEqual ( - 128 );
102 });
103 it ( "Int16" , async () => {
104 expect ( await value ( "SELECT toInt16(-32768) AS v" )). toEqual ( - 32768 );
105 });
106 it ( "Int32" , async () => {
107 expect ( await value ( "SELECT toInt32(-2147483648) AS v" )). toEqual (
108 - 2147483648 ,
109 );
110 });
111 it ( "Int64" , async () => {
112 expect ( await value ( "SELECT toInt64('-9223372036854775808') AS v" )). toEqual (
113 - 9223372036854775808 n ,
114 );
115 });
116 it ( "Int128" , async () => {
117 expect (
118 await value (
119 "SELECT toInt128('-170141183460469231731687303715884105728') AS v" ,
120 ),
121 ). toEqual ( - 170141183460469231731687303715884105728 n );
122 });
123 it ( "Int256" , async () => {
124 expect (
125 await value ( "SELECT toInt256('-123456789012345678901234567890') AS v" ),
126 ). toEqual ( - 123456789012345678901234567890 n );
127 });
128 it ( "Float32" , async () => {
129 expect ( await value ( "SELECT toFloat32(1.5) AS v" )). toEqual ( 1.5 );
130 });
131 it ( "Float64" , async () => {
132 expect ( await value ( "SELECT toFloat64(1.5) AS v" )). toEqual ( 1.5 );
133 });
134 it ( "BFloat16" , async () => {
135 expect ( await value ( "SELECT CAST(1.5 AS BFloat16) AS v" )). toEqual ( 1.5 );
136 });
137 it ( "Bool" , async () => {
138 expect ( await value ( "SELECT true AS v" )). toEqual ( true );
139 });
140 it ( "String" , async () => {
141 expect ( await value ( "SELECT 'hello' AS v" )). toEqual ( "hello" );
142 });
143 it ( "FixedString(N)" , async () => {
144 expect ( await value ( "SELECT CAST('abcde' AS FixedString(5)) AS v" )). toEqual (
145 "abcde" ,
146 );
147 });
148 it ( "UUID" , async () => {
149 expect (
150 await value ( "SELECT toUUID('61f0c404-5cb3-11e7-907b-a6006ad3dba0') AS v" ),
151 ). toEqual (
152 Buffer. from ([
153 231 , 17 , 179 , 92 , 4 , 196 , 240 , 97 , 160 , 219 , 211 , 106 , 0 , 166 , 123 , 144 ,
154 ]),
155 );
156 });
157 it ( "IPv4" , async () => {
158 expect ( await value ( "SELECT toIPv4('1.2.3.4') AS v" )). toEqual ( 16909060 );
159 });
160 it ( "IPv6" , async () => {
161 expect ( await value ( "SELECT toIPv6('2001:db8::1') AS v" )). toEqual (
162 Buffer. from ([ 32 , 1 , 13 , 184 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 ]),
163 );
164 });
165 it ( "Date" , async () => {
166 expect ( await value ( "SELECT toDate('2020-01-02') AS v" )). toEqual (
167 new Date ( 1577923200000 ),
168 );
169 });
170 it ( "Date32" , async () => {
171 expect ( await value ( "SELECT toDate32('2020-01-02') AS v" )). toEqual (
172 new Date ( 1577923200000 ),
173 );
174 });
175 it ( "DateTime" , async () => {
176 expect (
177 await value ( "SELECT toDateTime('2020-01-02 03:04:05', 'UTC') AS v" ),
178 ). toEqual ( new Date ( 1577934245000 ));
179 });
180 it ( "DateTime64(3) -> [Date, nanoseconds]" , async () => {
181 expect (
182 await value (
183 "SELECT toDateTime64('2020-01-02 03:04:05.123', 3, 'UTC') AS v" ,
184 ),
185 ). toEqual ([ new Date ( 1577934245000 ), 123000000 ]);
186 });
187 it ( "DateTime64(9) -> [Date, nanoseconds]" , async () => {
188 expect (
189 await value (
190 "SELECT toDateTime64('2020-01-02 03:04:05.123456789', 9, 'UTC') AS v" ,
191 ),
192 ). toEqual ([ new Date ( 1577934245000 ), 123456789 ]);
193 });
194 it ( "Time -> seconds" , async () => {
195 expect (
196 await value (
197 "SELECT CAST(3661 AS Time) AS v SETTINGS enable_time_time64_type=1" ,
198 ),
199 ). toEqual ( 3661 );
200 });
201 it ( "Time64(3) -> [ticks, precision]" , async () => {
202 expect (
203 await value (
204 "SELECT CAST(3661.5 AS Time64(3)) AS v SETTINGS enable_time_time64_type=1" ,
205 ),
206 ). toEqual ([ 3661500 n , 3 ]);
207 });
208 });
209
210 describe ( "decimals (width chosen by precision)" , () => {
211 it ( "Decimal(10, 2) -> Decimal32 width, [unscaled, scale]" , async () => {
212 const { types , rows } = await decode (
213 "SELECT CAST(1.5 AS Decimal(10, 2)) AS v" ,
214 );
215 expect (types). toEqual ([ "Decimal(10, 2)" ]);
216 expect (rows[ 0 ] ! .v). toEqual ([ 150 n , 2 ]);
217 });
218 it ( "Decimal(38, 10) -> Decimal128 width" , async () => {
219 expect ( await value ( "SELECT CAST(1.5 AS Decimal(38, 10)) AS v" )). toEqual ([
220 15000000000 n ,
221 10 ,
222 ]);
223 });
224 it ( "Decimal32(4) (header: Decimal(9, 4))" , async () => {
225 const { types , rows } = await decode (
226 "SELECT CAST(1.5 AS Decimal32(4)) AS v" ,
227 );
228 expect (types). toEqual ([ "Decimal(9, 4)" ]);
229 expect (rows[ 0 ] ! .v). toEqual ([ 15000 n , 4 ]);
230 });
231 it ( "Decimal64(4) (header: Decimal(18, 4))" , async () => {
232 expect ( await value ( "SELECT CAST(1.5 AS Decimal64(4)) AS v" )). toEqual ([
233 15000 n ,
234 4 ,
235 ]);
236 });
237 it ( "Decimal128(10) (header: Decimal(38, 10))" , async () => {
238 expect ( await value ( "SELECT CAST(1.5 AS Decimal128(10)) AS v" )). toEqual ([
239 15000000000 n ,
240 10 ,
241 ]);
242 });
243 it ( "Decimal256(20) (header: Decimal(76, 20))" , async () => {
244 expect ( await value ( "SELECT CAST(1.5 AS Decimal256(20)) AS v" )). toEqual ([
245 150000000000000000000 n ,
246 20 ,
247 ]);
248 });
249 });
250
251 describe ( "enums (resolve to the value's name)" , () => {
252 it ( "Enum8" , async () => {
253 expect (
254 await value ( "SELECT CAST('b' AS Enum8('a' = 1, 'b' = 2)) AS v" ),
255 ). toEqual ( "b" );
256 });
257 it ( "Enum16" , async () => {
258 expect (
259 await value ( "SELECT CAST('y' AS Enum16('x' = -1, 'y' = 100)) AS v" ),
260 ). toEqual ( "y" );
261 });
262 });
263
264 describe ( "intervals (count as bigint; unit lives in the type name)" , () => {
265 it ( "IntervalSecond" , async () => {
266 expect ( await value ( "SELECT INTERVAL 5 SECOND AS v" )). toEqual ( 5 n );
267 });
268 it ( "IntervalDay" , async () => {
269 expect ( await value ( "SELECT INTERVAL 3 DAY AS v" )). toEqual ( 3 n );
270 });
271 });
272
273 describe ( "geo" , () => {
274 it ( "Point" , async () => {
275 expect ( await value ( "SELECT CAST((1.5, 2.5) AS Point) AS v" )). toEqual ([
276 1.5 , 2.5 ,
277 ]);
278 });
279 it ( "Ring" , async () => {
280 expect (
281 await value ( "SELECT CAST([(0, 0), (1, 0), (1, 1)] AS Ring) AS v" ),
282 ). toEqual ([
283 [ 0 , 0 ],
284 [ 1 , 0 ],
285 [ 1 , 1 ],
286 ]);
287 });
288 it ( "LineString" , async () => {
289 expect (
290 await value ( "SELECT CAST([(0, 0), (1, 1)] AS LineString) AS v" ),
291 ). toEqual ([
292 [ 0 , 0 ],
293 [ 1 , 1 ],
294 ]);
295 });
296 it ( "Polygon" , async () => {
297 expect (
298 await value ( "SELECT CAST([[(0, 0), (1, 0), (1, 1)]] AS Polygon) AS v" ),
299 ). toEqual ([
300 [
301 [ 0 , 0 ],
302 [ 1 , 0 ],
303 [ 1 , 1 ],
304 ],
305 ]);
306 });
307 it ( "MultiLineString" , async () => {
308 expect (
309 await value ( "SELECT CAST([[(0, 0), (1, 1)]] AS MultiLineString) AS v" ),
310 ). toEqual ([
311 [
312 [ 0 , 0 ],
313 [ 1 , 1 ],
314 ],
315 ]);
316 });
317 it ( "MultiPolygon" , async () => {
318 expect (
319 await value (
320 "SELECT CAST([[[(0, 0), (1, 0), (1, 1)]]] AS MultiPolygon) AS v" ,
321 ),
322 ). toEqual ([
323 [
324 [
325 [ 0 , 0 ],
326 [ 1 , 0 ],
327 [ 1 , 1 ],
328 ],
329 ],
330 ]);
331 });
332 });
333
334 describe ( "composites (recurse into element/key/field readers)" , () => {
335 it ( "Nullable(UInt64) — present" , async () => {
336 expect ( await value ( "SELECT CAST(7 AS Nullable(UInt64)) AS v" )). toEqual ( 7 n );
337 });
338 it ( "Nullable(UInt64) — NULL" , async () => {
339 expect ( await value ( "SELECT CAST(NULL AS Nullable(UInt64)) AS v" )). toEqual (
340 null ,
341 );
342 });
343 it ( "Array(String)" , async () => {
344 expect (
345 await value ( "SELECT CAST(['a', 'bb'] AS Array(String)) AS v" ),
346 ). toEqual ([ "a" , "bb" ]);
347 });
348 it ( "Array(Array(Int32))" , async () => {
349 expect (
350 await value ( "SELECT CAST([[1], [2, 3]] AS Array(Array(Int32))) AS v" ),
351 ). toEqual ([[ 1 ], [ 2 , 3 ]]);
352 });
353 it ( "Array(Nullable(UInt64))" , async () => {
354 expect (
355 await value ( "SELECT CAST([1, NULL, 3] AS Array(Nullable(UInt64))) AS v" ),
356 ). toEqual ([ 1 n , null , 3 n ]);
357 });
358 it ( "Map(String, UInt64)" , async () => {
359 expect (
360 await value (
361 "SELECT CAST(map('a', 1, 'b', 2) AS Map(String, UInt64)) AS v" ,
362 ),
363 ). toEqual (
364 new Map ([
365 [ "a" , 1 n ],
366 [ "b" , 2 n ],
367 ]),
368 );
369 });
370 it ( "Map(String, Array(UInt8))" , async () => {
371 expect (
372 await value (
373 "SELECT CAST(map('a', [1, 2]) AS Map(String, Array(UInt8))) AS v" ,
374 ),
375 ). toEqual ( new Map ([[ "a" , [ 1 , 2 ]]]));
376 });
377 it ( "LowCardinality(String) — transparent" , async () => {
378 expect (
379 await value ( "SELECT CAST('x' AS LowCardinality(String)) AS v" ),
380 ). toEqual ( "x" );
381 });
382 it ( "LowCardinality(Nullable(String)) — NULL" , async () => {
383 expect (
384 await value ( "SELECT CAST(NULL AS LowCardinality(Nullable(String))) AS v" ),
385 ). toEqual ( null );
386 });
387 it ( "Array(Tuple(Float64, Float64))" , async () => {
388 expect (
389 await value (
390 "SELECT CAST([(1, 2), (3, 4)] AS Array(Tuple(Float64, Float64))) AS v" ,
391 ),
392 ). toEqual ([
393 [ 1 , 2 ],
394 [ 3 , 4 ],
395 ]);
396 });
397 it ( "Tuple(UInt8, String) — positional array" , async () => {
398 expect (
399 await value ( "SELECT CAST((7, 'x') AS Tuple(UInt8, String)) AS v" ),
400 ). toEqual ([ 7 , "x" ]);
401 });
402 it ( "Tuple(a UInt8, b String) — named object" , async () => {
403 expect (
404 await value ( "SELECT CAST((7, 'x') AS Tuple(a UInt8, b String)) AS v" ),
405 ). toEqual ({ a: 7 , b: "x" });
406 });
407 it ( "Tuple(Decimal(10, 2), Nullable(String))" , async () => {
408 expect (
409 await value (
410 "SELECT CAST((1.5, NULL) AS Tuple(Decimal(10, 2), Nullable(String))) AS v" ,
411 ),
412 ). toEqual ([[ 150 n , 2 ], null ]);
413 });
414 it ( "Nested(a UInt8, b String) — Array(Tuple) of objects" , async () => {
415 expect (
416 await value (
417 "SELECT CAST([(1, 'a'), (2, 'b')] AS Nested(a UInt8, b String)) AS v" ,
418 ),
419 ). toEqual ([
420 { a: 1 , b: "a" },
421 { a: 2 , b: "b" },
422 ]);
423 });
424 it ( "Variant(UInt8, String) — int alternative" , async () => {
425 expect (
426 await value (
427 "SELECT CAST(42 AS Variant(UInt8, String)) AS v SETTINGS allow_experimental_variant_type=1" ,
428 ),
429 ). toEqual ( 42 );
430 });
431 it ( "Variant(UInt8, String) — string alternative" , async () => {
432 expect (
433 await value (
434 "SELECT CAST('hi' AS Variant(UInt8, String)) AS v SETTINGS allow_experimental_variant_type=1" ,
435 ),
436 ). toEqual ( "hi" );
437 });
438 });
439
440 describe ( "self-describing types" , () => {
441 it ( "Dynamic" , async () => {
442 expect (
443 await value (
444 "SELECT CAST(42 AS Dynamic) AS v SETTINGS allow_experimental_dynamic_type=1" ,
445 ),
446 ). toEqual ( 42 );
447 });
448 it ( "JSON" , async () => {
449 expect (
450 await value (
451 `SELECT CAST('{"a":1}' AS JSON) AS v SETTINGS allow_experimental_json_type=1` ,
452 ),
453 ). toEqual ( new Map ([[ "a" , 1 n ]]));
454 });
455 });
456
457 describe ( "SQL-standard aliases normalize to canonical types in the header" , () => {
458 it ( "DOUBLE PRECISION -> Float64" , async () => {
459 const { types , rows } = await decode (
460 "SELECT CAST(1.5 AS DOUBLE PRECISION) AS v" ,
461 );
462 expect (types). toEqual ([ "Float64" ]);
463 expect (rows[ 0 ] ! .v). toEqual ( 1.5 );
464 });
465 it ( "REAL -> Float32" , async () => {
466 const { types } = await decode ( "SELECT CAST(1.5 AS REAL) AS v" );
467 expect (types). toEqual ([ "Float32" ]);
468 });
469 it ( "VARCHAR -> String" , async () => {
470 const { types , rows } = await decode (
471 "SELECT CAST('x' AS VARCHAR(10)) AS v" ,
472 );
473 expect (types). toEqual ([ "String" ]);
474 expect (rows[ 0 ] ! .v). toEqual ( "x" );
475 });
476 it ( "BIGINT -> Int64" , async () => {
477 const { types } = await decode ( "SELECT CAST(1 AS BIGINT) AS v" );
478 expect (types). toEqual ([ "Int64" ]);
479 });
480 it ( "NUMERIC(10, 2) -> Decimal(10, 2)" , async () => {
481 const { types , rows } = await decode (
482 "SELECT CAST(1.5 AS NUMERIC(10, 2)) AS v" ,
483 );
484 expect (types). toEqual ([ "Decimal(10, 2)" ]);
485 expect (rows[ 0 ] ! .v). toEqual ([ 150 n , 2 ]);
486 });
487 it ( "BINARY(4) -> FixedString(4)" , async () => {
488 const { types , rows } = await decode (
489 "SELECT CAST('abcd' AS BINARY(4)) AS v" ,
490 );
491 expect (types). toEqual ([ "FixedString(4)" ]);
492 expect (rows[ 0 ] ! .v). toEqual ( "abcd" );
493 });
494 });
495
496 describe ( "compileRowBinaryWithNamesAndTypes" , () => {
497 it ( "compiles a header and decodes the rest of the stream into rows" , async () => {
498 const { names , types , rows } = await ( async () => {
499 const s = await withNamesAndTypes ( `
500 SELECT id, name, score FROM (
501 SELECT toUInt32(1) AS id, CAST('alice' AS Nullable(String)) AS name, toFloat64(1.5) AS score
502 UNION ALL
503 SELECT toUInt32(2) AS id, CAST(NULL AS Nullable(String)) AS name, toFloat64(2.5) AS score
504 ) ORDER BY id
505 ` );
506 const compiled = compileRowBinaryWithNamesAndTypes (s);
507 const decoded = compiled. readRows (s);
508 expect (s.pos). toBe (s.buf. length );
509 return { names: compiled.names, types: compiled.types, rows: decoded };
510 })();
511
512 expect (names). toEqual ([ "id" , "name" , "score" ]);
513 expect (types). toEqual ([ "UInt32" , "Nullable(String)" , "Float64" ]);
514 expect (rows). toEqual ([
515 { id: 1 , name: "alice" , score: 1.5 },
516 { id: 2 , name: null , score: 2.5 },
517 ]);
518 });
519
520 it ( "handles a named Tuple and a Map column" , async () => {
521 const { rows } = await decode ( `
522 SELECT
523 CAST((10, 20) AS Tuple(a UInt32, b UInt32)) AS pair,
524 CAST(map('x', toUInt32(1), 'y', toUInt32(2)) AS Map(String, UInt32)) AS counts
525 ` );
526
527 expect (rows). toHaveLength ( 1 );
528 // A named Tuple folds to readTupleNamed -> an object keyed by field name.
529 expect (rows[ 0 ] ! .pair). toEqual ({ a: 10 , b: 20 });
530 expect (rows[ 0 ] ! .counts). toEqual (
531 new Map ([
532 [ "x" , 1 ],
533 [ "y" , 2 ],
534 ]),
535 );
536 });
537
538 it ( "accepts a custom resolver (a shared reader cache)" , async () => {
539 const cache = createTypeReaderCache ();
540 let calls = 0 ;
541 const resolve = ( t : string ) => {
542 calls ++ ;
543 return cache (t);
544 };
545
546 const s = await withNamesAndTypes ( "SELECT toUInt32(1) AS id, 'x' AS name" );
547 const compiled = compileRowBinaryWithNamesAndTypes (s, resolve);
548 expect (compiled. readRows (s)). toEqual ([{ id: 1 , name: "x" }]);
549 expect (calls). toBe ( 2 ); // resolver consulted once per column
550 });
551 });
552
553 describe ( "createTypeReaderCache (LRU, keyed by type string)" , () => {
554 it ( "returns the same reader instance for a repeated type, distinct for others" , () => {
555 const cache = createTypeReaderCache ();
556 const a = cache ( "Array(UInt8)" );
557 expect ( cache ( "Array(UInt8)" )). toBe (a); // hit -> same instance
558 expect ( cache ( "Array(UInt16)" )).not. toBe (a); // different type -> different reader
559 });
560
561 it ( "evicts least-recently-used past maxSize, but a hit refreshes recency" , () => {
562 // Use composite types: each compile of e.g. `Array(UInt8)` allocates a FRESH
563 // closure (readArray(...)), so instance identity actually reflects the cache.
564 // (A nullary scalar like `UInt8` returns the shared module-level readUInt8
565 // singleton every time, so it could never show an eviction.)
566 const cache = createTypeReaderCache ( 2 );
567 const a1 = cache ( "Array(UInt8)" ); // [A8]
568 const b1 = cache ( "Array(UInt16)" ); // [A8, A16]
569 expect ( cache ( "Array(UInt8)" )). toBe (a1); // hit -> refresh -> [A16, A8]
570
571 cache ( "Array(UInt32)" ); // insert -> size 3 -> evict LRU (A16) -> [A8, A32]
572
573 // Array(UInt8) was refreshed by the hit above, so it survived (same closure).
574 expect ( cache ( "Array(UInt8)" )). toBe (a1);
575 // Array(UInt16) was the least-recently-used, so it was evicted and now
576 // recompiles to a DIFFERENT closure — this is what proves eviction happened.
577 expect ( cache ( "Array(UInt16)" )).not. toBe (b1);
578 });
579
580 it ( "does not cache a parse failure" , () => {
581 const cache = createTypeReaderCache ();
582 expect (() => cache ( "AggregateFunction(sum, UInt64)" )). toThrow (
583 /cannot compile type/ ,
584 );
585 // A valid type still resolves afterwards (the failure left no poisoned entry).
586 expect ( typeof cache ( "UInt8" )). toBe ( "function" );
587 });
588 });