From a7562c616a40d39c304004616bca1edfb31b5238 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Wed, 13 May 2026 10:07:03 -0700 Subject: [PATCH 01/11] Initial nor tests Signed-off-by: PatersonProjects --- .../logical/nor/test_nor_argument_handling.py | 166 ++++++++++++ .../logical/nor/test_nor_array_fields.py | 102 ++++++++ .../logical/nor/test_nor_command_contexts.py | 77 ++++++ .../query/logical/nor/test_nor_core_logic.py | 166 ++++++++++++ .../query/logical/nor/test_nor_data_types.py | 238 ++++++++++++++++++ .../logical/nor/test_nor_nested_operators.py | 206 +++++++++++++++ .../logical/nor/test_nor_null_missing.py | 101 ++++++++ .../logical/nor/test_nor_special_values.py | 112 +++++++++ 8 files changed, 1168 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_command_contexts.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_core_logic.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_nested_operators.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py new file mode 100644 index 00000000..8b139122 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py @@ -0,0 +1,166 @@ +""" +Tests for $nor query operator argument handling. + +Covers array argument validation, expression element validation, +and error cases for invalid argument types. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +DOCS = [{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 2, "b": 1}] + +INVALID_ARGUMENT_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="non_array_object", + filter={"$nor": {"price": 1.99}}, + doc=DOCS, + error_code=2, + msg="$nor with non-array object argument should return BadValue error", + ), + QueryTestCase( + id="null_argument", + filter={"$nor": None}, + doc=DOCS, + error_code=2, + msg="$nor with null argument should return BadValue error", + ), + QueryTestCase( + id="string_argument", + filter={"$nor": "invalid"}, + doc=DOCS, + error_code=2, + msg="$nor with string argument should return BadValue error", + ), + QueryTestCase( + id="numeric_argument", + filter={"$nor": 123}, + doc=DOCS, + error_code=2, + msg="$nor with numeric argument should return BadValue error", + ), + QueryTestCase( + id="boolean_argument", + filter={"$nor": True}, + doc=DOCS, + error_code=2, + msg="$nor with boolean argument should return BadValue error", + ), +] + +INVALID_ELEMENT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="non_object_element_integer", + filter={"$nor": [123]}, + doc=DOCS, + error_code=2, + msg="$nor with integer element in array should return BadValue error", + ), + QueryTestCase( + id="non_object_element_string", + filter={"$nor": ["invalid"]}, + doc=DOCS, + error_code=2, + msg="$nor with string element in array should return BadValue error", + ), + QueryTestCase( + id="non_object_element_null", + filter={"$nor": [None]}, + doc=DOCS, + error_code=2, + msg="$nor with null element in array should return BadValue error", + ), + QueryTestCase( + id="non_object_element_array", + filter={"$nor": [[{"a": 1}]]}, + doc=DOCS, + error_code=2, + msg="$nor with array element in array should return BadValue error", + ), +] + +VALID_ARRAY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="empty_array", + filter={"$nor": []}, + doc=DOCS, + error_code=2, + msg="$nor with empty array should return BadValue error", + ), + QueryTestCase( + id="single_expression", + filter={"$nor": [{"a": 1}]}, + doc=DOCS, + expected=[{"_id": 2, "a": 2, "b": 1}], + msg="$nor with single expression should exclude matching docs", + ), + QueryTestCase( + id="two_expressions", + filter={"$nor": [{"a": 1}, {"b": 1}]}, + doc=[{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 2, "b": 1}, {"_id": 3, "a": 2, "b": 2}], + expected=[{"_id": 3, "a": 2, "b": 2}], + msg="$nor with two expressions should return docs failing both", + ), + QueryTestCase( + id="many_expressions", + filter={"$nor": [{"a": 1}, {"b": 1}, {"a": 3}, {"b": 3}, {"a": 4}]}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2, "b": 2}], + expected=[{"_id": 2, "a": 2, "b": 2}], + msg="$nor with many expressions should return docs failing all", + ), + QueryTestCase( + id="empty_object_in_array", + filter={"$nor": [{}]}, + doc=DOCS, + expected=[], + msg="$nor with empty object matches all docs so returns empty", + ), +] + +ERROR_HANDLING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="non_top_level_position", + filter={"field": {"$nor": [{"a": 1}]}}, + doc=DOCS, + error_code=2, + msg="$nor at non-top-level position should return BadValue error", + ), + QueryTestCase( + id="invalid_operator_inside_expression", + filter={"$nor": [{"val": {"$invalid": 1}}]}, + doc=DOCS, + error_code=2, + msg="$nor with invalid operator inside expression should return BadValue error", + ), + QueryTestCase( + id="mixed_valid_invalid_expressions", + filter={"$nor": [{"a": 1}, {"val": {"$invalid": 1}}]}, + doc=DOCS, + error_code=2, + msg="$nor with mixed valid/invalid expressions should return BadValue error", + ), +] + +ALL_TESTS = ( + INVALID_ARGUMENT_TYPE_TESTS + INVALID_ELEMENT_TESTS + VALID_ARRAY_TESTS + ERROR_HANDLING_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_nor_argument_handling(collection, test): + """Test $nor query operator argument validation.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + ignore_doc_order=True, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py new file mode 100644 index 00000000..64a7f62a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py @@ -0,0 +1,102 @@ +""" +Tests for $nor query operator with array fields. + +Covers element matching in arrays, nested array paths, empty arrays, +arrays of objects with dot notation, and $elemMatch on arrays of objects. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +ARRAY_FIELD_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="element_matches_in_array", + filter={"$nor": [{"val": 1}]}, + doc=[{"_id": 1, "val": [1, 2, 3]}, {"_id": 2, "val": [4, 5, 6]}], + expected=[{"_id": 2, "val": [4, 5, 6]}], + msg="$nor should exclude docs where array contains matching element", + ), + QueryTestCase( + id="no_element_matches_in_array", + filter={"$nor": [{"val": 1}]}, + doc=[{"_id": 1, "val": [4, 5, 6]}, {"_id": 2, "val": [7, 8]}], + expected=[{"_id": 1, "val": [4, 5, 6]}, {"_id": 2, "val": [7, 8]}], + msg="$nor should return docs where array does not contain matching element", + ), + QueryTestCase( + id="empty_array_with_size", + filter={"$nor": [{"val": {"$size": 0}}]}, + doc=[{"_id": 1, "val": []}, {"_id": 2, "val": [1]}], + expected=[{"_id": 2, "val": [1]}], + msg="$nor with $size:0 should exclude docs with empty array", + ), + QueryTestCase( + id="nested_arrays", + filter={"$nor": [{"val": [1, 2]}]}, + doc=[{"_id": 1, "val": [[1, 2], [3, 4]]}, {"_id": 2, "val": [[5, 6]]}], + expected=[{"_id": 2, "val": [[5, 6]]}], + msg="$nor should exclude docs where nested array contains matching sub-array", + ), + QueryTestCase( + id="array_of_objects_dot_notation", + filter={"$nor": [{"items.x": 1}]}, + doc=[ + {"_id": 1, "items": [{"x": 1}, {"x": 2}]}, + {"_id": 2, "items": [{"x": 3}, {"x": 4}]}, + ], + expected=[{"_id": 2, "items": [{"x": 3}, {"x": 4}]}], + msg="$nor with dot notation on array of objects should exclude matching docs", + ), + QueryTestCase( + id="nested_array_path", + filter={"$nor": [{"arr.field": "value"}]}, + doc=[ + {"_id": 1, "arr": [{"field": "value"}, {"field": "other"}]}, + {"_id": 2, "arr": [{"field": "other"}]}, + ], + expected=[{"_id": 2, "arr": [{"field": "other"}]}], + msg="$nor with nested array path should exclude docs with matching element", + ), + QueryTestCase( + id="elemMatch_on_array_of_objects", + filter={"$nor": [{"items": {"$elemMatch": {"qty": {"$gt": 5}, "price": {"$lt": 10}}}}]}, + doc=[ + {"_id": 1, "items": [{"qty": 10, "price": 5}]}, + {"_id": 2, "items": [{"qty": 3, "price": 5}]}, + ], + expected=[{"_id": 2, "items": [{"qty": 3, "price": 5}]}], + msg="$nor with $elemMatch on array of objects should exclude matching docs", + ), + QueryTestCase( + id="all_on_array", + filter={"$nor": [{"tags": {"$all": ["a", "b"]}}]}, + doc=[ + {"_id": 1, "tags": ["a", "b", "c"]}, + {"_id": 2, "tags": ["a", "c"]}, + ], + expected=[{"_id": 2, "tags": ["a", "c"]}], + msg="$nor with $all should exclude docs where array contains all specified elements", + ), +] + +ALL_TESTS = ARRAY_FIELD_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_nor_array_fields(collection, test): + """Test $nor query operator with array fields.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + ignore_doc_order=True, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_command_contexts.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_command_contexts.py new file mode 100644 index 00000000..c3fa07a6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_command_contexts.py @@ -0,0 +1,77 @@ +""" +Tests for $nor query operator in find command contexts. + +Covers $nor in find with projection, sort, and limit/skip. +""" + +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command + +DOCS = [ + {"_id": 1, "val": 5, "status": "a"}, + {"_id": 2, "val": 15, "status": "b"}, + {"_id": 3, "val": 25, "status": "a"}, +] + + +def test_nor_in_find(collection): + """Test $nor in find command filter.""" + collection.insert_many(DOCS) + result = execute_command( + collection, {"find": collection.name, "filter": {"$nor": [{"val": {"$gt": 10}}]}} + ) + assertSuccess( + result, [{"_id": 1, "val": 5, "status": "a"}], msg="$nor should work in find filter" + ) + + +def test_nor_with_projection(collection): + """Test $nor in query with projection.""" + collection.insert_many(DOCS) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"$nor": [{"val": {"$gt": 10}}]}, + "projection": {"val": 1}, + }, + ) + assertSuccess(result, [{"_id": 1, "val": 5}], msg="$nor should work with projection") + + +def test_nor_with_sort(collection): + """Test $nor in query with sort.""" + collection.insert_many(DOCS) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"$nor": [{"status": "b"}]}, + "sort": {"val": -1}, + }, + ) + assertSuccess( + result, + [{"_id": 3, "val": 25, "status": "a"}, {"_id": 1, "val": 5, "status": "a"}], + msg="$nor should work with sort", + ) + + +def test_nor_with_limit_skip(collection): + """Test $nor in query with limit and skip.""" + collection.insert_many(DOCS) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"$nor": [{"status": "b"}]}, + "sort": {"val": 1}, + "limit": 1, + "skip": 1, + }, + ) + assertSuccess( + result, + [{"_id": 3, "val": 25, "status": "a"}], + msg="$nor should work with limit and skip", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_core_logic.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_core_logic.py new file mode 100644 index 00000000..e8cc556c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_core_logic.py @@ -0,0 +1,166 @@ +""" +Tests for $nor query operator core logic. + +Covers basic NOR semantics: documents must fail ALL conditions to be returned, +implicit equality, multiple fields in single expression, and equivalence patterns. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +FOUR_DOCS = [ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 2, "a": 1, "b": 2}, + {"_id": 3, "a": 2, "b": 1}, + {"_id": 4, "a": 2, "b": 2}, +] + +BASIC_NOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="fails_all_conditions", + filter={"$nor": [{"a": 1}, {"b": 1}]}, + doc=FOUR_DOCS, + expected=[{"_id": 4, "a": 2, "b": 2}], + msg="$nor should return only docs that fail ALL conditions", + ), + QueryTestCase( + id="single_condition_negation", + filter={"$nor": [{"a": 1}]}, + doc=FOUR_DOCS, + expected=[{"_id": 3, "a": 2, "b": 1}, {"_id": 4, "a": 2, "b": 2}], + msg="$nor with single condition should negate it", + ), + QueryTestCase( + id="all_docs_match_at_least_one", + filter={"$nor": [{"a": 1}, {"a": 2}]}, + doc=FOUR_DOCS, + expected=[], + msg="$nor should return empty when all docs match at least one condition", + ), + QueryTestCase( + id="no_docs_match_any", + filter={"$nor": [{"a": 99}, {"b": 99}]}, + doc=FOUR_DOCS, + expected=FOUR_DOCS, + msg="$nor should return all docs when none match any condition", + ), + QueryTestCase( + id="three_expressions", + filter={"$nor": [{"a": 1}, {"b": 1}, {"a": 2, "b": 2}]}, + doc=FOUR_DOCS, + expected=[], + msg="$nor with three expressions — docs must fail ALL THREE", + ), + QueryTestCase( + id="duplicate_expressions", + filter={"$nor": [{"a": 1}, {"a": 1}]}, + doc=FOUR_DOCS, + expected=[{"_id": 3, "a": 2, "b": 1}, {"_id": 4, "a": 2, "b": 2}], + msg="$nor with duplicate expressions should behave same as single", + ), +] + +IMPLICIT_EQUALITY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="multiple_equalities_same_field", + filter={"$nor": [{"a": 1}, {"a": 2}]}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}, {"_id": 3, "a": 3}], + expected=[{"_id": 3, "a": 3}], + msg="$nor with multiple equalities on same field excludes all matching values", + ), + QueryTestCase( + id="equalities_different_fields", + filter={"$nor": [{"a": 1}, {"b": 2}]}, + doc=[ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 2, "a": 2, "b": 2}, + {"_id": 3, "a": 2, "b": 1}, + ], + expected=[{"_id": 3, "a": 2, "b": 1}], + msg="$nor with equalities on different fields returns docs failing both", + ), + QueryTestCase( + id="includes_missing_field_docs", + filter={"$nor": [{"a": 1}]}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}, {"_id": 3, "b": 1}], + expected=[{"_id": 2, "a": 2}, {"_id": 3, "b": 1}], + msg="$nor includes docs where referenced field does not exist", + ), +] + +MULTIPLE_FIELDS_IN_EXPRESSION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="implicit_and_in_expression", + filter={"$nor": [{"a": 1, "b": 2}]}, + doc=FOUR_DOCS, + expected=[ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 3, "a": 2, "b": 1}, + {"_id": 4, "a": 2, "b": 2}, + ], + msg="$nor with multiple fields in one expression is implicit AND within", + ), + QueryTestCase( + id="overlapping_field_conditions", + filter={"$nor": [{"a": {"$gt": 5}}, {"a": {"$lt": 2}}]}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 3}, {"_id": 3, "a": 7}], + expected=[{"_id": 2, "a": 3}], + msg="$nor with overlapping conditions returns docs in the gap", + ), + QueryTestCase( + id="conflicting_operators_same_field", + filter={"$nor": [{"val": {"$gt": 10}}, {"val": {"$lt": 5}}, {"val": {"$eq": 7}}]}, + doc=[ + {"_id": 1, "val": 3}, + {"_id": 2, "val": 7}, + {"_id": 3, "val": 8}, + {"_id": 4, "val": 12}, + ], + expected=[{"_id": 3, "val": 8}], + msg="$nor with conflicting operators on same field returns docs failing all", + ), +] + +EQUIVALENCE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="nor_single_equivalent_to_ne", + filter={"$nor": [{"a": 1}]}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}], + expected=[{"_id": 2, "a": 2}], + msg="$nor with single equality is equivalent to $ne", + ), + QueryTestCase( + id="ne_equivalent_to_nor_single", + filter={"a": {"$ne": 1}}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}], + expected=[{"_id": 2, "a": 2}], + msg="$ne should produce same results as $nor with single equality", + ), +] + +ALL_TESTS = ( + BASIC_NOR_TESTS + + IMPLICIT_EQUALITY_TESTS + + MULTIPLE_FIELDS_IN_EXPRESSION_TESTS + + EQUIVALENCE_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_nor_core_logic(collection, test): + """Test $nor query operator core NOR logic.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + ignore_doc_order=True, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py new file mode 100644 index 00000000..4fad6684 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py @@ -0,0 +1,238 @@ +""" +Tests for $nor query operator data type coverage. + +Covers BSON type matching, numeric equivalence across types, +and BSON type distinction (false vs 0, true vs 1, null vs missing). +""" + +from datetime import datetime, timezone + +import pytest +from bson import Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +BSON_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="double", + filter={"$nor": [{"val": 3.14}]}, + doc=[{"_id": 1, "val": 3.14}, {"_id": 2, "val": 2.0}], + expected=[{"_id": 2, "val": 2.0}], + msg="$nor should exclude docs matching double value", + ), + QueryTestCase( + id="string", + filter={"$nor": [{"val": "hello"}]}, + doc=[{"_id": 1, "val": "hello"}, {"_id": 2, "val": "world"}], + expected=[{"_id": 2, "val": "world"}], + msg="$nor should exclude docs matching string value", + ), + QueryTestCase( + id="object", + filter={"$nor": [{"val": {"nested": 1}}]}, + doc=[{"_id": 1, "val": {"nested": 1}}, {"_id": 2, "val": {"nested": 2}}], + expected=[{"_id": 2, "val": {"nested": 2}}], + msg="$nor should exclude docs matching object value", + ), + QueryTestCase( + id="array", + filter={"$nor": [{"val": [1, 2, 3]}]}, + doc=[{"_id": 1, "val": [1, 2, 3]}, {"_id": 2, "val": [4, 5]}], + expected=[{"_id": 2, "val": [4, 5]}], + msg="$nor should exclude docs matching array value", + ), + QueryTestCase( + id="objectid", + filter={"$nor": [{"val": ObjectId("507f1f77bcf86cd799439011")}]}, + doc=[ + {"_id": 1, "val": ObjectId("507f1f77bcf86cd799439011")}, + {"_id": 2, "val": ObjectId("507f1f77bcf86cd799439012")}, + ], + expected=[{"_id": 2, "val": ObjectId("507f1f77bcf86cd799439012")}], + msg="$nor should exclude docs matching ObjectId value", + ), + QueryTestCase( + id="boolean_true", + filter={"$nor": [{"val": True}]}, + doc=[{"_id": 1, "val": True}, {"_id": 2, "val": False}], + expected=[{"_id": 2, "val": False}], + msg="$nor should exclude docs matching boolean true", + ), + QueryTestCase( + id="boolean_false", + filter={"$nor": [{"val": False}]}, + doc=[{"_id": 1, "val": True}, {"_id": 2, "val": False}], + expected=[{"_id": 1, "val": True}], + msg="$nor should exclude docs matching boolean false", + ), + QueryTestCase( + id="date", + filter={"$nor": [{"val": datetime(2024, 1, 1, tzinfo=timezone.utc)}]}, + doc=[ + {"_id": 1, "val": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + {"_id": 2, "val": datetime(2024, 6, 1, tzinfo=timezone.utc)}, + ], + expected=[{"_id": 2, "val": datetime(2024, 6, 1, tzinfo=timezone.utc)}], + msg="$nor should exclude docs matching date value", + ), + QueryTestCase( + id="null", + filter={"$nor": [{"val": None}]}, + doc=[{"_id": 1, "val": None}, {"_id": 2, "val": 5}], + expected=[{"_id": 2, "val": 5}], + msg="$nor should exclude docs matching null value", + ), + QueryTestCase( + id="int32", + filter={"$nor": [{"val": 42}]}, + doc=[{"_id": 1, "val": 42}, {"_id": 2, "val": 99}], + expected=[{"_id": 2, "val": 99}], + msg="$nor should exclude docs matching int32 value", + ), + QueryTestCase( + id="int64", + filter={"$nor": [{"val": Int64(42)}]}, + doc=[{"_id": 1, "val": Int64(42)}, {"_id": 2, "val": Int64(99)}], + expected=[{"_id": 2, "val": Int64(99)}], + msg="$nor should exclude docs matching int64 value", + ), + QueryTestCase( + id="decimal128", + filter={"$nor": [{"val": Decimal128("42.0")}]}, + doc=[{"_id": 1, "val": Decimal128("42.0")}, {"_id": 2, "val": Decimal128("99.0")}], + expected=[{"_id": 2, "val": Decimal128("99.0")}], + msg="$nor should exclude docs matching decimal128 value", + ), + QueryTestCase( + id="timestamp", + filter={"$nor": [{"val": Timestamp(1, 1)}]}, + doc=[{"_id": 1, "val": Timestamp(1, 1)}, {"_id": 2, "val": Timestamp(2, 1)}], + expected=[{"_id": 2, "val": Timestamp(2, 1)}], + msg="$nor should exclude docs matching timestamp value", + ), + QueryTestCase( + id="minkey", + filter={"$nor": [{"val": MinKey()}]}, + doc=[{"_id": 1, "val": MinKey()}, {"_id": 2, "val": 1}], + expected=[{"_id": 2, "val": 1}], + msg="$nor should exclude docs matching MinKey value", + ), + QueryTestCase( + id="maxkey", + filter={"$nor": [{"val": MaxKey()}]}, + doc=[{"_id": 1, "val": MaxKey()}, {"_id": 2, "val": 1}], + expected=[{"_id": 2, "val": 1}], + msg="$nor should exclude docs matching MaxKey value", + ), +] + +BSON_TYPE_BINARY_REGEX_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="binary", + filter={"$nor": [{"val": b"\x01\x02\x03"}]}, + doc=[{"_id": 1, "val": b"\x01\x02\x03"}, {"_id": 2, "val": b"\x04\x05"}], + expected=[{"_id": 2, "val": b"\x04\x05"}], + msg="$nor should exclude docs matching binary value", + ), + QueryTestCase( + id="regex", + filter={"$nor": [{"val": Regex("^hello")}]}, + doc=[{"_id": 1, "val": "hello world"}, {"_id": 2, "val": "goodbye"}], + expected=[{"_id": 2, "val": "goodbye"}], + msg="$nor should exclude docs matching regex value", + ), +] + +NUMERIC_EQUIVALENCE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int32_matches_int64", + filter={"$nor": [{"val": 1}]}, + doc=[{"_id": 1, "val": Int64(1)}, {"_id": 2, "val": Int64(2)}], + expected=[{"_id": 2, "val": Int64(2)}], + msg="$nor int32 condition should match equivalent int64 value", + ), + QueryTestCase( + id="int32_matches_double", + filter={"$nor": [{"val": 1}]}, + doc=[{"_id": 1, "val": 1.0}, {"_id": 2, "val": 2.0}], + expected=[{"_id": 2, "val": 2.0}], + msg="$nor int32 condition should match equivalent double value", + ), + QueryTestCase( + id="int32_matches_decimal128", + filter={"$nor": [{"val": 1}]}, + doc=[{"_id": 1, "val": Decimal128("1")}, {"_id": 2, "val": Decimal128("2")}], + expected=[{"_id": 2, "val": Decimal128("2")}], + msg="$nor int32 condition should match equivalent decimal128 value", + ), + QueryTestCase( + id="in_across_numeric_types", + filter={"$nor": [{"val": {"$in": [1, Int64(1), 1.0, Decimal128("1")]}}]}, + doc=[ + {"_id": 1, "val": 1}, + {"_id": 2, "val": Int64(1)}, + {"_id": 3, "val": 1.0}, + {"_id": 4, "val": Decimal128("1")}, + {"_id": 5, "val": 2}, + ], + expected=[{"_id": 5, "val": 2}], + msg="$nor with $in across numeric types should exclude all numerically equivalent values", + ), +] + +BSON_TYPE_DISTINCTION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="false_not_equal_to_zero", + filter={"$nor": [{"val": False}]}, + doc=[{"_id": 1, "val": False}, {"_id": 2, "val": 0}], + expected=[{"_id": 2, "val": 0}], + msg="$nor with false should NOT exclude docs with val=0 (type distinction)", + ), + QueryTestCase( + id="true_not_equal_to_one", + filter={"$nor": [{"val": True}]}, + doc=[{"_id": 1, "val": True}, {"_id": 2, "val": 1}], + expected=[{"_id": 2, "val": 1}], + msg="$nor with true should NOT exclude docs with val=1 (type distinction)", + ), + QueryTestCase( + id="null_matches_missing", + filter={"$nor": [{"val": None}]}, + doc=[{"_id": 1, "val": None}, {"_id": 2, "other": 1}, {"_id": 3, "val": 5}], + expected=[{"_id": 3, "val": 5}], + msg="$nor with null excludes both null and missing field docs", + ), + QueryTestCase( + id="empty_string_not_equal_to_null", + filter={"$nor": [{"val": ""}]}, + doc=[{"_id": 1, "val": ""}, {"_id": 2, "val": None}], + expected=[{"_id": 2, "val": None}], + msg="$nor with empty string should NOT exclude docs with val=null", + ), +] + +ALL_TESTS = ( + BSON_TYPE_TESTS + + BSON_TYPE_BINARY_REGEX_TESTS + + NUMERIC_EQUIVALENCE_TESTS + + BSON_TYPE_DISTINCTION_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_nor_data_types(collection, test): + """Test $nor query operator data type coverage.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + ignore_doc_order=True, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_nested_operators.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_nested_operators.py new file mode 100644 index 00000000..6afc4a57 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_nested_operators.py @@ -0,0 +1,206 @@ +""" +Tests for $nor query operator with nested operators. + +Covers comparison operators, array operators, $regex, $type, $mod, $expr, +and nested logical operators ($and, $or, $not) within $nor expressions. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +COMPARISON_OPERATOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="with_gt", + filter={"$nor": [{"val": {"$gt": 10}}]}, + doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 15}], + expected=[{"_id": 1, "val": 5}], + msg="$nor with $gt should exclude docs where val > 10", + ), + QueryTestCase( + id="with_gte", + filter={"$nor": [{"val": {"$gte": 10}}]}, + doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 10}, {"_id": 3, "val": 15}], + expected=[{"_id": 1, "val": 5}], + msg="$nor with $gte should exclude docs where val >= 10", + ), + QueryTestCase( + id="with_lt", + filter={"$nor": [{"val": {"$lt": 10}}]}, + doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 15}], + expected=[{"_id": 2, "val": 15}], + msg="$nor with $lt should exclude docs where val < 10", + ), + QueryTestCase( + id="with_lte", + filter={"$nor": [{"val": {"$lte": 10}}]}, + doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 10}, {"_id": 3, "val": 15}], + expected=[{"_id": 3, "val": 15}], + msg="$nor with $lte should exclude docs where val <= 10", + ), + QueryTestCase( + id="with_in", + filter={"$nor": [{"val": {"$in": [1, 2, 3]}}]}, + doc=[{"_id": 1, "val": 1}, {"_id": 2, "val": 4}], + expected=[{"_id": 2, "val": 4}], + msg="$nor with $in should exclude docs where val is in list", + ), + QueryTestCase( + id="with_nin", + filter={"$nor": [{"val": {"$nin": [1, 2, 3]}}]}, + doc=[{"_id": 1, "val": 1}, {"_id": 2, "val": 4}], + expected=[{"_id": 1, "val": 1}], + msg="$nor with $nin should exclude docs where val is NOT in list", + ), +] + +ARRAY_OPERATOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="with_size", + filter={"$nor": [{"arr": {"$size": 3}}]}, + doc=[{"_id": 1, "arr": [1, 2, 3]}, {"_id": 2, "arr": [1, 2]}], + expected=[{"_id": 2, "arr": [1, 2]}], + msg="$nor with $size should exclude docs where array has specified size", + ), + QueryTestCase( + id="with_elemMatch", + filter={"$nor": [{"arr": {"$elemMatch": {"$gt": 5}}}]}, + doc=[{"_id": 1, "arr": [1, 2, 3]}, {"_id": 2, "arr": [1, 10]}], + expected=[{"_id": 1, "arr": [1, 2, 3]}], + msg="$nor with $elemMatch should exclude docs where any element matches", + ), + QueryTestCase( + id="with_all", + filter={"$nor": [{"arr": {"$all": [1, 2]}}]}, + doc=[{"_id": 1, "arr": [1, 2, 3]}, {"_id": 2, "arr": [1, 3]}], + expected=[{"_id": 2, "arr": [1, 3]}], + msg="$nor with $all should exclude docs where array contains all elements", + ), +] + +OTHER_OPERATOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="with_regex", + filter={"$nor": [{"name": {"$regex": "^A"}}]}, + doc=[{"_id": 1, "name": "Alice"}, {"_id": 2, "name": "Bob"}], + expected=[{"_id": 2, "name": "Bob"}], + msg="$nor with $regex should exclude docs matching pattern", + ), + QueryTestCase( + id="with_type", + filter={"$nor": [{"val": {"$type": "string"}}]}, + doc=[{"_id": 1, "val": "hello"}, {"_id": 2, "val": 42}], + expected=[{"_id": 2, "val": 42}], + msg="$nor with $type should exclude docs where field is specified type", + ), + QueryTestCase( + id="with_mod", + filter={"$nor": [{"val": {"$mod": [2, 0]}}]}, + doc=[{"_id": 1, "val": 4}, {"_id": 2, "val": 5}], + expected=[{"_id": 2, "val": 5}], + msg="$nor with $mod should exclude docs where val satisfies modulo", + ), + QueryTestCase( + id="with_expr", + filter={"$nor": [{"$expr": {"$gt": ["$a", "$b"]}}]}, + doc=[{"_id": 1, "a": 10, "b": 5}, {"_id": 2, "a": 3, "b": 7}], + expected=[{"_id": 2, "a": 3, "b": 7}], + msg="$nor with $expr should exclude docs where expression is true", + ), +] + +NESTED_LOGICAL_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="containing_and", + filter={"$nor": [{"$and": [{"a": 1}, {"b": 2}]}]}, + doc=[ + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 1, "b": 1}, + {"_id": 3, "a": 2, "b": 2}, + ], + expected=[{"_id": 2, "a": 1, "b": 1}, {"_id": 3, "a": 2, "b": 2}], + msg="$nor containing $and should exclude docs matching the AND condition", + ), + QueryTestCase( + id="containing_or", + filter={"$nor": [{"$or": [{"a": 1}, {"b": 2}]}]}, + doc=[ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 2, "a": 2, "b": 2}, + {"_id": 3, "a": 2, "b": 1}, + ], + expected=[{"_id": 3, "a": 2, "b": 1}], + msg="$nor containing $or should exclude docs matching any OR condition", + ), + QueryTestCase( + id="containing_not", + filter={"$nor": [{"val": {"$not": {"$gt": 10}}}]}, + doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 15}], + expected=[{"_id": 2, "val": 15}], + msg="$nor containing $not — double negation: excludes docs where val <= 10", + ), + QueryTestCase( + id="nested_inside_and", + filter={"$and": [{"$nor": [{"a": 1}]}, {"b": 2}]}, + doc=[ + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 2, "b": 2}, + {"_id": 3, "a": 2, "b": 1}, + ], + expected=[{"_id": 2, "a": 2, "b": 2}], + msg="$nor nested inside $and should combine with other conditions", + ), + QueryTestCase( + id="nested_inside_or", + filter={"$or": [{"$nor": [{"a": 1}]}, {"b": 2}]}, + doc=[ + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 2, "b": 1}, + {"_id": 3, "a": 1, "b": 1}, + ], + expected=[{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 2, "b": 1}], + msg="$nor nested inside $or should be one branch of the OR", + ), +] + +COMBINED_WITH_TOP_LEVEL_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="combined_with_top_level_field", + filter={"status": "active", "$nor": [{"priority": "low"}, {"age": {"$gt": 30}}]}, + doc=[ + {"_id": 1, "status": "active", "priority": "low", "age": 25}, + {"_id": 2, "status": "active", "priority": "high", "age": 35}, + {"_id": 3, "status": "active", "priority": "high", "age": 25}, + {"_id": 4, "status": "inactive", "priority": "high", "age": 25}, + ], + expected=[{"_id": 3, "status": "active", "priority": "high", "age": 25}], + msg="$nor combined with top-level field conditions should apply both", + ), +] + +ALL_TESTS = ( + COMPARISON_OPERATOR_TESTS + + ARRAY_OPERATOR_TESTS + + OTHER_OPERATOR_TESTS + + NESTED_LOGICAL_TESTS + + COMBINED_WITH_TOP_LEVEL_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_nor_nested_operators(collection, test): + """Test $nor query operator with nested operators.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + ignore_doc_order=True, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py new file mode 100644 index 00000000..e1d76225 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py @@ -0,0 +1,101 @@ +""" +Tests for $nor query operator null and missing field handling. + +Covers $nor behavior with null values, missing fields, and $exists interaction. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +NULL_MISSING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="missing_field_returned", + filter={"$nor": [{"val": 5}]}, + doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 10}, {"_id": 3, "other": 1}], + expected=[{"_id": 2, "val": 10}, {"_id": 3, "other": 1}], + msg="$nor should return docs where field does not exist (fails the match)", + ), + QueryTestCase( + id="null_field_excluded_by_null_match", + filter={"$nor": [{"val": None}]}, + doc=[{"_id": 1, "val": None}, {"_id": 2, "val": 5}, {"_id": 3, "other": 1}], + expected=[{"_id": 2, "val": 5}], + msg="$nor with null condition excludes docs with val=null AND missing field docs", + ), + QueryTestCase( + id="null_field_with_gt_operator", + filter={"$nor": [{"val": {"$gt": 5}}]}, + doc=[{"_id": 1, "val": None}, {"_id": 2, "val": 10}, {"_id": 3, "val": 3}], + expected=[{"_id": 1, "val": None}, {"_id": 3, "val": 3}], + msg="$nor with $gt — docs with val=null are returned (null doesn't satisfy $gt)", + ), +] + +EXISTS_INTERACTION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="exists_true", + filter={"$nor": [{"val": {"$exists": True}}]}, + doc=[{"_id": 1, "val": 5}, {"_id": 2, "other": 1}], + expected=[{"_id": 2, "other": 1}], + msg="$nor with $exists:true returns only docs without the field", + ), + QueryTestCase( + id="exists_false", + filter={"$nor": [{"val": {"$exists": False}}]}, + doc=[{"_id": 1, "val": 5}, {"_id": 2, "other": 1}], + expected=[{"_id": 1, "val": 5}], + msg="$nor with $exists:false returns only docs with the field", + ), + QueryTestCase( + id="value_and_exists_false_combined", + filter={"$nor": [{"price": 1.99}, {"price": {"$exists": False}}]}, + doc=[ + {"_id": 1, "price": 1.99}, + {"_id": 2, "price": 5.00}, + {"_id": 3, "other": 1}, + ], + expected=[{"_id": 2, "price": 5.00}], + msg="$nor combining value and $exists:false returns docs where field exists AND != match", + ), + QueryTestCase( + id="multiple_fields_with_exists", + filter={ + "$nor": [ + {"price": 1.99}, + {"price": {"$exists": False}}, + {"sale": True}, + {"sale": {"$exists": False}}, + ] + }, + doc=[ + {"_id": 1, "price": 1.99, "sale": False}, + {"_id": 2, "price": 5.00, "sale": True}, + {"_id": 3, "price": 5.00, "sale": False}, + {"_id": 4, "price": 5.00}, + ], + expected=[{"_id": 3, "price": 5.00, "sale": False}], + msg="$nor with multiple fields & $exists returns docs where both exist, neither matches", + ), +] + +ALL_TESTS = NULL_MISSING_TESTS + EXISTS_INTERACTION_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_nor_null_missing(collection, test): + """Test $nor query operator null and missing field handling.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + ignore_doc_order=True, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py new file mode 100644 index 00000000..c26bfe54 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py @@ -0,0 +1,112 @@ +""" +Tests for $nor query operator with special values and edge cases. + +Covers NaN, Infinity, -Infinity, negative zero, Decimal128 special values, +empty collections, non-existent fields, and deeply nested field paths. +""" + +import pytest +from bson import Decimal128 + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +SPECIAL_VALUE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="nan_excluded_by_nan_match", + filter={"$nor": [{"val": float("nan")}]}, + doc=[{"_id": 1, "val": float("nan")}, {"_id": 2, "val": 5}], + expected=[{"_id": 2, "val": 5}], + msg="$nor with NaN should exclude docs with NaN value", + ), + QueryTestCase( + id="infinity_excluded_by_gt", + filter={"$nor": [{"val": {"$gt": 1000000}}]}, + doc=[{"_id": 1, "val": float("inf")}, {"_id": 2, "val": 100}], + expected=[{"_id": 2, "val": 100}], + msg="$nor with $gt should exclude Infinity (Infinity > any number)", + ), + QueryTestCase( + id="negative_infinity_excluded_by_lt", + filter={"$nor": [{"val": {"$lt": -1000000}}]}, + doc=[{"_id": 1, "val": float("-inf")}, {"_id": 2, "val": -100}], + expected=[{"_id": 2, "val": -100}], + msg="$nor with $lt should exclude -Infinity (-Infinity < any number)", + ), + QueryTestCase( + id="negative_zero", + filter={"$nor": [{"val": 0}]}, + doc=[{"_id": 1, "val": -0.0}, {"_id": 2, "val": 1}], + expected=[{"_id": 2, "val": 1}], + msg="$nor with 0 should exclude docs with -0.0 (negative zero equals zero)", + ), + QueryTestCase( + id="decimal128_nan", + filter={"$nor": [{"val": Decimal128("NaN")}]}, + doc=[{"_id": 1, "val": Decimal128("NaN")}, {"_id": 2, "val": Decimal128("5")}], + expected=[{"_id": 2, "val": Decimal128("5")}], + msg="$nor with Decimal128 NaN should exclude matching docs", + ), + QueryTestCase( + id="decimal128_infinity", + filter={"$nor": [{"val": Decimal128("Infinity")}]}, + doc=[{"_id": 1, "val": Decimal128("Infinity")}, {"_id": 2, "val": Decimal128("5")}], + expected=[{"_id": 2, "val": Decimal128("5")}], + msg="$nor with Decimal128 Infinity should exclude matching docs", + ), +] + +EDGE_CASE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="empty_collection", + filter={"$nor": [{"a": 1}]}, + doc=[], + expected=[], + msg="$nor on empty collection should return empty result", + ), + QueryTestCase( + id="all_non_existent_fields", + filter={"$nor": [{"x": 1}, {"y": 2}]}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "b": 2}], + expected=[{"_id": 1, "a": 1}, {"_id": 2, "b": 2}], + msg="$nor with all non-existent fields should return all documents", + ), + QueryTestCase( + id="deeply_nested_field_path", + filter={"$nor": [{"a.b.c.d": 1}]}, + doc=[ + {"_id": 1, "a": {"b": {"c": {"d": 1}}}}, + {"_id": 2, "a": {"b": {"c": {"d": 2}}}}, + ], + expected=[{"_id": 2, "a": {"b": {"c": {"d": 2}}}}], + msg="$nor with deeply nested field path should work correctly", + ), + QueryTestCase( + id="large_number_of_expressions", + filter={"$nor": [{"a": i} for i in range(50)]}, + doc=[{"_id": 1, "a": 99}, {"_id": 2, "a": 5}], + expected=[{"_id": 1, "a": 99}], + msg="$nor with 50 expressions should work without error", + ), +] + +ALL_TESTS = SPECIAL_VALUE_TESTS + EDGE_CASE_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_nor_special_values(collection, test): + """Test $nor query operator with special values and edge cases.""" + if test.doc: + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + ignore_doc_order=True, + msg=test.msg, + ) From 8b25d6e140f7fd7e8aed77cdee97e89fbaf323fa Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Wed, 13 May 2026 11:31:15 -0700 Subject: [PATCH 02/11] Removed out of scope cases, added some missing coverage of edge cases Signed-off-by: PatersonProjects --- .../logical/nor/test_nor_argument_handling.py | 108 +------------- .../logical/nor/test_nor_command_contexts.py | 77 ---------- .../query/logical/nor/test_nor_data_types.py | 16 +- .../query/logical/nor/test_nor_errors.py | 138 ++++++++++++++++++ .../logical/nor/test_nor_null_missing.py | 7 + .../logical/nor/test_nor_special_values.py | 12 ++ 6 files changed, 175 insertions(+), 183 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_command_contexts.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py index 8b139122..462d795e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py @@ -1,8 +1,8 @@ """ Tests for $nor query operator argument handling. -Covers array argument validation, expression element validation, -and error cases for invalid argument types. +Covers valid array argument variations: single expression, multiple expressions, +many expressions, and empty object in array. """ import pytest @@ -16,83 +16,7 @@ DOCS = [{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 2, "b": 1}] -INVALID_ARGUMENT_TYPE_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="non_array_object", - filter={"$nor": {"price": 1.99}}, - doc=DOCS, - error_code=2, - msg="$nor with non-array object argument should return BadValue error", - ), - QueryTestCase( - id="null_argument", - filter={"$nor": None}, - doc=DOCS, - error_code=2, - msg="$nor with null argument should return BadValue error", - ), - QueryTestCase( - id="string_argument", - filter={"$nor": "invalid"}, - doc=DOCS, - error_code=2, - msg="$nor with string argument should return BadValue error", - ), - QueryTestCase( - id="numeric_argument", - filter={"$nor": 123}, - doc=DOCS, - error_code=2, - msg="$nor with numeric argument should return BadValue error", - ), - QueryTestCase( - id="boolean_argument", - filter={"$nor": True}, - doc=DOCS, - error_code=2, - msg="$nor with boolean argument should return BadValue error", - ), -] - -INVALID_ELEMENT_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="non_object_element_integer", - filter={"$nor": [123]}, - doc=DOCS, - error_code=2, - msg="$nor with integer element in array should return BadValue error", - ), - QueryTestCase( - id="non_object_element_string", - filter={"$nor": ["invalid"]}, - doc=DOCS, - error_code=2, - msg="$nor with string element in array should return BadValue error", - ), - QueryTestCase( - id="non_object_element_null", - filter={"$nor": [None]}, - doc=DOCS, - error_code=2, - msg="$nor with null element in array should return BadValue error", - ), - QueryTestCase( - id="non_object_element_array", - filter={"$nor": [[{"a": 1}]]}, - doc=DOCS, - error_code=2, - msg="$nor with array element in array should return BadValue error", - ), -] - VALID_ARRAY_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="empty_array", - filter={"$nor": []}, - doc=DOCS, - error_code=2, - msg="$nor with empty array should return BadValue error", - ), QueryTestCase( id="single_expression", filter={"$nor": [{"a": 1}]}, @@ -123,33 +47,7 @@ ), ] -ERROR_HANDLING_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="non_top_level_position", - filter={"field": {"$nor": [{"a": 1}]}}, - doc=DOCS, - error_code=2, - msg="$nor at non-top-level position should return BadValue error", - ), - QueryTestCase( - id="invalid_operator_inside_expression", - filter={"$nor": [{"val": {"$invalid": 1}}]}, - doc=DOCS, - error_code=2, - msg="$nor with invalid operator inside expression should return BadValue error", - ), - QueryTestCase( - id="mixed_valid_invalid_expressions", - filter={"$nor": [{"a": 1}, {"val": {"$invalid": 1}}]}, - doc=DOCS, - error_code=2, - msg="$nor with mixed valid/invalid expressions should return BadValue error", - ), -] - -ALL_TESTS = ( - INVALID_ARGUMENT_TYPE_TESTS + INVALID_ELEMENT_TESTS + VALID_ARRAY_TESTS + ERROR_HANDLING_TESTS -) +ALL_TESTS = VALID_ARRAY_TESTS @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_command_contexts.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_command_contexts.py deleted file mode 100644 index c3fa07a6..00000000 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_command_contexts.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Tests for $nor query operator in find command contexts. - -Covers $nor in find with projection, sort, and limit/skip. -""" - -from documentdb_tests.framework.assertions import assertSuccess -from documentdb_tests.framework.executor import execute_command - -DOCS = [ - {"_id": 1, "val": 5, "status": "a"}, - {"_id": 2, "val": 15, "status": "b"}, - {"_id": 3, "val": 25, "status": "a"}, -] - - -def test_nor_in_find(collection): - """Test $nor in find command filter.""" - collection.insert_many(DOCS) - result = execute_command( - collection, {"find": collection.name, "filter": {"$nor": [{"val": {"$gt": 10}}]}} - ) - assertSuccess( - result, [{"_id": 1, "val": 5, "status": "a"}], msg="$nor should work in find filter" - ) - - -def test_nor_with_projection(collection): - """Test $nor in query with projection.""" - collection.insert_many(DOCS) - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"$nor": [{"val": {"$gt": 10}}]}, - "projection": {"val": 1}, - }, - ) - assertSuccess(result, [{"_id": 1, "val": 5}], msg="$nor should work with projection") - - -def test_nor_with_sort(collection): - """Test $nor in query with sort.""" - collection.insert_many(DOCS) - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"$nor": [{"status": "b"}]}, - "sort": {"val": -1}, - }, - ) - assertSuccess( - result, - [{"_id": 3, "val": 25, "status": "a"}, {"_id": 1, "val": 5, "status": "a"}], - msg="$nor should work with sort", - ) - - -def test_nor_with_limit_skip(collection): - """Test $nor in query with limit and skip.""" - collection.insert_many(DOCS) - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"$nor": [{"status": "b"}]}, - "sort": {"val": 1}, - "limit": 1, - "skip": 1, - }, - ) - assertSuccess( - result, - [{"_id": 3, "val": 25, "status": "a"}], - msg="$nor should work with limit and skip", - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py index 4fad6684..51e33976 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py @@ -8,7 +8,7 @@ from datetime import datetime, timezone import pytest -from bson import Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp +from bson import Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( QueryTestCase, @@ -131,6 +131,19 @@ ), ] +BSON_TYPE_CODE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="javascript_code", + filter={"$nor": [{"val": Code("function() { return 1; }")}]}, + doc=[ + {"_id": 1, "val": Code("function() { return 1; }")}, + {"_id": 2, "val": Code("function() { return 2; }")}, + ], + expected=[{"_id": 2, "val": Code("function() { return 2; }")}], + msg="$nor should exclude docs matching JavaScript Code value", + ), +] + BSON_TYPE_BINARY_REGEX_TESTS: list[QueryTestCase] = [ QueryTestCase( id="binary", @@ -218,6 +231,7 @@ ALL_TESTS = ( BSON_TYPE_TESTS + + BSON_TYPE_CODE_TESTS + BSON_TYPE_BINARY_REGEX_TESTS + NUMERIC_EQUIVALENCE_TESTS + BSON_TYPE_DISTINCTION_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py new file mode 100644 index 00000000..8b516ce9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py @@ -0,0 +1,138 @@ +""" +Tests for $nor query operator error handling. + +Covers invalid argument types, invalid array element types, +empty array, non-top-level usage, and invalid operators inside expressions. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +DOCS = [{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 2, "b": 1}] + +INVALID_ARGUMENT_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="non_array_object", + filter={"$nor": {"price": 1.99}}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with non-array object argument should return BadValue error", + ), + QueryTestCase( + id="null_argument", + filter={"$nor": None}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with null argument should return BadValue error", + ), + QueryTestCase( + id="string_argument", + filter={"$nor": "invalid"}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with string argument should return BadValue error", + ), + QueryTestCase( + id="numeric_argument", + filter={"$nor": 123}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with numeric argument should return BadValue error", + ), + QueryTestCase( + id="boolean_argument", + filter={"$nor": True}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with boolean argument should return BadValue error", + ), +] + +INVALID_ELEMENT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="non_object_element_integer", + filter={"$nor": [123]}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with integer element in array should return BadValue error", + ), + QueryTestCase( + id="non_object_element_string", + filter={"$nor": ["invalid"]}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with string element in array should return BadValue error", + ), + QueryTestCase( + id="non_object_element_null", + filter={"$nor": [None]}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with null element in array should return BadValue error", + ), + QueryTestCase( + id="non_object_element_array", + filter={"$nor": [[{"a": 1}]]}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with array element in array should return BadValue error", + ), +] + +EMPTY_ARRAY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="empty_array", + filter={"$nor": []}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with empty array should return BadValue error", + ), +] + +ERROR_HANDLING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="non_top_level_position", + filter={"field": {"$nor": [{"a": 1}]}}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor at non-top-level position should return BadValue error", + ), + QueryTestCase( + id="invalid_operator_inside_expression", + filter={"$nor": [{"val": {"$invalid": 1}}]}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with invalid operator inside expression should return BadValue error", + ), + QueryTestCase( + id="mixed_valid_invalid_expressions", + filter={"$nor": [{"a": 1}, {"val": {"$invalid": 1}}]}, + doc=DOCS, + error_code=BAD_VALUE_ERROR, + msg="$nor with mixed valid/invalid expressions should return BadValue error", + ), +] + +ALL_TESTS = ( + INVALID_ARGUMENT_TYPE_TESTS + INVALID_ELEMENT_TESTS + EMPTY_ARRAY_TESTS + ERROR_HANDLING_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_nor_errors(collection, test): + """Test $nor query operator error handling.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py index e1d76225..2fe128ed 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py @@ -28,6 +28,13 @@ expected=[{"_id": 2, "val": 5}], msg="$nor with null condition excludes docs with val=null AND missing field docs", ), + QueryTestCase( + id="type_null_excludes_null_and_missing", + filter={"$nor": [{"val": {"$type": "null"}}]}, + doc=[{"_id": 1, "val": None}, {"_id": 2, "val": 5}, {"_id": 3, "other": 1}], + expected=[{"_id": 2, "val": 5}, {"_id": 3, "other": 1}], + msg="$nor with $type null excludes docs where val is null but not missing-field docs", + ), QueryTestCase( id="null_field_with_gt_operator", filter={"$nor": [{"val": {"$gt": 5}}]}, diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py index c26bfe54..bb46a18a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py @@ -44,6 +44,18 @@ expected=[{"_id": 2, "val": 1}], msg="$nor with 0 should exclude docs with -0.0 (negative zero equals zero)", ), + QueryTestCase( + id="nan_included_when_filtering_numbers", + filter={"$nor": [{"val": 5}, {"val": 10}]}, + doc=[ + {"_id": 1, "val": float("nan")}, + {"_id": 2, "val": 5}, + {"_id": 3, "val": 10}, + {"_id": 4, "val": 20}, + ], + expected=[{"_id": 1, "val": float("nan")}, {"_id": 4, "val": 20}], + msg="$nor filtering numeric values should include NaN doc (NaN != any number)", + ), QueryTestCase( id="decimal128_nan", filter={"$nor": [{"val": Decimal128("NaN")}]}, From a0432c220515aec4c5041ed8483d89f248c5cc89 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Wed, 13 May 2026 14:12:58 -0700 Subject: [PATCH 03/11] Removed nested operators (covered by and PR) Signed-off-by: PatersonProjects --- .../logical/nor/test_nor_nested_operators.py | 206 ------------------ 1 file changed, 206 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_nested_operators.py diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_nested_operators.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_nested_operators.py deleted file mode 100644 index 6afc4a57..00000000 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_nested_operators.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -Tests for $nor query operator with nested operators. - -Covers comparison operators, array operators, $regex, $type, $mod, $expr, -and nested logical operators ($and, $or, $not) within $nor expressions. -""" - -import pytest - -from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( - QueryTestCase, -) -from documentdb_tests.framework.assertions import assertResult -from documentdb_tests.framework.executor import execute_command -from documentdb_tests.framework.parametrize import pytest_params - -COMPARISON_OPERATOR_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="with_gt", - filter={"$nor": [{"val": {"$gt": 10}}]}, - doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 15}], - expected=[{"_id": 1, "val": 5}], - msg="$nor with $gt should exclude docs where val > 10", - ), - QueryTestCase( - id="with_gte", - filter={"$nor": [{"val": {"$gte": 10}}]}, - doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 10}, {"_id": 3, "val": 15}], - expected=[{"_id": 1, "val": 5}], - msg="$nor with $gte should exclude docs where val >= 10", - ), - QueryTestCase( - id="with_lt", - filter={"$nor": [{"val": {"$lt": 10}}]}, - doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 15}], - expected=[{"_id": 2, "val": 15}], - msg="$nor with $lt should exclude docs where val < 10", - ), - QueryTestCase( - id="with_lte", - filter={"$nor": [{"val": {"$lte": 10}}]}, - doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 10}, {"_id": 3, "val": 15}], - expected=[{"_id": 3, "val": 15}], - msg="$nor with $lte should exclude docs where val <= 10", - ), - QueryTestCase( - id="with_in", - filter={"$nor": [{"val": {"$in": [1, 2, 3]}}]}, - doc=[{"_id": 1, "val": 1}, {"_id": 2, "val": 4}], - expected=[{"_id": 2, "val": 4}], - msg="$nor with $in should exclude docs where val is in list", - ), - QueryTestCase( - id="with_nin", - filter={"$nor": [{"val": {"$nin": [1, 2, 3]}}]}, - doc=[{"_id": 1, "val": 1}, {"_id": 2, "val": 4}], - expected=[{"_id": 1, "val": 1}], - msg="$nor with $nin should exclude docs where val is NOT in list", - ), -] - -ARRAY_OPERATOR_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="with_size", - filter={"$nor": [{"arr": {"$size": 3}}]}, - doc=[{"_id": 1, "arr": [1, 2, 3]}, {"_id": 2, "arr": [1, 2]}], - expected=[{"_id": 2, "arr": [1, 2]}], - msg="$nor with $size should exclude docs where array has specified size", - ), - QueryTestCase( - id="with_elemMatch", - filter={"$nor": [{"arr": {"$elemMatch": {"$gt": 5}}}]}, - doc=[{"_id": 1, "arr": [1, 2, 3]}, {"_id": 2, "arr": [1, 10]}], - expected=[{"_id": 1, "arr": [1, 2, 3]}], - msg="$nor with $elemMatch should exclude docs where any element matches", - ), - QueryTestCase( - id="with_all", - filter={"$nor": [{"arr": {"$all": [1, 2]}}]}, - doc=[{"_id": 1, "arr": [1, 2, 3]}, {"_id": 2, "arr": [1, 3]}], - expected=[{"_id": 2, "arr": [1, 3]}], - msg="$nor with $all should exclude docs where array contains all elements", - ), -] - -OTHER_OPERATOR_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="with_regex", - filter={"$nor": [{"name": {"$regex": "^A"}}]}, - doc=[{"_id": 1, "name": "Alice"}, {"_id": 2, "name": "Bob"}], - expected=[{"_id": 2, "name": "Bob"}], - msg="$nor with $regex should exclude docs matching pattern", - ), - QueryTestCase( - id="with_type", - filter={"$nor": [{"val": {"$type": "string"}}]}, - doc=[{"_id": 1, "val": "hello"}, {"_id": 2, "val": 42}], - expected=[{"_id": 2, "val": 42}], - msg="$nor with $type should exclude docs where field is specified type", - ), - QueryTestCase( - id="with_mod", - filter={"$nor": [{"val": {"$mod": [2, 0]}}]}, - doc=[{"_id": 1, "val": 4}, {"_id": 2, "val": 5}], - expected=[{"_id": 2, "val": 5}], - msg="$nor with $mod should exclude docs where val satisfies modulo", - ), - QueryTestCase( - id="with_expr", - filter={"$nor": [{"$expr": {"$gt": ["$a", "$b"]}}]}, - doc=[{"_id": 1, "a": 10, "b": 5}, {"_id": 2, "a": 3, "b": 7}], - expected=[{"_id": 2, "a": 3, "b": 7}], - msg="$nor with $expr should exclude docs where expression is true", - ), -] - -NESTED_LOGICAL_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="containing_and", - filter={"$nor": [{"$and": [{"a": 1}, {"b": 2}]}]}, - doc=[ - {"_id": 1, "a": 1, "b": 2}, - {"_id": 2, "a": 1, "b": 1}, - {"_id": 3, "a": 2, "b": 2}, - ], - expected=[{"_id": 2, "a": 1, "b": 1}, {"_id": 3, "a": 2, "b": 2}], - msg="$nor containing $and should exclude docs matching the AND condition", - ), - QueryTestCase( - id="containing_or", - filter={"$nor": [{"$or": [{"a": 1}, {"b": 2}]}]}, - doc=[ - {"_id": 1, "a": 1, "b": 1}, - {"_id": 2, "a": 2, "b": 2}, - {"_id": 3, "a": 2, "b": 1}, - ], - expected=[{"_id": 3, "a": 2, "b": 1}], - msg="$nor containing $or should exclude docs matching any OR condition", - ), - QueryTestCase( - id="containing_not", - filter={"$nor": [{"val": {"$not": {"$gt": 10}}}]}, - doc=[{"_id": 1, "val": 5}, {"_id": 2, "val": 15}], - expected=[{"_id": 2, "val": 15}], - msg="$nor containing $not — double negation: excludes docs where val <= 10", - ), - QueryTestCase( - id="nested_inside_and", - filter={"$and": [{"$nor": [{"a": 1}]}, {"b": 2}]}, - doc=[ - {"_id": 1, "a": 1, "b": 2}, - {"_id": 2, "a": 2, "b": 2}, - {"_id": 3, "a": 2, "b": 1}, - ], - expected=[{"_id": 2, "a": 2, "b": 2}], - msg="$nor nested inside $and should combine with other conditions", - ), - QueryTestCase( - id="nested_inside_or", - filter={"$or": [{"$nor": [{"a": 1}]}, {"b": 2}]}, - doc=[ - {"_id": 1, "a": 1, "b": 2}, - {"_id": 2, "a": 2, "b": 1}, - {"_id": 3, "a": 1, "b": 1}, - ], - expected=[{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 2, "b": 1}], - msg="$nor nested inside $or should be one branch of the OR", - ), -] - -COMBINED_WITH_TOP_LEVEL_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="combined_with_top_level_field", - filter={"status": "active", "$nor": [{"priority": "low"}, {"age": {"$gt": 30}}]}, - doc=[ - {"_id": 1, "status": "active", "priority": "low", "age": 25}, - {"_id": 2, "status": "active", "priority": "high", "age": 35}, - {"_id": 3, "status": "active", "priority": "high", "age": 25}, - {"_id": 4, "status": "inactive", "priority": "high", "age": 25}, - ], - expected=[{"_id": 3, "status": "active", "priority": "high", "age": 25}], - msg="$nor combined with top-level field conditions should apply both", - ), -] - -ALL_TESTS = ( - COMPARISON_OPERATOR_TESTS - + ARRAY_OPERATOR_TESTS - + OTHER_OPERATOR_TESTS - + NESTED_LOGICAL_TESTS - + COMBINED_WITH_TOP_LEVEL_TESTS -) - - -@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) -def test_nor_nested_operators(collection, test): - """Test $nor query operator with nested operators.""" - collection.insert_many(test.doc) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertResult( - result, - expected=test.expected, - error_code=test.error_code, - ignore_doc_order=True, - msg=test.msg, - ) From aaa6ba438aa0829ad2cd4aae7b10329ac46dd7d2 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Wed, 13 May 2026 15:21:12 -0700 Subject: [PATCH 04/11] Moved tests to more concisely group together Signed-off-by: PatersonProjects --- .../logical/nor/test_nor_argument_handling.py | 83 ++++++++- .../query/logical/nor/test_nor_core_logic.py | 166 ------------------ .../logical/nor/test_nor_null_missing.py | 15 +- 3 files changed, 92 insertions(+), 172 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_core_logic.py diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py index 462d795e..49a7a0e2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py @@ -2,7 +2,7 @@ Tests for $nor query operator argument handling. Covers valid array argument variations: single expression, multiple expressions, -many expressions, and empty object in array. +many expressions, empty object in array, and multiple fields in a single expression. """ import pytest @@ -38,6 +38,47 @@ expected=[{"_id": 2, "a": 2, "b": 2}], msg="$nor with many expressions should return docs failing all", ), + QueryTestCase( + id="all_docs_match_at_least_one", + filter={"$nor": [{"a": 1}, {"a": 2}]}, + doc=[ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 2, "a": 1, "b": 2}, + {"_id": 3, "a": 2, "b": 1}, + {"_id": 4, "a": 2, "b": 2}, + ], + expected=[], + msg="$nor should return empty when all docs match at least one condition", + ), + QueryTestCase( + id="no_docs_match_any", + filter={"$nor": [{"a": 99}, {"b": 99}]}, + doc=[ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 2, "a": 1, "b": 2}, + {"_id": 3, "a": 2, "b": 1}, + {"_id": 4, "a": 2, "b": 2}, + ], + expected=[ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 2, "a": 1, "b": 2}, + {"_id": 3, "a": 2, "b": 1}, + {"_id": 4, "a": 2, "b": 2}, + ], + msg="$nor should return all docs when none match any condition", + ), + QueryTestCase( + id="duplicate_expressions", + filter={"$nor": [{"a": 1}, {"a": 1}]}, + doc=[ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 2, "a": 1, "b": 2}, + {"_id": 3, "a": 2, "b": 1}, + {"_id": 4, "a": 2, "b": 2}, + ], + expected=[{"_id": 3, "a": 2, "b": 1}, {"_id": 4, "a": 2, "b": 2}], + msg="$nor with duplicate expressions should behave same as single", + ), QueryTestCase( id="empty_object_in_array", filter={"$nor": [{}]}, @@ -47,7 +88,45 @@ ), ] -ALL_TESTS = VALID_ARRAY_TESTS +MULTIPLE_FIELDS_IN_EXPRESSION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="implicit_and_in_expression", + filter={"$nor": [{"a": 1, "b": 2}]}, + doc=[ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 2, "a": 1, "b": 2}, + {"_id": 3, "a": 2, "b": 1}, + {"_id": 4, "a": 2, "b": 2}, + ], + expected=[ + {"_id": 1, "a": 1, "b": 1}, + {"_id": 3, "a": 2, "b": 1}, + {"_id": 4, "a": 2, "b": 2}, + ], + msg="$nor with multiple fields in one expression is implicit AND within", + ), + QueryTestCase( + id="overlapping_field_conditions", + filter={"$nor": [{"a": {"$gt": 5}}, {"a": {"$lt": 2}}]}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 3}, {"_id": 3, "a": 7}], + expected=[{"_id": 2, "a": 3}], + msg="$nor with overlapping conditions returns docs in the gap", + ), + QueryTestCase( + id="conflicting_operators_same_field", + filter={"$nor": [{"val": {"$gt": 10}}, {"val": {"$lt": 5}}, {"val": {"$eq": 7}}]}, + doc=[ + {"_id": 1, "val": 3}, + {"_id": 2, "val": 7}, + {"_id": 3, "val": 8}, + {"_id": 4, "val": 12}, + ], + expected=[{"_id": 3, "val": 8}], + msg="$nor with conflicting operators on same field returns docs failing all", + ), +] + +ALL_TESTS = VALID_ARRAY_TESTS + MULTIPLE_FIELDS_IN_EXPRESSION_TESTS @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_core_logic.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_core_logic.py deleted file mode 100644 index e8cc556c..00000000 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_core_logic.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -Tests for $nor query operator core logic. - -Covers basic NOR semantics: documents must fail ALL conditions to be returned, -implicit equality, multiple fields in single expression, and equivalence patterns. -""" - -import pytest - -from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( - QueryTestCase, -) -from documentdb_tests.framework.assertions import assertResult -from documentdb_tests.framework.executor import execute_command -from documentdb_tests.framework.parametrize import pytest_params - -FOUR_DOCS = [ - {"_id": 1, "a": 1, "b": 1}, - {"_id": 2, "a": 1, "b": 2}, - {"_id": 3, "a": 2, "b": 1}, - {"_id": 4, "a": 2, "b": 2}, -] - -BASIC_NOR_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="fails_all_conditions", - filter={"$nor": [{"a": 1}, {"b": 1}]}, - doc=FOUR_DOCS, - expected=[{"_id": 4, "a": 2, "b": 2}], - msg="$nor should return only docs that fail ALL conditions", - ), - QueryTestCase( - id="single_condition_negation", - filter={"$nor": [{"a": 1}]}, - doc=FOUR_DOCS, - expected=[{"_id": 3, "a": 2, "b": 1}, {"_id": 4, "a": 2, "b": 2}], - msg="$nor with single condition should negate it", - ), - QueryTestCase( - id="all_docs_match_at_least_one", - filter={"$nor": [{"a": 1}, {"a": 2}]}, - doc=FOUR_DOCS, - expected=[], - msg="$nor should return empty when all docs match at least one condition", - ), - QueryTestCase( - id="no_docs_match_any", - filter={"$nor": [{"a": 99}, {"b": 99}]}, - doc=FOUR_DOCS, - expected=FOUR_DOCS, - msg="$nor should return all docs when none match any condition", - ), - QueryTestCase( - id="three_expressions", - filter={"$nor": [{"a": 1}, {"b": 1}, {"a": 2, "b": 2}]}, - doc=FOUR_DOCS, - expected=[], - msg="$nor with three expressions — docs must fail ALL THREE", - ), - QueryTestCase( - id="duplicate_expressions", - filter={"$nor": [{"a": 1}, {"a": 1}]}, - doc=FOUR_DOCS, - expected=[{"_id": 3, "a": 2, "b": 1}, {"_id": 4, "a": 2, "b": 2}], - msg="$nor with duplicate expressions should behave same as single", - ), -] - -IMPLICIT_EQUALITY_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="multiple_equalities_same_field", - filter={"$nor": [{"a": 1}, {"a": 2}]}, - doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}, {"_id": 3, "a": 3}], - expected=[{"_id": 3, "a": 3}], - msg="$nor with multiple equalities on same field excludes all matching values", - ), - QueryTestCase( - id="equalities_different_fields", - filter={"$nor": [{"a": 1}, {"b": 2}]}, - doc=[ - {"_id": 1, "a": 1, "b": 1}, - {"_id": 2, "a": 2, "b": 2}, - {"_id": 3, "a": 2, "b": 1}, - ], - expected=[{"_id": 3, "a": 2, "b": 1}], - msg="$nor with equalities on different fields returns docs failing both", - ), - QueryTestCase( - id="includes_missing_field_docs", - filter={"$nor": [{"a": 1}]}, - doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}, {"_id": 3, "b": 1}], - expected=[{"_id": 2, "a": 2}, {"_id": 3, "b": 1}], - msg="$nor includes docs where referenced field does not exist", - ), -] - -MULTIPLE_FIELDS_IN_EXPRESSION_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="implicit_and_in_expression", - filter={"$nor": [{"a": 1, "b": 2}]}, - doc=FOUR_DOCS, - expected=[ - {"_id": 1, "a": 1, "b": 1}, - {"_id": 3, "a": 2, "b": 1}, - {"_id": 4, "a": 2, "b": 2}, - ], - msg="$nor with multiple fields in one expression is implicit AND within", - ), - QueryTestCase( - id="overlapping_field_conditions", - filter={"$nor": [{"a": {"$gt": 5}}, {"a": {"$lt": 2}}]}, - doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 3}, {"_id": 3, "a": 7}], - expected=[{"_id": 2, "a": 3}], - msg="$nor with overlapping conditions returns docs in the gap", - ), - QueryTestCase( - id="conflicting_operators_same_field", - filter={"$nor": [{"val": {"$gt": 10}}, {"val": {"$lt": 5}}, {"val": {"$eq": 7}}]}, - doc=[ - {"_id": 1, "val": 3}, - {"_id": 2, "val": 7}, - {"_id": 3, "val": 8}, - {"_id": 4, "val": 12}, - ], - expected=[{"_id": 3, "val": 8}], - msg="$nor with conflicting operators on same field returns docs failing all", - ), -] - -EQUIVALENCE_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="nor_single_equivalent_to_ne", - filter={"$nor": [{"a": 1}]}, - doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}], - expected=[{"_id": 2, "a": 2}], - msg="$nor with single equality is equivalent to $ne", - ), - QueryTestCase( - id="ne_equivalent_to_nor_single", - filter={"a": {"$ne": 1}}, - doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}], - expected=[{"_id": 2, "a": 2}], - msg="$ne should produce same results as $nor with single equality", - ), -] - -ALL_TESTS = ( - BASIC_NOR_TESTS - + IMPLICIT_EQUALITY_TESTS - + MULTIPLE_FIELDS_IN_EXPRESSION_TESTS - + EQUIVALENCE_TESTS -) - - -@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) -def test_nor_core_logic(collection, test): - """Test $nor query operator core NOR logic.""" - collection.insert_many(test.doc) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertResult( - result, - expected=test.expected, - error_code=test.error_code, - ignore_doc_order=True, - msg=test.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py index 2fe128ed..ef10a114 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py @@ -24,16 +24,23 @@ QueryTestCase( id="null_field_excluded_by_null_match", filter={"$nor": [{"val": None}]}, - doc=[{"_id": 1, "val": None}, {"_id": 2, "val": 5}, {"_id": 3, "other": 1}], + doc=[{"_id": 1, "val": None}, {"_id": 2, "val": 5}], expected=[{"_id": 2, "val": 5}], - msg="$nor with null condition excludes docs with val=null AND missing field docs", + msg="$nor with null condition excludes docs with val=null", + ), + QueryTestCase( + id="missing_field_excluded_by_null_match", + filter={"$nor": [{"val": None}]}, + doc=[{"_id": 1, "val": 5}, {"_id": 2, "other": 1}], + expected=[{"_id": 1, "val": 5}], + msg="$nor with null condition excludes docs where field is missing", ), QueryTestCase( - id="type_null_excludes_null_and_missing", + id="type_null_excludes_only_null", filter={"$nor": [{"val": {"$type": "null"}}]}, doc=[{"_id": 1, "val": None}, {"_id": 2, "val": 5}, {"_id": 3, "other": 1}], expected=[{"_id": 2, "val": 5}, {"_id": 3, "other": 1}], - msg="$nor with $type null excludes docs where val is null but not missing-field docs", + msg="$nor with $type null excludes only null docs, missing-field docs are kept", ), QueryTestCase( id="null_field_with_gt_operator", From 0ded51a121b06d5a1693b5cb936041d05eb070b7 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Wed, 13 May 2026 16:26:57 -0700 Subject: [PATCH 05/11] Added tests to cover holes in coverage Signed-off-by: PatersonProjects --- .../logical/nor/test_nor_argument_handling.py | 23 ++++++++++++++++++- .../logical/nor/test_nor_array_fields.py | 10 ++++++++ .../query/logical/nor/test_nor_data_types.py | 15 ++++++++++++ .../logical/nor/test_nor_null_missing.py | 11 +++++++++ .../logical/nor/test_nor_special_values.py | 14 +++++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py index 49a7a0e2..650c52ac 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py @@ -126,7 +126,28 @@ ), ] -ALL_TESTS = VALID_ARRAY_TESTS + MULTIPLE_FIELDS_IN_EXPRESSION_TESTS +CLAUSE_BEHAVIOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="clause_ordering_invariance", + filter={"$nor": [{"b": 1}, {"a": 1}]}, + doc=[{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 2, "b": 1}, {"_id": 3, "a": 2, "b": 2}], + expected=[{"_id": 3, "a": 2, "b": 2}], + msg="$nor with clauses in different order should produce identical results", + ), + QueryTestCase( + id="combined_with_top_level_filter", + filter={"x": 1, "$nor": [{"a": 1}, {"b": 2}]}, + doc=[ + {"_id": 1, "x": 1, "a": 1, "b": 1}, + {"_id": 2, "x": 1, "a": 2, "b": 1}, + {"_id": 3, "x": 2, "a": 2, "b": 1}, + ], + expected=[{"_id": 2, "x": 1, "a": 2, "b": 1}], + msg="$nor combined with top-level field filter applies implicit AND", + ), +] + +ALL_TESTS = VALID_ARRAY_TESTS + MULTIPLE_FIELDS_IN_EXPRESSION_TESTS + CLAUSE_BEHAVIOR_TESTS @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py index 64a7f62a..b73c7374 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py @@ -83,6 +83,16 @@ expected=[{"_id": 2, "tags": ["a", "c"]}], msg="$nor with $all should exclude docs where array contains all specified elements", ), + QueryTestCase( + id="dot_notation_array_of_arrays", + filter={"$nor": [{"a.0.0": 1}]}, + doc=[ + {"_id": 1, "a": [[1, 2], [3, 4]]}, + {"_id": 2, "a": [[5, 6], [7, 8]]}, + ], + expected=[{"_id": 2, "a": [[5, 6], [7, 8]]}], + msg="$nor with dot notation into array of arrays should exclude matching docs", + ), ] ALL_TESTS = ARRAY_FIELD_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py index 51e33976..0225e5b7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py @@ -229,12 +229,27 @@ ), ] +MIXED_TYPE_CLAUSE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="mixed_types_in_clauses", + filter={"$nor": [{"val": 1}, {"val": "hello"}]}, + doc=[ + {"_id": 1, "val": 1}, + {"_id": 2, "val": "hello"}, + {"_id": 3, "val": True}, + ], + expected=[{"_id": 3, "val": True}], + msg="$nor with mixed types in clauses should exclude each matching type", + ), +] + ALL_TESTS = ( BSON_TYPE_TESTS + BSON_TYPE_CODE_TESTS + BSON_TYPE_BINARY_REGEX_TESTS + NUMERIC_EQUIVALENCE_TESTS + BSON_TYPE_DISTINCTION_TESTS + + MIXED_TYPE_CLAUSE_TESTS ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py index ef10a114..db142380 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py @@ -49,6 +49,17 @@ expected=[{"_id": 1, "val": None}, {"_id": 3, "val": 3}], msg="$nor with $gt — docs with val=null are returned (null doesn't satisfy $gt)", ), + QueryTestCase( + id="dot_notation_into_null_intermediate", + filter={"$nor": [{"a.b": 1}]}, + doc=[ + {"_id": 1, "a": None}, + {"_id": 2, "a": 5}, + {"_id": 3, "a": {"b": 1}}, + ], + expected=[{"_id": 1, "a": None}, {"_id": 2, "a": 5}], + msg="$nor with dot notation where intermediate is null or scalar returns those docs", + ), ] EXISTS_INTERACTION_TESTS: list[QueryTestCase] = [ diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py index bb46a18a..4ba183a1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py @@ -97,6 +97,20 @@ expected=[{"_id": 2, "a": {"b": {"c": {"d": 2}}}}], msg="$nor with deeply nested field path should work correctly", ), + QueryTestCase( + id="missing_dot_path", + filter={"$nor": [{"x.y.z": 1}]}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}], + expected=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}], + msg="$nor with non-existent dot path should return all docs (none match)", + ), + QueryTestCase( + id="empty_string_field_name", + filter={"$nor": [{"": 1}]}, + doc=[{"_id": 1, "": 1, "a": 2}, {"_id": 2, "": 2, "a": 1}], + expected=[{"_id": 2, "": 2, "a": 1}], + msg="$nor with empty string field name should match docs where '' field equals value", + ), QueryTestCase( id="large_number_of_expressions", filter={"$nor": [{"a": i} for i in range(50)]}, From d7c0a86be9d20b738119439aae1466aea2f81e51 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Wed, 13 May 2026 16:39:53 -0700 Subject: [PATCH 06/11] Used pytest.approx to fix nan comparison Signed-off-by: PatersonProjects --- .../operator/query/logical/nor/test_nor_special_values.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py index 4ba183a1..ca9dfe03 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py @@ -53,7 +53,10 @@ {"_id": 3, "val": 10}, {"_id": 4, "val": 20}, ], - expected=[{"_id": 1, "val": float("nan")}, {"_id": 4, "val": 20}], + expected=[ + {"_id": 1, "val": pytest.approx(float("nan"), nan_ok=True)}, + {"_id": 4, "val": 20}, + ], msg="$nor filtering numeric values should include NaN doc (NaN != any number)", ), QueryTestCase( From 39cf8d3e63640bdae3c46248b27a09406c0fc79b Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 14 May 2026 10:37:59 -0700 Subject: [PATCH 07/11] Changed to use contants Signed-off-by: PatersonProjects --- .../logical/nor/test_nor_special_values.py | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py index ca9dfe03..6ded047e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py @@ -14,26 +14,33 @@ from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_command from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) SPECIAL_VALUE_TESTS: list[QueryTestCase] = [ QueryTestCase( id="nan_excluded_by_nan_match", - filter={"$nor": [{"val": float("nan")}]}, - doc=[{"_id": 1, "val": float("nan")}, {"_id": 2, "val": 5}], + filter={"$nor": [{"val": FLOAT_NAN}]}, + doc=[{"_id": 1, "val": FLOAT_NAN}, {"_id": 2, "val": 5}], expected=[{"_id": 2, "val": 5}], msg="$nor with NaN should exclude docs with NaN value", ), QueryTestCase( id="infinity_excluded_by_gt", filter={"$nor": [{"val": {"$gt": 1000000}}]}, - doc=[{"_id": 1, "val": float("inf")}, {"_id": 2, "val": 100}], + doc=[{"_id": 1, "val": FLOAT_INFINITY}, {"_id": 2, "val": 100}], expected=[{"_id": 2, "val": 100}], msg="$nor with $gt should exclude Infinity (Infinity > any number)", ), QueryTestCase( id="negative_infinity_excluded_by_lt", filter={"$nor": [{"val": {"$lt": -1000000}}]}, - doc=[{"_id": 1, "val": float("-inf")}, {"_id": 2, "val": -100}], + doc=[{"_id": 1, "val": FLOAT_NEGATIVE_INFINITY}, {"_id": 2, "val": -100}], expected=[{"_id": 2, "val": -100}], msg="$nor with $lt should exclude -Infinity (-Infinity < any number)", ), @@ -48,28 +55,28 @@ id="nan_included_when_filtering_numbers", filter={"$nor": [{"val": 5}, {"val": 10}]}, doc=[ - {"_id": 1, "val": float("nan")}, + {"_id": 1, "val": FLOAT_NAN}, {"_id": 2, "val": 5}, {"_id": 3, "val": 10}, {"_id": 4, "val": 20}, ], expected=[ - {"_id": 1, "val": pytest.approx(float("nan"), nan_ok=True)}, + {"_id": 1, "val": pytest.approx(FLOAT_NAN, nan_ok=True)}, {"_id": 4, "val": 20}, ], msg="$nor filtering numeric values should include NaN doc (NaN != any number)", ), QueryTestCase( id="decimal128_nan", - filter={"$nor": [{"val": Decimal128("NaN")}]}, - doc=[{"_id": 1, "val": Decimal128("NaN")}, {"_id": 2, "val": Decimal128("5")}], + filter={"$nor": [{"val": DECIMAL128_NAN}]}, + doc=[{"_id": 1, "val": DECIMAL128_NAN}, {"_id": 2, "val": Decimal128("5")}], expected=[{"_id": 2, "val": Decimal128("5")}], msg="$nor with Decimal128 NaN should exclude matching docs", ), QueryTestCase( id="decimal128_infinity", - filter={"$nor": [{"val": Decimal128("Infinity")}]}, - doc=[{"_id": 1, "val": Decimal128("Infinity")}, {"_id": 2, "val": Decimal128("5")}], + filter={"$nor": [{"val": DECIMAL128_INFINITY}]}, + doc=[{"_id": 1, "val": DECIMAL128_INFINITY}, {"_id": 2, "val": Decimal128("5")}], expected=[{"_id": 2, "val": Decimal128("5")}], msg="$nor with Decimal128 Infinity should exclude matching docs", ), From d6adb2c2982beabc31628cc4563fb27a78d4707b Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 14 May 2026 11:29:32 -0700 Subject: [PATCH 08/11] Docstring edits, removed duplicates, added PR requested tests Signed-off-by: PatersonProjects --- .../logical/nor/test_nor_argument_handling.py | 14 +++++++++- .../logical/nor/test_nor_array_fields.py | 26 ++++++++++++++++++- .../query/logical/nor/test_nor_data_types.py | 17 ++---------- .../logical/nor/test_nor_special_values.py | 3 ++- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py index 650c52ac..0bff64bb 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py @@ -2,7 +2,8 @@ Tests for $nor query operator argument handling. Covers valid array argument variations: single expression, multiple expressions, -many expressions, empty object in array, and multiple fields in a single expression. +many expressions, empty object in array, multiple fields in a single expression, +and clause behavior (ordering invariance, nested double negation, combined filters). """ import pytest @@ -134,6 +135,17 @@ expected=[{"_id": 3, "a": 2, "b": 2}], msg="$nor with clauses in different order should produce identical results", ), + QueryTestCase( + id="nested_nor_double_negation", + filter={"$nor": [{"$nor": [{"a": 1}, {"b": 1}]}]}, + doc=[ + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 2, "b": 1}, + {"_id": 3, "a": 2, "b": 2}, + ], + expected=[{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 2, "b": 1}], + msg="$nor inside $nor (double negation) should be equivalent to $or", + ), QueryTestCase( id="combined_with_top_level_filter", filter={"x": 1, "$nor": [{"a": 1}, {"b": 2}]}, diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py index b73c7374..70e1dd5a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py @@ -2,7 +2,8 @@ Tests for $nor query operator with array fields. Covers element matching in arrays, nested array paths, empty arrays, -arrays of objects with dot notation, and $elemMatch on arrays of objects. +arrays of objects with dot notation, $elemMatch and $all on arrays, +null elements in arrays, and multi-element clause matching. """ import pytest @@ -83,6 +84,29 @@ expected=[{"_id": 2, "tags": ["a", "c"]}], msg="$nor with $all should exclude docs where array contains all specified elements", ), + QueryTestCase( + id="array_containing_null", + filter={"$nor": [{"val": None}]}, + doc=[ + {"_id": 1, "val": [1, None, 3]}, + {"_id": 2, "val": [1, 2, 3]}, + {"_id": 3, "val": [None]}, + ], + expected=[{"_id": 2, "val": [1, 2, 3]}], + msg="$nor with null should exclude docs where array contains null element", + ), + QueryTestCase( + id="multiple_elements_match_different_clauses", + filter={"$nor": [{"val": 1}, {"val": 5}]}, + doc=[ + {"_id": 1, "val": [1, 5, 10]}, + {"_id": 2, "val": [2, 3]}, + {"_id": 3, "val": [5, 6]}, + {"_id": 4, "val": [10, 20]}, + ], + expected=[{"_id": 2, "val": [2, 3]}, {"_id": 4, "val": [10, 20]}], + msg="$nor should exclude doc when different array elements match different clauses", + ), QueryTestCase( id="dot_notation_array_of_arrays", filter={"$nor": [{"a.0.0": 1}]}, diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py index 0225e5b7..4cfdc5ba 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py @@ -2,7 +2,8 @@ Tests for $nor query operator data type coverage. Covers BSON type matching, numeric equivalence across types, -and BSON type distinction (false vs 0, true vs 1, null vs missing). +BSON type distinction (false vs 0, true vs 1, null vs missing), +and mixed types across clauses. """ from datetime import datetime, timezone @@ -80,13 +81,6 @@ expected=[{"_id": 2, "val": datetime(2024, 6, 1, tzinfo=timezone.utc)}], msg="$nor should exclude docs matching date value", ), - QueryTestCase( - id="null", - filter={"$nor": [{"val": None}]}, - doc=[{"_id": 1, "val": None}, {"_id": 2, "val": 5}], - expected=[{"_id": 2, "val": 5}], - msg="$nor should exclude docs matching null value", - ), QueryTestCase( id="int32", filter={"$nor": [{"val": 42}]}, @@ -213,13 +207,6 @@ expected=[{"_id": 2, "val": 1}], msg="$nor with true should NOT exclude docs with val=1 (type distinction)", ), - QueryTestCase( - id="null_matches_missing", - filter={"$nor": [{"val": None}]}, - doc=[{"_id": 1, "val": None}, {"_id": 2, "other": 1}, {"_id": 3, "val": 5}], - expected=[{"_id": 3, "val": 5}], - msg="$nor with null excludes both null and missing field docs", - ), QueryTestCase( id="empty_string_not_equal_to_null", filter={"$nor": [{"val": ""}]}, diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py index 6ded047e..4daaaec6 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py @@ -2,7 +2,8 @@ Tests for $nor query operator with special values and edge cases. Covers NaN, Infinity, -Infinity, negative zero, Decimal128 special values, -empty collections, non-existent fields, and deeply nested field paths. +empty collections, non-existent fields, deeply nested field paths, +and edge-case field names. """ import pytest From 8e90cf8a60282143dcc346891d2ddffff3c6c7e8 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 14 May 2026 11:36:00 -0700 Subject: [PATCH 09/11] Added mixed scalar and array test Signed-off-by: PatersonProjects --- .../query/logical/nor/test_nor_array_fields.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py index 70e1dd5a..07a1700f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py @@ -3,7 +3,8 @@ Covers element matching in arrays, nested array paths, empty arrays, arrays of objects with dot notation, $elemMatch and $all on arrays, -null elements in arrays, and multi-element clause matching. +null elements in arrays, mixed scalar and array values, and multi-element +clause matching. """ import pytest @@ -107,6 +108,18 @@ expected=[{"_id": 2, "val": [2, 3]}, {"_id": 4, "val": [10, 20]}], msg="$nor should exclude doc when different array elements match different clauses", ), + QueryTestCase( + id="mixed_scalar_and_array_same_field", + filter={"$nor": [{"val": 2}]}, + doc=[ + {"_id": 1, "val": 2}, + {"_id": 2, "val": [1, 2, 3]}, + {"_id": 3, "val": 5}, + {"_id": 4, "val": [4, 5, 6]}, + ], + expected=[{"_id": 3, "val": 5}, {"_id": 4, "val": [4, 5, 6]}], + msg="$nor should exclude docs whether matching value is a scalar or array element", + ), QueryTestCase( id="dot_notation_array_of_arrays", filter={"$nor": [{"a.0.0": 1}]}, From 50651fb3900f963330bbdfa49f622f42a2c4ae65 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 15 May 2026 09:19:16 -0700 Subject: [PATCH 10/11] Added nested elemMatch test case Signed-off-by: PatersonProjects --- .../logical/nor/test_nor_array_fields.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py index 07a1700f..712e5825 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py @@ -130,6 +130,27 @@ expected=[{"_id": 2, "a": [[5, 6], [7, 8]]}], msg="$nor with dot notation into array of arrays should exclude matching docs", ), + QueryTestCase( + id="elemMatch_with_nested_nor", + filter={ + "$nor": [ + {"items": {"$elemMatch": {"$nor": [{"qty": {"$gt": 5}}, {"price": {"$lt": 3}}]}}} + ] + }, + doc=[ + {"_id": 1, "items": [{"qty": 2, "price": 5}]}, + {"_id": 2, "items": [{"qty": 10, "price": 5}]}, + {"_id": 3, "items": [{"qty": 2, "price": 1}]}, + {"_id": 4, "items": [{"qty": 10, "price": 1}]}, + ], + expected=[ + {"_id": 2, "items": [{"qty": 10, "price": 5}]}, + {"_id": 3, "items": [{"qty": 2, "price": 1}]}, + {"_id": 4, "items": [{"qty": 10, "price": 1}]}, + ], + msg="$nor with $elemMatch containing nested $nor should exclude docs where an element " + "satisfies neither condition (qty<=5 AND price>=3)", + ), ] ALL_TESTS = ARRAY_FIELD_TESTS From 587f691949225b908a4c49d8338f6499aca3df5b Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 19 May 2026 08:56:26 -0700 Subject: [PATCH 11/11] Removed out of scope tests Signed-off-by: PatersonProjects --- .../operator/query/logical/nor/test_nor_errors.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py index 8b516ce9..d0ea4925 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py @@ -104,20 +104,6 @@ error_code=BAD_VALUE_ERROR, msg="$nor at non-top-level position should return BadValue error", ), - QueryTestCase( - id="invalid_operator_inside_expression", - filter={"$nor": [{"val": {"$invalid": 1}}]}, - doc=DOCS, - error_code=BAD_VALUE_ERROR, - msg="$nor with invalid operator inside expression should return BadValue error", - ), - QueryTestCase( - id="mixed_valid_invalid_expressions", - filter={"$nor": [{"a": 1}, {"val": {"$invalid": 1}}]}, - doc=DOCS, - error_code=BAD_VALUE_ERROR, - msg="$nor with mixed valid/invalid expressions should return BadValue error", - ), ] ALL_TESTS = (