fix(python): write NaN and infinity as SQL literals in table.update (#4326)

## What's wrong

`value_to_sql` renders floats with `str()`, so `float("nan")` becomes
`nan` and `float("inf")` becomes `inf`. SQL reads those as column names:

```python
table.update(where="id = 1", values={"price": float("nan")})
# ValueError: Invalid input, Schema error: No field named nan. Valid fields are id, price.
```

The same happens for `math.inf`, `-math.inf`, `np.nan` and
`np.float32("inf")`, which go through the same float path. The only
workaround today is `values_sql`.

## Fix

Emit `CAST('NaN' AS DOUBLE)`, `CAST('Infinity' AS DOUBLE)` and
`CAST('-Infinity' AS DOUBLE)` for these values. Finite floats render as
before. The cast also lands correctly in `float32` columns.

## Tests

`test_value_to_sql_nan_and_infinity` in
`python/python/tests/test_util.py` updates `float64` and `float32`
columns to NaN, +inf and -inf, then reads them back:

- On `main`, it fails with `ValueError: Invalid input, Schema error: No
field named nan`.
- With the fix, it passes. All of `test_util.py` passes (75 passed, 1
skipped), and so do the update tests in `test_table.py` (`-k update`, 8
passed).
- `ruff format --check` and `ruff check` are clean.

NaN inside a list value (for example a vector) still fails, because
Lance only accepts literals inside array values. This PR covers scalar
columns only.

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>
This commit is contained in:
breken
2026-09-28 20:16:26 -05:00
committed by GitHub
co-authored by breken-ai
parent 580240278f
commit 70a1123404
2 changed files with 37 additions and 0 deletions
+8
View File
@@ -5,6 +5,7 @@
import binascii
import functools
import importlib
import math
import os
import pathlib
import warnings
@@ -351,6 +352,13 @@ def _(value: int):
@value_to_sql.register(float)
def _(value: float):
# str() gives "nan" / "inf", which SQL would read as column names.
if math.isnan(value):
return "CAST('NaN' AS DOUBLE)"
if math.isinf(value):
return (
"CAST('Infinity' AS DOUBLE)" if value > 0 else "CAST('-Infinity' AS DOUBLE)"
)
return str(value)
+29
View File
@@ -213,6 +213,35 @@ def test_value_to_sql_numpy_scalars():
assert value_to_sql(np.bool_(False)) == "FALSE"
def test_value_to_sql_nan_and_infinity(tmp_path):
# str(float("nan")) is "nan", which SQL reads as a column named nan, so
# table.update(values={"x": float("nan")}) failed with "No field named nan".
import math
import numpy as np
db = lancedb.connect(tmp_path)
table = db.create_table(
"test",
pa.table(
{
"id": [1, 2, 3, 4],
"f64": [1.0, 2.0, 3.0, 4.0],
"f32": pa.array([1.0, 2.0, 3.0, 4.0], pa.float32()),
}
),
)
table.update(where="id = 1", values={"f64": float("nan"), "f32": np.nan})
table.update(where="id = 2", values={"f64": math.inf, "f32": np.float32("inf")})
table.update(where="id = 3", values={"f64": -math.inf, "f32": -math.inf})
rows = {row["id"]: row for row in table.to_arrow().to_pylist()}
assert math.isnan(rows[1]["f64"]) and math.isnan(rows[1]["f32"])
assert rows[2]["f64"] == math.inf and rows[2]["f32"] == math.inf
assert rows[3]["f64"] == -math.inf and rows[3]["f32"] == -math.inf
assert rows[4]["f64"] == 4.0 and rows[4]["f32"] == 4.0
def test_append_vector_columns():
registry = EmbeddingFunctionRegistry.get_instance()
registry.register("test")(MockTextEmbeddingFunction)