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..0bff64bb --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_argument_handling.py @@ -0,0 +1,176 @@ +""" +Tests for $nor query operator argument handling. + +Covers valid array argument variations: single expression, multiple expressions, +many expressions, empty object in array, multiple fields in a single expression, +and clause behavior (ordering invariance, nested double negation, combined filters). +""" + +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}] + +VALID_ARRAY_TESTS: list[QueryTestCase] = [ + 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="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": [{}]}, + doc=DOCS, + expected=[], + msg="$nor with empty object matches all docs so returns empty", + ), +] + +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", + ), +] + +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="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}]}, + 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)) +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..712e5825 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_array_fields.py @@ -0,0 +1,170 @@ +""" +Tests for $nor query operator with array fields. + +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, mixed scalar and array values, and multi-element +clause matching. +""" + +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", + ), + 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="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}]}, + 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", + ), + 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 + + +@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_data_types.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py new file mode 100644 index 00000000..4cfdc5ba --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_data_types.py @@ -0,0 +1,254 @@ +""" +Tests for $nor query operator data type coverage. + +Covers BSON type matching, numeric equivalence across types, +BSON type distinction (false vs 0, true vs 1, null vs missing), +and mixed types across clauses. +""" + +from datetime import datetime, timezone + +import pytest +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, +) +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="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_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", + 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="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", + ), +] + +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 +) + + +@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_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py new file mode 100644 index 00000000..d0ea4925 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_errors.py @@ -0,0 +1,124 @@ +""" +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", + ), +] + +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 new file mode 100644 index 00000000..db142380 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_null_missing.py @@ -0,0 +1,126 @@ +""" +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}], + expected=[{"_id": 2, "val": 5}], + 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_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 only null docs, missing-field docs are kept", + ), + 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)", + ), + 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] = [ + 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..4daaaec6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/nor/test_nor_special_values.py @@ -0,0 +1,149 @@ +""" +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, deeply nested field paths, +and edge-case field names. +""" + +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 +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}], + 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_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_NEGATIVE_INFINITY}, {"_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="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": 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")}], + 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="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)]}, + 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, + )