Skip to content

Commit 1eae42f

Browse files
committed
fix: BigInt(Infinity) crash in readers + HNSW serialize round-trip
csv-reader/json-reader: BigInt(Math.trunc(Number(raw))) throws RangeError when raw parses to Infinity/NaN (e.g. "1e999"). Now guards with Number.isFinite() and returns null. json-reader: Boolean(null) silently converted null to false in bool columns, losing null semantics. Now returns null for null/undefined. hnsw.ts: serialize() didn't write efConstruction, so deserialize() always used default 200. Now included in header (29 bytes). Also removed dead Float32Array allocation in serialize().
1 parent 95b4459 commit 1eae42f

File tree

3 files changed

+23
-10
lines changed

3 files changed

+23
-10
lines changed

src/hnsw.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -481,14 +481,15 @@ export class HnswIndex {
481481
* Serialize the index to a compact binary format.
482482
*
483483
* Layout:
484-
* Header (25 bytes):
484+
* Header (29 bytes):
485485
* magic: u32 ("HNSW")
486486
* dim: u32
487487
* M: u32
488488
* maxLevel: u32
489489
* entryPoint: u32
490490
* size: u32
491491
* metric: u8
492+
* efConstruction: u32
492493
* Vectors: size * dim * 4 bytes (float32)
493494
* Node levels: size * u32
494495
* Per level (maxLevel + 1 levels):
@@ -500,7 +501,7 @@ export class HnswIndex {
500501
*/
501502
serialize(): ArrayBuffer {
502503
// Calculate total size
503-
let totalSize = 25; // header
504+
let totalSize = 29; // header
504505
totalSize += this._size * this.dim * 4; // vectors
505506
totalSize += this._size * 4; // node levels
506507

@@ -515,7 +516,6 @@ export class HnswIndex {
515516

516517
const buf = new ArrayBuffer(totalSize);
517518
const view = new DataView(buf);
518-
const f32 = new Float32Array(buf);
519519
let offset = 0;
520520

521521
// Header
@@ -526,6 +526,7 @@ export class HnswIndex {
526526
view.setUint32(offset, this.entryPoint >= 0 ? this.entryPoint : 0, true); offset += 4;
527527
view.setUint32(offset, this._size, true); offset += 4;
528528
view.setUint8(offset, METRIC_BYTE[this.metric] ?? 0); offset += 1;
529+
view.setUint32(offset, this.efConstruction, true); offset += 4;
529530

530531
// Vectors
531532
for (let i = 0; i < this._size; i++) {
@@ -576,10 +577,12 @@ export class HnswIndex {
576577
const entryPoint = view.getUint32(offset, true); offset += 4;
577578
const size = view.getUint32(offset, true); offset += 4;
578579
const metricByte = view.getUint8(offset); offset += 1;
580+
const efConstruction = offset + 4 <= data.byteLength ? view.getUint32(offset, true) : 200;
581+
offset += 4;
579582

580583
const resolvedMetric = metric ?? BYTE_METRIC[metricByte] ?? "cosine";
581584

582-
const idx = new HnswIndex({ dim, metric: resolvedMetric, M });
585+
const idx = new HnswIndex({ dim, metric: resolvedMetric, M, efConstruction });
583586
idx.maxLevel = size > 0 ? maxLevel : -1;
584587
idx.entryPoint = size > 0 ? entryPoint : -1;
585588

src/readers/csv-reader.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,11 @@ function coerceValue(raw: string, dtype: DataType): number | bigint | string | b
170170
const lower = trimmed.toLowerCase();
171171
return lower === "true" || lower === "1";
172172
}
173-
case "int64":
174-
return BigInt(Math.trunc(Number(trimmed)));
173+
case "int64": {
174+
const n = Number(trimmed);
175+
if (!Number.isFinite(n)) return null;
176+
return BigInt(Math.trunc(n));
177+
}
175178
case "float64":
176179
return Number(trimmed);
177180
default:

src/readers/json-reader.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ function coerceValue(raw: unknown, dtype: DataType): number | bigint | string |
4949
if (raw === null || raw === undefined) return null;
5050
switch (dtype) {
5151
case "bool":
52+
if (raw === null || raw === undefined) return null;
5253
return Boolean(raw);
53-
case "int64":
54+
case "int64": {
5455
if (typeof raw === "bigint") return raw;
55-
return BigInt(Math.trunc(Number(raw)));
56+
const n = Number(raw);
57+
if (!Number.isFinite(n)) return null;
58+
return BigInt(Math.trunc(n));
59+
}
5660
case "float64":
5761
return Number(raw);
5862
case "utf8":
@@ -66,9 +70,12 @@ function coerceValue(raw: unknown, dtype: DataType): number | bigint | string |
6670
case "uint32":
6771
case "float32":
6872
return Number(raw);
69-
case "uint64":
73+
case "uint64": {
7074
if (typeof raw === "bigint") return raw;
71-
return BigInt(Math.trunc(Number(raw)));
75+
const n = Number(raw);
76+
if (!Number.isFinite(n)) return null;
77+
return BigInt(Math.trunc(n));
78+
}
7279
default:
7380
return String(raw);
7481
}

0 commit comments

Comments
 (0)