From bd8117361fc3ce6a848bace0dfcbad57b3cc5725 Mon Sep 17 00:00:00 2001 From: Disha Prakash Date: Mon, 12 Oct 2026 14:40:31 +0000 Subject: [PATCH 1/4] fix: parameterize metadata filter keys in AsyncPostgresVectorStore --- .../async_vector_store.py | 12 +++-- tests/test_async_vector_store.py | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/llama_index_cloud_sql_pg/async_vector_store.py b/src/llama_index_cloud_sql_pg/async_vector_store.py index c2c75a2..64b0910 100644 --- a/src/llama_index_cloud_sql_pg/async_vector_store.py +++ b/src/llama_index_cloud_sql_pg/async_vector_store.py @@ -598,7 +598,7 @@ def __parse_metadata_filters_recursively( def __parse_metadata_filter( self, filter: MetadataFilter, bind_params: dict[str, Any] ) -> str: - key = self.__to_postgres_key(filter.key) + key = self.__to_postgres_key(filter.key, bind_params) op = self.__to_postgres_operator(filter.operator) param_name = f"param_{len(bind_params)}" if filter.operator == FilterOperator.IS_EMPTY: @@ -617,7 +617,7 @@ def __parse_metadata_filter( Value -> '{filter.value}'""" ) return "" - bind_params[param_name] = f'["{filter.value}"]' + bind_params[param_name] = json.dumps([filter.value]) return f"({key})::jsonb {op} :{param_name} " if filter.operator == FilterOperator.TEXT_MATCH: bind_params[param_name] = f"%{filter.value}%" @@ -698,12 +698,14 @@ def __to_postgres_operator(self, operator: FilterOperator) -> str: warnings.warn(f"Unknown operator: {operator}, fallback to '='") return "=" - def __to_postgres_key(self, key: str) -> str: + def __to_postgres_key(self, key: str, bind_params: dict[str, Any]) -> str: if key in [ *self._metadata_columns, self._id_column, self._ref_doc_id_column, self._text_column, ]: - return key - return f"{self._metadata_json_column}->>'{key}'" + return f'"{key}"' + key_param = f"key_{len(bind_params)}" + bind_params[key_param] = key + return f"{self._metadata_json_column}->>:{key_param}" diff --git a/tests/test_async_vector_store.py b/tests/test_async_vector_store.py index 9a07d51..f307e3c 100644 --- a/tests/test_async_vector_store.py +++ b/tests/test_async_vector_store.py @@ -467,3 +467,54 @@ async def test_delete_nodes(self, vs): async def test_clear(self, vs): with pytest.raises(Exception, match=sync_method_exception_str): vs.clear() + + +def test_parse_metadata_filters_parameterizes_key_and_value(): + vs = AsyncPostgresVectorStore( + AsyncPostgresVectorStore._AsyncPostgresVectorStore__create_key, + engine=None, # type: ignore[arg-type] + table_name="test_table", + metadata_columns=["len"], + ) + injected_key = "k' IS NULL OR 1=1 OR li_metadata->>'k" + injected_value = "x') OR 1=1;--" + + bind_params: dict[str, Any] = {} + filters = MetadataFilters( + filters=[ + MetadataFilter( + key=injected_key, + value=injected_value, + operator=FilterOperator.EQ, + ), + MetadataFilter( + key="len", + value=3, + operator=FilterOperator.EQ, + ), + MetadataFilter( + key="tags", + value='a"b', + operator=FilterOperator.CONTAINS, + ), + ], + condition=FilterCondition.AND, + ) + + clause = vs._AsyncPostgresVectorStore__parse_metadata_filters_recursively( + filters, bind_params + ) + + assert injected_key not in clause + assert injected_value not in clause + assert ( + clause + == 'li_metadata->>:key_0 = :param_1 AND "len"::float = :param_2 AND (li_metadata->>:key_3)::jsonb @> :param_4 ' + ) + assert bind_params == { + "key_0": injected_key, + "param_1": injected_value, + "param_2": 3, + "key_3": "tags", + "param_4": '["a\\"b"]', + } From 6cc6546cfd3bd11e4030d6a1155fe10a90a27382 Mon Sep 17 00:00:00 2001 From: Disha Prakash Date: Mon, 12 Oct 2026 14:43:43 +0000 Subject: [PATCH 2/4] update test --- tests/test_async_vector_store.py | 96 ++++++++++++++++---------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/tests/test_async_vector_store.py b/tests/test_async_vector_store.py index f307e3c..29ed2c3 100644 --- a/tests/test_async_vector_store.py +++ b/tests/test_async_vector_store.py @@ -468,53 +468,55 @@ async def test_clear(self, vs): with pytest.raises(Exception, match=sync_method_exception_str): vs.clear() + async def test_parse_metadata_filters_parameterizes_key_and_value( + self, engine, custom_vs + ): + injected_key = "k' IS NULL OR 1=1 OR li_metadata->>'k" + injected_value = "x') OR 1=1;--" -def test_parse_metadata_filters_parameterizes_key_and_value(): - vs = AsyncPostgresVectorStore( - AsyncPostgresVectorStore._AsyncPostgresVectorStore__create_key, - engine=None, # type: ignore[arg-type] - table_name="test_table", - metadata_columns=["len"], - ) - injected_key = "k' IS NULL OR 1=1 OR li_metadata->>'k" - injected_value = "x') OR 1=1;--" - - bind_params: dict[str, Any] = {} - filters = MetadataFilters( - filters=[ - MetadataFilter( - key=injected_key, - value=injected_value, - operator=FilterOperator.EQ, - ), - MetadataFilter( - key="len", - value=3, - operator=FilterOperator.EQ, - ), - MetadataFilter( - key="tags", - value='a"b', - operator=FilterOperator.CONTAINS, - ), - ], - condition=FilterCondition.AND, - ) + bind_params: dict[str, Any] = {} + filters = MetadataFilters( + filters=[ + MetadataFilter( + key=injected_key, + value=injected_value, + operator=FilterOperator.EQ, + ), + MetadataFilter( + key="len", + value=3, + operator=FilterOperator.EQ, + ), + MetadataFilter( + key="tags", + value='a"b', + operator=FilterOperator.CONTAINS, + ), + ], + condition=FilterCondition.AND, + ) - clause = vs._AsyncPostgresVectorStore__parse_metadata_filters_recursively( - filters, bind_params - ) + clause = ( + custom_vs._AsyncPostgresVectorStore__parse_metadata_filters_recursively( + filters, bind_params + ) + ) - assert injected_key not in clause - assert injected_value not in clause - assert ( - clause - == 'li_metadata->>:key_0 = :param_1 AND "len"::float = :param_2 AND (li_metadata->>:key_3)::jsonb @> :param_4 ' - ) - assert bind_params == { - "key_0": injected_key, - "param_1": injected_value, - "param_2": 3, - "key_3": "tags", - "param_4": '["a\\"b"]', - } + assert injected_key not in clause + assert injected_value not in clause + assert ( + clause + == 'li_metadata->>:key_0 = :param_1 AND "len"::float = :param_2 AND (li_metadata->>:key_3)::jsonb @> :param_4 ' + ) + assert bind_params == { + "key_0": injected_key, + "param_1": injected_value, + "param_2": 3, + "key_3": "tags", + "param_4": '["a\\"b"]', + } + + results = await run_on_background( + engine, custom_vs.aget_nodes(filters=filters) + ) + assert len(results) == 0 From ce6b61931379309d4cf84b5dda0d00dd217f86c4 Mon Sep 17 00:00:00 2001 From: Disha Prakash Date: Mon, 12 Oct 2026 14:50:26 +0000 Subject: [PATCH 3/4] update test --- tests/test_async_vector_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_async_vector_store.py b/tests/test_async_vector_store.py index 29ed2c3..eaf1637 100644 --- a/tests/test_async_vector_store.py +++ b/tests/test_async_vector_store.py @@ -506,7 +506,7 @@ async def test_parse_metadata_filters_parameterizes_key_and_value( assert injected_value not in clause assert ( clause - == 'li_metadata->>:key_0 = :param_1 AND "len"::float = :param_2 AND (li_metadata->>:key_3)::jsonb @> :param_4 ' + == 'li_metadata->>:key_0 = :param_1 and "len"::float = :param_2 and (li_metadata->>:key_3)::jsonb @> :param_4 ' ) assert bind_params == { "key_0": injected_key, From d41e88de91e59267b99b73f992f0ba54b27c3954 Mon Sep 17 00:00:00 2001 From: Disha Prakash Date: Mon, 12 Oct 2026 14:55:22 +0000 Subject: [PATCH 4/4] update lint --- tests/test_async_vector_store.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_async_vector_store.py b/tests/test_async_vector_store.py index eaf1637..b5bc845 100644 --- a/tests/test_async_vector_store.py +++ b/tests/test_async_vector_store.py @@ -516,7 +516,5 @@ async def test_parse_metadata_filters_parameterizes_key_and_value( "param_4": '["a\\"b"]', } - results = await run_on_background( - engine, custom_vs.aget_nodes(filters=filters) - ) + results = await run_on_background(engine, custom_vs.aget_nodes(filters=filters)) assert len(results) == 0