From d8063545d9da6ebb55ffe523534c267bd9d1d548 Mon Sep 17 00:00:00 2001 From: breken Date: Mon, 28 Sep 2026 18:11:39 -0700 Subject: [PATCH] fix(node): write NaN and Infinity as SQL literals in table.update (#4327) ## What's wrong `toSQL` renders numbers with `toString()`, so `NaN` becomes `NaN` and `Infinity` becomes `Infinity`. SQL reads those as column names: ```ts await table.update({ values: { price: NaN }, where: "id = 1" }); // Error: Invalid input, Schema error: No field named "NaN". Valid fields are id, price. ``` `Infinity` and `-Infinity` fail the same way. The only workaround today is `valuesSql`. ## Fix Emit `CAST('NaN' AS DOUBLE)`, `CAST('Infinity' AS DOUBLE)` and `CAST('-Infinity' AS DOUBLE)` for these values. Finite numbers render as before. ## Tests - `__test__/util.test.ts` adds `toSQL` cases for `NaN`, `Infinity` and `-Infinity`. - `__test__/table.test.ts` adds "should let me update float values to NaN and Infinity", which updates a float column and reads the values back. - On `main`, the new cases fail with `No field named "NaN"`. - With the fix, all of `util.test.ts` and `table.test.ts` pass (344 tests). These were run against the published `@lancedb/lancedb@0.40.0-beta.11` native binary. - `biome check` is clean on the changed files. The Python SDK has the same issue in `value_to_sql`. That fix is in #4326. The fix was found and written by an AI agent (Breken) and checked against the tests above. Co-authored-by: breken-ai <312387581+breken-ai@users.noreply.github.com> --- nodejs/__test__/table.test.ts | 20 ++++++++++++++++++++ nodejs/__test__/util.test.ts | 3 +++ nodejs/lancedb/util.ts | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/nodejs/__test__/table.test.ts b/nodejs/__test__/table.test.ts index ac8904c16..7fe854438 100644 --- a/nodejs/__test__/table.test.ts +++ b/nodejs/__test__/table.test.ts @@ -416,6 +416,26 @@ describe.each([arrow15, arrow16, arrow17, arrow18])( expect(await table.countRows("id == 10")).toBe(1); }); + it("should let me update float values to NaN and Infinity", async () => { + const db = await connect(tmpDir.name); + const floats = await db.createTable("floats", [ + { id: 1, price: 1.5 }, + { id: 2, price: 2.5 }, + { id: 3, price: 3.5 }, + { id: 4, price: 4.5 }, + ]); + await floats.update({ values: { price: NaN }, where: "id = 1" }); + await floats.update({ values: { price: Infinity }, where: "id = 2" }); + await floats.update({ values: { price: -Infinity }, where: "id = 3" }); + + const rows = await floats.query().toArray(); + const price = (id: number) => rows.find((row) => row.id === id).price; + expect(price(1)).toBeNaN(); + expect(price(2)).toBe(Infinity); + expect(price(3)).toBe(-Infinity); + expect(price(4)).toBe(4.5); + }); + // https://github.com/lancedb/lancedb/issues/1293 test.each([new arrow.Float16(), new arrow.Float32(), new arrow.Float64()])( "can create empty table with non default float type: %s", diff --git a/nodejs/__test__/util.test.ts b/nodejs/__test__/util.test.ts index 802f8ed4d..d7e489f73 100644 --- a/nodejs/__test__/util.test.ts +++ b/nodejs/__test__/util.test.ts @@ -14,6 +14,9 @@ test.each([ [new ArrayBuffer(8), "X'0000000000000000'"], [Buffer.from("hello"), "X'68656c6c6f'"], ["Hello 'world'", "'Hello ''world'''"], + [NaN, "CAST('NaN' AS DOUBLE)"], + [Infinity, "CAST('Infinity' AS DOUBLE)"], + [-Infinity, "CAST('-Infinity' AS DOUBLE)"], ])("toSQL(%p) === %p", (value, expected) => { expect(toSQL(value)).toBe(expected); }); diff --git a/nodejs/lancedb/util.ts b/nodejs/lancedb/util.ts index 9219f3d1d..5127a1c7f 100644 --- a/nodejs/lancedb/util.ts +++ b/nodejs/lancedb/util.ts @@ -15,6 +15,14 @@ export function toSQL(value: IntoSql): string { if (typeof value === "string") { return `'${value.replace(/'/g, "''")}'`; } else if (typeof value === "number") { + // toString() gives "NaN" / "Infinity", which SQL reads as column names. + if (Number.isNaN(value)) { + return "CAST('NaN' AS DOUBLE)"; + } else if (!Number.isFinite(value)) { + return value > 0 + ? "CAST('Infinity' AS DOUBLE)" + : "CAST('-Infinity' AS DOUBLE)"; + } return value.toString(); } else if (typeof value === "boolean") { return value ? "TRUE" : "FALSE";