From 2f3cfdbc5ae8c2350afa04b69db3e62fa8d237e9 Mon Sep 17 00:00:00 2001 From: Divyam Talwar Date: Thu, 17 Sep 2026 09:00:41 +0530 Subject: [PATCH] fix: evaluate only the requested metadata comparator --- datamind/capabilities/kb/filters.py | 30 +++++++++++++------- datamind/tests/test_filters.py | 43 +++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 datamind/tests/test_filters.py diff --git a/datamind/capabilities/kb/filters.py b/datamind/capabilities/kb/filters.py index 0f8d926..fdc23dd 100644 --- a/datamind/capabilities/kb/filters.py +++ b/datamind/capabilities/kb/filters.py @@ -52,16 +52,26 @@ def matches_metadata(metadata: dict[str, Any], where: dict[str, Any] | None) -> continue op, value = next(iter(expected.items())) try: - ok = { - "$eq": actual == value, - "$ne": actual != value, - "$in": actual in value, - "$nin": actual not in value, - "$gt": actual > value, - "$gte": actual >= value, - "$lt": actual < value, - "$lte": actual <= value, - }[op] + # Dispatch before evaluating the operation. Building a mapping of + # boolean results evaluates every comparator, so an unrelated + # ordering or membership operation can turn a valid match into a + # false negative (for example, 42 == 42 also evaluates 42 in 42). + if op == "$eq": + ok = actual == value + elif op == "$ne": + ok = actual != value + elif op == "$in": + ok = actual in value + elif op == "$nin": + ok = actual not in value + elif op == "$gt": + ok = actual > value + elif op == "$gte": + ok = actual >= value + elif op == "$lt": + ok = actual < value + else: + ok = actual <= value except (TypeError, KeyError): ok = False if not ok: diff --git a/datamind/tests/test_filters.py b/datamind/tests/test_filters.py new file mode 100644 index 0000000..5f57828 --- /dev/null +++ b/datamind/tests/test_filters.py @@ -0,0 +1,43 @@ +"""Metadata comparator dispatch tests.""" +from __future__ import annotations + +import pytest + +from datamind.capabilities.kb.filters import matches_metadata + + +@pytest.mark.parametrize( + ("actual", "operator", "expected", "matched"), + [ + (42, "$eq", 42, True), + (42, "$eq", 41, False), + (42, "$ne", 41, True), + (42, "$ne", 42, False), + ("a", "$in", ["a", "b"], True), + ("c", "$in", ["a", "b"], False), + ("c", "$nin", ["a", "b"], True), + ("a", "$nin", ["a", "b"], False), + (42, "$gt", 40, True), + (42, "$gt", 42, False), + (42, "$gte", 42, True), + (42, "$gte", 43, False), + (40, "$lt", 42, True), + (42, "$lt", 42, False), + (42, "$lte", 42, True), + (43, "$lte", 42, False), + ], +) +def test_matches_metadata_dispatches_only_the_requested_comparator( + actual, operator, expected, matched, +): + assert matches_metadata({"value": actual}, {"value": {operator: expected}}) is matched + + +def test_nested_boolean_filters_keep_comparator_dispatch_local(): + assert matches_metadata( + {"age": 42, "tag": "a"}, + {"$and": [ + {"age": {"$gt": 40}}, + {"tag": {"$in": ["a", "b"]}}, + ]}, + )