Skip to content

Commit 948b271

Browse files
committed
fix(pgvector): use type check for numeric filter values
Avoid casting string metadata values to float by checking the Python type of the filter value instead of calling float(). This prevents PostgreSQL DataError for strings like '2024_123' that Python float() accepts but PostgreSQL ::float does not. Fixes #15962
1 parent d802122 commit 948b271

2 files changed

Lines changed: 68 additions & 6 deletions

File tree

‎llama-index-integrations/vector_stores/llama-index-vector-stores-postgres/llama_index/vector_stores/postgres/base.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -735,16 +735,21 @@ def _build_filter_clause(self, filter_: MetadataFilter) -> Any:
735735
f"{self._to_postgres_operator(filter_.operator)}"
736736
)
737737
else:
738-
# Check if value is a number. If so, cast the metadata value to a float
739-
# This is necessary because the metadata is stored as a string
740-
try:
738+
# If the filter value is a numeric type (int or float), cast the
739+
# metadata JSON text to float for the comparison. We check the
740+
# Python type explicitly rather than trying float(value) because
741+
# Python's float() accepts underscore-separated strings like
742+
# "2024_123" (returning 2024123.0) while PostgreSQL's ::float cast
743+
# does not, which would produce a DataError at query time.
744+
if isinstance(filter_.value, (int, float)) and not isinstance(
745+
filter_.value, bool
746+
):
741747
return text(
742748
f"(metadata_->>'{filter_.key}')::float "
743749
f"{self._to_postgres_operator(filter_.operator)} "
744750
f"{float(filter_.value)}"
745751
)
746-
except ValueError:
747-
# If not a number, then treat it as a string
752+
else:
748753
return text(
749754
f"metadata_->>'{filter_.key}' "
750755
f"{self._to_postgres_operator(filter_.operator)} "
Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,64 @@
1-
from llama_index.core.vector_stores.types import BasePydanticVectorStore
1+
from llama_index.core.vector_stores.types import (
2+
BasePydanticVectorStore,
3+
FilterOperator,
4+
MetadataFilter,
5+
)
26
from llama_index.vector_stores.postgres import PGVectorStore
7+
from llama_index.vector_stores.postgres.base import PGVectorStore as _PGVectorStore
38

49

510
def test_class():
611
names_of_base_classes = [b.__name__ for b in PGVectorStore.__mro__]
712
assert BasePydanticVectorStore.__name__ in names_of_base_classes
13+
14+
15+
def _make_store() -> PGVectorStore:
16+
"""Create a minimal PGVectorStore using __new__ so no DB connection is needed."""
17+
store = _PGVectorStore.__new__(_PGVectorStore)
18+
# _to_postgres_operator only needs the operator enum, so we delegate to the
19+
# real implementation; we just avoid touching any DB-related attributes.
20+
return store
21+
22+
23+
def test_build_filter_clause_string_with_underscores():
24+
"""String values that look like numbers (e.g., '2024_123') must use string
25+
comparison, not a ::float cast. Python's float() accepts underscores, but
26+
PostgreSQL's ::float cast does not, so using float() as the type check
27+
caused DataError for such values (issue #15962)."""
28+
store = _make_store()
29+
f = MetadataFilter(key="file_name", value="2024_123", operator=FilterOperator.EQ)
30+
clause = store._build_filter_clause(f)
31+
sql = clause.text
32+
assert "::float" not in sql, f"string value should not use ::float cast; got: {sql}"
33+
assert "'2024_123'" in sql
34+
35+
36+
def test_build_filter_clause_int_value():
37+
"""Integer filter values should use the ::float cast for numeric comparison."""
38+
store = _make_store()
39+
f = MetadataFilter(key="year", value=2024, operator=FilterOperator.EQ)
40+
clause = store._build_filter_clause(f)
41+
sql = clause.text
42+
assert "::float" in sql, f"int value should use ::float cast; got: {sql}"
43+
assert "2024" in sql
44+
45+
46+
def test_build_filter_clause_float_value():
47+
"""Float filter values should use the ::float cast."""
48+
store = _make_store()
49+
f = MetadataFilter(key="score", value=0.9, operator=FilterOperator.GT)
50+
clause = store._build_filter_clause(f)
51+
sql = clause.text
52+
assert "::float" in sql, f"float value should use ::float cast; got: {sql}"
53+
54+
55+
def test_build_filter_clause_plain_number_string():
56+
"""A string value that happens to be a plain decimal number (e.g., '123')
57+
should still use string comparison because the user explicitly provided a
58+
string type."""
59+
store = _make_store()
60+
f = MetadataFilter(key="year", value="123", operator=FilterOperator.EQ)
61+
clause = store._build_filter_clause(f)
62+
sql = clause.text
63+
assert "::float" not in sql, f"string '123' should not use ::float cast; got: {sql}"
64+
assert "'123'" in sql

0 commit comments

Comments
 (0)