Skip to content

Commit c4c23bd

Browse files
asg017claude
andcommitted
Reject NaN and Inf in float32 vector input
NaN/Inf values in vectors break heap/sort invariants in KNN, causing wrong or unpredictable results. Now rejected at parse time in fvec_from_value() for both blob and JSON text input paths, with a clear error message identifying the offending element index. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5522e86 commit c4c23bd

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

sqlite-vec.c

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -984,8 +984,18 @@ static int fvec_from_value(sqlite3_value *value, f32 **vector,
984984
return SQLITE_NOMEM;
985985
}
986986
memcpy(buf, blob, bytes);
987+
size_t n = bytes / sizeof(f32);
988+
for (size_t i = 0; i < n; i++) {
989+
if (isnan(buf[i]) || isinf(buf[i])) {
990+
*pzErr = sqlite3_mprintf(
991+
"invalid float32 vector: element %d is %s",
992+
(int)i, isnan(buf[i]) ? "NaN" : "Inf");
993+
sqlite3_free(buf);
994+
return SQLITE_ERROR;
995+
}
996+
}
987997
*vector = buf;
988-
*dimensions = bytes / sizeof(f32);
998+
*dimensions = n;
989999
*cleanup = sqlite3_free;
9901000
return SQLITE_OK;
9911001
}
@@ -1053,6 +1063,13 @@ static int fvec_from_value(sqlite3_value *value, f32 **vector,
10531063
}
10541064

10551065
f32 res = (f32)result;
1066+
if (isnan(res) || isinf(res)) {
1067+
sqlite3_free(x.z);
1068+
*pzErr = sqlite3_mprintf(
1069+
"invalid float32 vector: element %d is %s",
1070+
(int)x.length, isnan(res) ? "NaN" : "Inf");
1071+
return SQLITE_ERROR;
1072+
}
10561073
array_append(&x, (const void *)&res);
10571074

10581075
offset += (endptr - ptr);

tests/test-loadable.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,34 @@ def check(a, b, dtype=np.float32):
365365
)
366366

367367

368+
def test_vec_reject_nan_inf():
369+
"""NaN and Inf in float32 vectors should be rejected."""
370+
import struct, math
371+
372+
# NaN via blob
373+
nan_blob = struct.pack("4f", 1.0, float("nan"), 3.0, 4.0)
374+
with pytest.raises(sqlite3.OperationalError, match="NaN"):
375+
db.execute("SELECT vec_length(?)", [nan_blob])
376+
377+
# Inf via blob
378+
inf_blob = struct.pack("4f", 1.0, float("inf"), 3.0, 4.0)
379+
with pytest.raises(sqlite3.OperationalError, match="Inf"):
380+
db.execute("SELECT vec_length(?)", [inf_blob])
381+
382+
# -Inf via blob
383+
ninf_blob = struct.pack("4f", 1.0, float("-inf"), 3.0, 4.0)
384+
with pytest.raises(sqlite3.OperationalError, match="Inf"):
385+
db.execute("SELECT vec_length(?)", [ninf_blob])
386+
387+
# NaN via JSON
388+
# Note: JSON doesn't have NaN literal, but strtod may parse "NaN"
389+
# This tests the blob path which is the primary input method
390+
391+
# Valid vectors still work
392+
ok_blob = struct.pack("4f", 1.0, 2.0, 3.0, 4.0)
393+
assert db.execute("SELECT vec_length(?)", [ok_blob]).fetchone()[0] == 4
394+
395+
368396
def test_vec_distance_l2():
369397
vec_distance_l2 = lambda *args, a="?", b="?": db.execute(
370398
f"select vec_distance_l2({a}, {b})", args

0 commit comments

Comments
 (0)