From 033455fa713df103008b9156e7e3f5d4d10a9ff5 Mon Sep 17 00:00:00 2001 From: Rudra Date: Fri, 16 Oct 2026 21:36:27 +0530 Subject: [PATCH] fix: reject list filter values with a clear error Filtering on a list of values (e.g. short_name=[Equals("FOO"), Equals("BAR")]) silently ignored the Regex/Equals marker and generated a CONTAINS filter whose value was the whole list, changing the meaning of the query without warning. For link-typed properties a list value would instead crash with an AttributeError from str.startswith being called on the list. The KG query API only supports a single value per filter, so there is no way to express "any of these values". Raise a TypeError for list filter values instead of silently altering the query, and document the single-value restriction in the Equals docstring. Fixes #150 --- fairgraph/queries.py | 11 ++++++++++- test/test_queries.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/fairgraph/queries.py b/fairgraph/queries.py index 2888289a..5030cfcf 100644 --- a/fairgraph/queries.py +++ b/fairgraph/queries.py @@ -75,7 +75,7 @@ class Equals(str): The KG ignores case when matching, but not whitespace, so `Equals("FOO")` does match "foo", but not "FOO ". - Only a single value is supported: a list of `Equals` values is filtered with "CONTAINS". + Only a single value is supported: filtering on a list of values raises a TypeError. Args: value (str): the value the property must be equal to. @@ -495,6 +495,15 @@ def get_query_filter_property(property, context, filter: Any) -> QueryProperty: filter_obj = None else: # we have a filter value for this property + if isinstance(filter, list): + # The KG query API only supports a single value per filter, so there is no + # way to express "any of these values". Reject the list rather than silently + # changing the meaning of the query (e.g. by sending CONTAINS with a list). + raise TypeError( + "Filtering on a list of values is not supported: " + "the KG query API applies a single value per filter. " + "Filter on one value at a time instead." + ) if isinstance(filter, Regex): op = "REGEX" elif isinstance(filter, Equals): diff --git a/test/test_queries.py b/test/test_queries.py index 1b029056..974f5b38 100644 --- a/test/test_queries.py +++ b/test/test_queries.py @@ -825,6 +825,34 @@ def test_equals_rejects_a_non_id_string_for_a_link(mock_client): omcore.DatasetVersion.generate_query(client=mock_client, space=None, filters={"is_new_version_of": Equals("FOO")}) +@pytest.mark.parametrize( + "value", + [ + [Equals("FOO"), Equals("BAR")], + [Regex("^FOO$"), Regex("^BAR$")], + ["FOO", "BAR"], + ], +) +def test_list_filter_values_are_rejected(mock_client, value): + # the KG query API only supports a single value per filter, so a list must not + # be silently turned into a CONTAINS filter whose value is the whole list + with pytest.raises(TypeError): + omcore.Dataset.generate_query(client=mock_client, space=None, filters={"short_name": value}) + + +def test_list_filter_values_are_rejected_for_a_link(mock_client): + # a list value for a link-typed property used to crash with an AttributeError + # from str.startswith being called on the list + instance_ids = [ + "https://kg.ebrains.eu/api/instances/00000000-0000-0000-0000-000000000001", + "https://kg.ebrains.eu/api/instances/00000000-0000-0000-0000-000000000002", + ] + with pytest.raises(TypeError): + omcore.DatasetVersion.generate_query( + client=mock_client, space=None, filters={"is_new_version_of": instance_ids} + ) + + def test_existence_query_filters_strings_with_equals(mock_client): dataset = omcore.Dataset(short_name="FOO") query = omcore.Dataset.generate_minimal_query(client=mock_client, filters=dataset._build_existence_query())