From d25ec5543710c56f3a6786cbc8d5498244be669e Mon Sep 17 00:00:00 2001 From: Saquib Jawed <138011435+saquibjawedbit@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:39:32 +0530 Subject: [PATCH] Python: Keep null fields out of Azure DocumentDB membership filters Portable in/not_in membership never matches a null field, but the DocumentDB filter compiler kept None in the $in operands behind an $exists guard, so Filter("x", "in", [None, "a"]) also returned documents whose field is null. Drop None operands like the MongoDB, Redis and Postgres connectors do. Fixes #8653 --- .../_vector_store.py | 11 ++++------- .../tests/azure_documentdb/test_vector_store.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/python/packages/azure-documentdb/agent_framework_azure_documentdb/_vector_store.py b/python/packages/azure-documentdb/agent_framework_azure_documentdb/_vector_store.py index 3ad4d2520f2..fc9d94e768c 100644 --- a/python/packages/azure-documentdb/agent_framework_azure_documentdb/_vector_store.py +++ b/python/packages/azure-documentdb/agent_framework_azure_documentdb/_vector_store.py @@ -429,19 +429,16 @@ def _prepare_filter_condition(self, expression: FilterExpression) -> _Document: raise NotImplementedError( "Use contains, contains_any, or contains_all for Azure DocumentDB list fields." ) - operands = [_prepare_filter_operand(field, item) for item in value] + # Portable membership, unlike eq/ne, never matches a null field, even when None is an operand. + operands = [_prepare_filter_operand(field, item) for item in value if item is not None] operands = [item for item in operands if item is not _TYPE_MISMATCH] if not operands: if operator == "in": return self._false_filter() return {"$and": [{name: {"$exists": True}}, {name: {"$ne": None}}]} condition = {name: {"$in" if operator == "in" else "$nin": operands}} - if operator == "not_in" or None in operands: - guards: list[_Document] = [{name: {"$exists": True}}] - if operator == "not_in": - guards.append({name: {"$ne": None}}) - guards.append(condition) - return {"$and": guards} + if operator == "not_in": + return {"$and": [{name: {"$exists": True}}, {name: {"$ne": None}}, condition]} return condition if operator in {"contains", "contains_any", "contains_all"}: if field.type_ != "list": diff --git a/python/packages/azure-documentdb/tests/azure_documentdb/test_vector_store.py b/python/packages/azure-documentdb/tests/azure_documentdb/test_vector_store.py index 9d30ac8a0f1..77a3a6f21ee 100644 --- a/python/packages/azure-documentdb/tests/azure_documentdb/test_vector_store.py +++ b/python/packages/azure-documentdb/tests/azure_documentdb/test_vector_store.py @@ -320,6 +320,23 @@ def test_empty_membership_semantics(collection): } +def test_membership_never_matches_null_operands(collection): + assert collection._prepare_filter(Filter("category", "in", [None, "database"])) == { + "category": {"$in": ["database"]} + } + assert collection._prepare_filter(Filter("category", "in", [None])) == {"_id": {"$exists": False}} + assert collection._prepare_filter(Filter("category", "not_in", [None, "database"])) == { + "$and": [ + {"category": {"$exists": True}}, + {"category": {"$ne": None}}, + {"category": {"$nin": ["database"]}}, + ] + } + assert collection._prepare_filter(Filter("category", "not_in", [None])) == { + "$and": [{"category": {"$exists": True}}, {"category": {"$ne": None}}] + } + + def test_filter_type_mismatches_preserve_bool_number_distinction(collection): assert collection._prepare_filter(Filter("number", "eq", True)) == {"_id": {"$exists": False}} assert collection._prepare_filter(Filter("flag", "eq", 1)) == {"_id": {"$exists": False}}