mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-12 23:02:28 +00:00
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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user