diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/or/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_argument_handling.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_argument_handling.py new file mode 100644 index 00000000..d877ed5d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_argument_handling.py @@ -0,0 +1,97 @@ +""" +Tests for $or argument handling. + +Tests argument count variations, valid argument patterns, and deduplication +behavior when multiple clauses match the same document. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +DOCS = [ + {"_id": 1, "a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, + {"_id": 2, "a": 2, "b": 2, "c": 3, "d": 4, "e": 5}, + {"_id": 3, "a": 1, "b": 3, "c": 3, "d": 4, "e": 5}, +] + +SUCCESS_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="single_expression", + filter={"$or": [{"a": 1}]}, + doc=DOCS, + expected=[ + {"_id": 1, "a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, + {"_id": 3, "a": 1, "b": 3, "c": 3, "d": 4, "e": 5}, + ], + msg="$or with single expression matches documents satisfying it", + ), + QueryTestCase( + id="two_expressions", + filter={"$or": [{"a": 1}, {"a": 2}]}, + doc=DOCS, + expected=DOCS, + msg="$or with two expressions matches documents satisfying either", + ), + QueryTestCase( + id="five_expressions", + filter={"$or": [{"a": 1}, {"b": 2}, {"c": 3}, {"d": 4}, {"e": 5}]}, + doc=DOCS, + expected=DOCS, + msg="$or with five expressions matches documents satisfying any", + ), + QueryTestCase( + id="empty_object_expression", + filter={"$or": [{}]}, + doc=DOCS, + expected=DOCS, + msg="$or with empty object matches all documents", + ), + QueryTestCase( + id="duplicate_same_field_same_value", + filter={"$or": [{"a": 1}, {"a": 1}, {"a": 1}]}, + doc=DOCS, + expected=[ + {"_id": 1, "a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, + {"_id": 3, "a": 1, "b": 3, "c": 3, "d": 4, "e": 5}, + ], + msg="$or with repeated identical clauses does not duplicate results", + ), + QueryTestCase( + id="large_array_100_expressions", + filter={"$or": [{"a": 1}] * 100}, + doc=DOCS, + expected=[ + {"_id": 1, "a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, + {"_id": 3, "a": 1, "b": 3, "c": 3, "d": 4, "e": 5}, + ], + msg="$or with 100 expressions does not hit a limit", + ), + QueryTestCase( + id="no_duplicate_results_overlapping_clauses", + filter={"$or": [{"a": {"$gt": 0}}, {"a": {"$lt": 10}}]}, + doc=[{"_id": 1, "a": 5}], + expected=[{"_id": 1, "a": 5}], + msg="$or does not return duplicates when multiple clauses match same doc", + ), + QueryTestCase( + id="no_duplicate_results_all_match_all", + filter={"$or": [{"a": {"$gte": 1}}, {"b": {"$gte": 2}}]}, + doc=DOCS, + expected=DOCS, + msg="$or where all clauses match all documents returns all without duplicates", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SUCCESS_TESTS)) +def test_or_argument_success(collection, test): + """Test $or with valid argument variations.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, msg=test.msg, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_data_types.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_data_types.py new file mode 100644 index 00000000..712a87f6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_data_types.py @@ -0,0 +1,324 @@ +""" +Tests for $or data type coverage and BSON type distinction. + +Tests that $or correctly matches documents with various BSON types +(including Regex, MinKey, MaxKey, Code), respects type distinctions +(e.g., bool vs int, null vs missing, 0.0 vs -0.0), and handles +special values (Infinity, NaN). +""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, 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 assertSuccess +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, +) + +TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int32", + filter={"$or": [{"val": 42}]}, + doc=[{"_id": 1, "val": 42}, {"_id": 2, "val": 99}], + expected=[{"_id": 1, "val": 42}], + msg="$or matches int32 value", + ), + QueryTestCase( + id="int64", + filter={"$or": [{"val": Int64(123456789012345)}]}, + doc=[{"_id": 1, "val": Int64(123456789012345)}, {"_id": 2, "val": 0}], + expected=[{"_id": 1, "val": Int64(123456789012345)}], + msg="$or matches int64 value", + ), + QueryTestCase( + id="double", + filter={"$or": [{"val": 3.14}]}, + doc=[{"_id": 1, "val": 3.14}, {"_id": 2, "val": 2.71}], + expected=[{"_id": 1, "val": 3.14}], + msg="$or matches double value", + ), + QueryTestCase( + id="decimal128", + filter={"$or": [{"val": Decimal128("1.23")}]}, + doc=[{"_id": 1, "val": Decimal128("1.23")}, {"_id": 2, "val": Decimal128("4.56")}], + expected=[{"_id": 1, "val": Decimal128("1.23")}], + msg="$or matches decimal128 value", + ), + QueryTestCase( + id="string", + filter={"$or": [{"val": "hello"}]}, + doc=[{"_id": 1, "val": "hello"}, {"_id": 2, "val": "world"}], + expected=[{"_id": 1, "val": "hello"}], + msg="$or matches string value", + ), + QueryTestCase( + id="bool_true", + filter={"$or": [{"val": True}]}, + doc=[{"_id": 1, "val": True}, {"_id": 2, "val": False}], + expected=[{"_id": 1, "val": True}], + msg="$or matches boolean true", + ), + QueryTestCase( + id="bool_false", + filter={"$or": [{"val": False}]}, + doc=[{"_id": 1, "val": True}, {"_id": 2, "val": False}], + expected=[{"_id": 2, "val": False}], + msg="$or matches boolean false", + ), + QueryTestCase( + id="date", + filter={"$or": [{"val": datetime(2024, 1, 1, tzinfo=timezone.utc)}]}, + doc=[ + {"_id": 1, "val": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + {"_id": 2, "val": datetime(2025, 1, 1, tzinfo=timezone.utc)}, + ], + expected=[{"_id": 1, "val": datetime(2024, 1, 1, tzinfo=timezone.utc)}], + msg="$or matches date value", + ), + QueryTestCase( + id="null", + filter={"$or": [{"val": None}]}, + doc=[{"_id": 1, "val": None}, {"_id": 2, "val": 1}], + expected=[{"_id": 1, "val": None}], + msg="$or matches null value", + ), + QueryTestCase( + id="object", + filter={"$or": [{"val": {"x": 1}}]}, + doc=[{"_id": 1, "val": {"x": 1}}, {"_id": 2, "val": {"x": 2}}], + expected=[{"_id": 1, "val": {"x": 1}}], + msg="$or matches embedded object", + ), + QueryTestCase( + id="array", + filter={"$or": [{"val": [1, 2, 3]}]}, + doc=[{"_id": 1, "val": [1, 2, 3]}, {"_id": 2, "val": [4, 5]}], + expected=[{"_id": 1, "val": [1, 2, 3]}], + msg="$or matches array value", + ), + QueryTestCase( + id="objectid", + filter={"$or": [{"val": ObjectId("507f1f77bcf86cd799439011")}]}, + doc=[ + {"_id": 1, "val": ObjectId("507f1f77bcf86cd799439011")}, + {"_id": 2, "val": ObjectId("507f1f77bcf86cd799439012")}, + ], + expected=[{"_id": 1, "val": ObjectId("507f1f77bcf86cd799439011")}], + msg="$or matches ObjectId value", + ), + QueryTestCase( + id="timestamp", + filter={"$or": [{"val": Timestamp(1, 1)}]}, + doc=[{"_id": 1, "val": Timestamp(1, 1)}, {"_id": 2, "val": Timestamp(2, 1)}], + expected=[{"_id": 1, "val": Timestamp(1, 1)}], + msg="$or matches Timestamp value", + ), + QueryTestCase( + id="binary", + filter={"$or": [{"val": Binary(b"\x01\x02")}]}, + doc=[{"_id": 1, "val": Binary(b"\x01\x02")}, {"_id": 2, "val": Binary(b"\x03")}], + expected=[{"_id": 1, "val": b"\x01\x02"}], + msg="$or matches Binary value", + ), + QueryTestCase( + id="regex", + filter={"$or": [{"val": Regex("^hello")}]}, + doc=[{"_id": 1, "val": "hello world"}, {"_id": 2, "val": "world"}], + expected=[{"_id": 1, "val": "hello world"}], + msg="$or matches Regex value", + ), + QueryTestCase( + id="minkey", + filter={"$or": [{"val": MinKey()}]}, + doc=[{"_id": 1, "val": MinKey()}, {"_id": 2, "val": 1}], + expected=[{"_id": 1, "val": MinKey()}], + msg="$or matches MinKey value", + ), + QueryTestCase( + id="maxkey", + filter={"$or": [{"val": MaxKey()}]}, + doc=[{"_id": 1, "val": MaxKey()}, {"_id": 2, "val": 1}], + expected=[{"_id": 1, "val": MaxKey()}], + msg="$or matches MaxKey value", + ), + QueryTestCase( + id="javascript_code", + filter={"$or": [{"val": Code("function() { return true; }")}]}, + doc=[ + {"_id": 1, "val": Code("function() { return true; }")}, + {"_id": 2, "val": Code("function() { return false; }")}, + ], + expected=[{"_id": 1, "val": Code("function() { return true; }")}], + msg="$or matches JavaScript Code value", + ), +] + + +BSON_DISTINCTION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="false_not_zero", + filter={"$or": [{"val": False}]}, + doc=[{"_id": 1, "val": 0}], + expected=[], + msg="bool false does not match int 0", + ), + QueryTestCase( + id="zero_not_false", + filter={"$or": [{"val": 0}]}, + doc=[{"_id": 1, "val": False}], + expected=[], + msg="int 0 does not match bool false", + ), + QueryTestCase( + id="true_not_one", + filter={"$or": [{"val": True}]}, + doc=[{"_id": 1, "val": 1}], + expected=[], + msg="bool true does not match int 1", + ), + QueryTestCase( + id="empty_string_not_null", + filter={"$or": [{"val": ""}]}, + doc=[{"_id": 1, "val": None}], + expected=[], + msg="empty string does not match null", + ), + QueryTestCase( + id="numeric_equivalence_int_long", + filter={"$or": [{"val": 1}]}, + doc=[{"_id": 1, "val": Int64(1)}], + expected=[{"_id": 1, "val": Int64(1)}], + msg="int 1 matches long 1 (numeric equivalence)", + ), + QueryTestCase( + id="numeric_equivalence_int_double", + filter={"$or": [{"val": 1.0}]}, + doc=[{"_id": 1, "val": 1}], + expected=[{"_id": 1, "val": 1}], + msg="double 1.0 matches int 1 (numeric equivalence)", + ), + QueryTestCase( + id="numeric_equivalence_int_decimal128", + filter={"$or": [{"val": Decimal128("1")}]}, + doc=[{"_id": 1, "val": 1}], + expected=[{"_id": 1, "val": 1}], + msg="Decimal128('1') matches int 1 (numeric equivalence)", + ), + QueryTestCase( + id="null_matches_missing", + filter={"$or": [{"a": None}]}, + doc=[{"_id": 1}, {"_id": 2, "a": 1}], + expected=[{"_id": 1}], + msg="$or with null clause matches missing field", + ), + QueryTestCase( + id="null_no_match_non_null", + filter={"$or": [{"val": None}]}, + doc=[{"_id": 1, "val": 1}], + expected=[], + msg="null query does not match non-null field", + ), + QueryTestCase( + id="positive_zero_matches_negative_zero", + filter={"$or": [{"val": 0.0}]}, + doc=[{"_id": 1, "val": -0.0}], + expected=[{"_id": 1, "val": -0.0}], + msg="0.0 matches -0.0 (numeric equivalence)", + ), + QueryTestCase( + id="negative_zero_matches_positive_zero", + filter={"$or": [{"val": -0.0}]}, + doc=[{"_id": 1, "val": 0.0}], + expected=[{"_id": 1, "val": 0.0}], + msg="-0.0 matches 0.0 (numeric equivalence)", + ), + QueryTestCase( + id="negative_zero_matches_int_zero", + filter={"$or": [{"val": -0.0}]}, + doc=[{"_id": 1, "val": 0}], + expected=[{"_id": 1, "val": 0}], + msg="-0.0 matches int 0 (numeric equivalence)", + ), +] + + +SPECIAL_VALUE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="float_infinity", + filter={"$or": [{"val": FLOAT_INFINITY}]}, + doc=[{"_id": 1, "val": FLOAT_INFINITY}, {"_id": 2, "val": 1.0}], + expected=[{"_id": 1, "val": FLOAT_INFINITY}], + msg="$or matches float Infinity", + ), + QueryTestCase( + id="float_neg_infinity", + filter={"$or": [{"val": FLOAT_NEGATIVE_INFINITY}]}, + doc=[{"_id": 1, "val": FLOAT_NEGATIVE_INFINITY}, {"_id": 2, "val": 1.0}], + expected=[{"_id": 1, "val": FLOAT_NEGATIVE_INFINITY}], + msg="$or matches float -Infinity", + ), + QueryTestCase( + id="decimal128_infinity", + filter={"$or": [{"val": DECIMAL128_INFINITY}]}, + doc=[ + {"_id": 1, "val": DECIMAL128_INFINITY}, + {"_id": 2, "val": Decimal128("1")}, + ], + expected=[{"_id": 1, "val": DECIMAL128_INFINITY}], + msg="$or matches Decimal128 Infinity", + ), + QueryTestCase( + id="cross_type_infinity", + filter={"$or": [{"val": FLOAT_INFINITY}]}, + doc=[{"_id": 1, "val": DECIMAL128_INFINITY}, {"_id": 2, "val": 1.0}], + expected=[{"_id": 1, "val": DECIMAL128_INFINITY}], + msg="Float Infinity matches Decimal128 Infinity (numeric equivalence)", + ), +] + + +NAN_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="float_nan", + filter={"$or": [{"val": FLOAT_NAN}]}, + doc=[{"_id": 1, "val": FLOAT_NAN}, {"_id": 2, "val": 1.0}], + expected=[{"_id": 1, "val": pytest.approx(FLOAT_NAN, nan_ok=True)}], + msg="$or matches float NaN", + ), + QueryTestCase( + id="decimal128_nan", + filter={"$or": [{"val": DECIMAL128_NAN}]}, + doc=[{"_id": 1, "val": DECIMAL128_NAN}, {"_id": 2, "val": Decimal128("1")}], + expected=[{"_id": 1, "val": DECIMAL128_NAN}], + msg="$or matches Decimal128 NaN", + ), + QueryTestCase( + id="cross_type_nan", + filter={"$or": [{"val": FLOAT_NAN}]}, + doc=[{"_id": 1, "val": DECIMAL128_NAN}, {"_id": 2, "val": 1.0}], + expected=[{"_id": 1, "val": DECIMAL128_NAN}], + msg="Float NaN matches Decimal128 NaN", + ), +] + + +ALL_TESTS = TYPE_TESTS + BSON_DISTINCTION_TESTS + SPECIAL_VALUE_TESTS + NAN_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_or_data_types(collection, test): + """Test $or data type coverage, BSON type distinctions, and special values.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_edge_cases.py new file mode 100644 index 00000000..1f52d31e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_edge_cases.py @@ -0,0 +1,100 @@ +""" +Tests for $or edge cases. + +Tests empty collection, no-match, single-document, null/missing field handling, +$exists behavior, and nested $or nesting. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +ALL_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="no_clause_matches", + filter={"$or": [{"a": 99}, {"b": 99}]}, + doc=[{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 2, "b": 3}], + expected=[], + msg="$or where no clause matches returns empty", + ), + QueryTestCase( + id="empty_collection", + filter={"$or": [{"a": 1}, {"b": 2}]}, + doc=[], + expected=[], + msg="$or on empty collection returns empty", + ), + QueryTestCase( + id="single_document_match", + filter={"$or": [{"a": 1}, {"b": 99}]}, + doc=[{"_id": 1, "a": 1, "b": 2}], + expected=[{"_id": 1, "a": 1, "b": 2}], + msg="$or on single-document collection matches correctly", + ), + QueryTestCase( + id="single_document_no_match", + filter={"$or": [{"a": 99}, {"b": 99}]}, + doc=[{"_id": 1, "a": 1, "b": 2}], + expected=[], + msg="$or on single-document collection returns empty when not matched", + ), + QueryTestCase( + id="null_field_matches", + filter={"$or": [{"a": None}, {"b": 1}]}, + doc=[{"_id": 1, "a": None}, {"_id": 2, "b": 1}, {"_id": 3, "a": 1}], + expected=[{"_id": 1, "a": None}, {"_id": 2, "b": 1}], + msg="$or with null clause matches null field", + ), + QueryTestCase( + id="exists_true_matches", + filter={"$or": [{"a": {"$exists": True}}, {"b": 1}]}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "b": 1}, {"_id": 3, "c": 1}], + expected=[{"_id": 1, "a": 1}, {"_id": 2, "b": 1}], + msg="$or with $exists:true matches docs with field present", + ), + QueryTestCase( + id="exists_true_no_match", + filter={"$or": [{"a": {"$exists": True}}, {"b": 1}]}, + doc=[{"_id": 1, "c": 1}], + expected=[], + msg="$or with $exists:true does not match when no clause satisfied", + ), + QueryTestCase( + id="nested_or", + filter={"$or": [{"$or": [{"a": 1}, {"b": 2}]}, {"c": 3}]}, + doc=[ + {"_id": 1, "a": 1}, + {"_id": 2, "b": 2}, + {"_id": 3, "c": 3}, + {"_id": 4, "d": 4}, + ], + expected=[{"_id": 1, "a": 1}, {"_id": 2, "b": 2}, {"_id": 3, "c": 3}], + msg="Nested $or matches documents satisfying any inner or outer clause", + ), + QueryTestCase( + id="three_level_nesting", + filter={"$or": [{"$or": [{"$or": [{"a": 1}]}, {"b": 2}]}, {"c": 3}]}, + doc=[ + {"_id": 1, "a": 1}, + {"_id": 2, "b": 2}, + {"_id": 3, "c": 3}, + {"_id": 4, "d": 4}, + ], + expected=[{"_id": 1, "a": 1}, {"_id": 2, "b": 2}, {"_id": 3, "c": 3}], + msg="Three-level nested $or matches correctly", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_or_edge_cases(collection, test): + """Test $or edge cases.""" + if test.doc: + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, msg=test.msg, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_errors.py new file mode 100644 index 00000000..6225fe0a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_errors.py @@ -0,0 +1,153 @@ +""" +Tests for $or error cases and invalid argument handling. + +Tests that $or returns correct error codes for malformed expressions, +invalid argument types, and invalid element types at various positions. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode +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 + +ERROR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="empty_array", + filter={"$or": []}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with empty array errors", + ), + QueryTestCase( + id="not_array_object", + filter={"$or": {"a": 1}}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with object instead of array errors", + ), + QueryTestCase( + id="not_array_int", + filter={"$or": 1}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with integer errors", + ), + QueryTestCase( + id="not_array_string", + filter={"$or": "string"}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with string errors", + ), + QueryTestCase( + id="not_array_null", + filter={"$or": None}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with null errors", + ), + QueryTestCase( + id="element_int", + filter={"$or": [1]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with integer element errors", + ), + QueryTestCase( + id="element_string", + filter={"$or": ["string"]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with string element errors", + ), + QueryTestCase( + id="element_null", + filter={"$or": [None]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with null element errors", + ), + QueryTestCase( + id="element_bool", + filter={"$or": [True]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with boolean element errors", + ), + QueryTestCase( + id="non_object_position_0", + filter={"$or": [1, {"a": 1}]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with non-object at position 0 errors", + ), + QueryTestCase( + id="non_object_position_1", + filter={"$or": [{"a": 1}, 1]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with non-object at position 1 errors", + ), + QueryTestCase( + id="non_object_position_2", + filter={"$or": [{"a": 1}, {"b": 2}, 1]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with non-object at position 2 errors", + ), + QueryTestCase( + id="null_position_0", + filter={"$or": [None, {"a": 1}]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with null at position 0 errors", + ), + QueryTestCase( + id="null_position_1", + filter={"$or": [{"a": 1}, None]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with null at position 1 errors", + ), + QueryTestCase( + id="string_position_0", + filter={"$or": ["x", {"a": 1}]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with string at position 0 errors", + ), + QueryTestCase( + id="array_position_1", + filter={"$or": [{"a": 1}, [1, 2]]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with array at position 1 errors", + ), + QueryTestCase( + id="all_non_objects", + filter={"$or": [1, 2, 3]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with all non-object elements errors", + ), + QueryTestCase( + id="unknown_operator", + filter={"$or": [{"$invalidOp": 1}]}, + expected=None, + error_code=BAD_VALUE_ERROR, + msg="$or with unknown query operator errors", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ERROR_TESTS)) +def test_or_errors(collection, test): + """Test $or with invalid arguments returns correct error code.""" + collection.insert_one({"_id": 1, "a": 1}) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_field_lookup.py b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_field_lookup.py new file mode 100644 index 00000000..246d0871 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/logical/or/test_or_field_lookup.py @@ -0,0 +1,89 @@ +""" +Tests for $or with array and embedded document field lookups. + +Tests dot notation, array element matching, and deeply nested paths. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +ALL_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="array_element_match", + filter={"$or": [{"arr": 1}, {"arr": 2}]}, + doc=[ + {"_id": 1, "arr": [1, 3]}, + {"_id": 2, "arr": [2, 4]}, + {"_id": 3, "arr": [5, 6]}, + ], + expected=[{"_id": 1, "arr": [1, 3]}, {"_id": 2, "arr": [2, 4]}], + msg="$or matches docs where array contains either value", + ), + QueryTestCase( + id="dot_notation_array_index", + filter={"$or": [{"arr.0": 1}, {"arr.1": 2}]}, + doc=[ + {"_id": 1, "arr": [1, 9]}, + {"_id": 2, "arr": [9, 2]}, + {"_id": 3, "arr": [9, 9]}, + ], + expected=[{"_id": 1, "arr": [1, 9]}, {"_id": 2, "arr": [9, 2]}], + msg="$or with dot notation into array positions", + ), + QueryTestCase( + id="dot_notation_embedded_doc", + filter={"$or": [{"a.b": 1}, {"a.c": 2}]}, + doc=[ + {"_id": 1, "a": {"b": 1, "c": 9}}, + {"_id": 2, "a": {"b": 9, "c": 2}}, + {"_id": 3, "a": {"b": 9, "c": 9}}, + ], + expected=[ + {"_id": 1, "a": {"b": 1, "c": 9}}, + {"_id": 2, "a": {"b": 9, "c": 2}}, + ], + msg="$or with dot notation into embedded document", + ), + QueryTestCase( + id="deeply_nested_paths", + filter={"$or": [{"a.b.c": 1}, {"x.y.z": 2}]}, + doc=[ + {"_id": 1, "a": {"b": {"c": 1}}}, + {"_id": 2, "x": {"y": {"z": 2}}}, + {"_id": 3, "a": {"b": {"c": 9}}}, + ], + expected=[ + {"_id": 1, "a": {"b": {"c": 1}}}, + {"_id": 2, "x": {"y": {"z": 2}}}, + ], + msg="$or with deeply nested dot notation paths", + ), + QueryTestCase( + id="array_of_objects_dot_notation", + filter={"$or": [{"a.b": 1}, {"a.c": 2}]}, + doc=[ + {"_id": 1, "a": [{"b": 1}, {"c": 9}]}, + {"_id": 2, "a": [{"b": 9}, {"c": 2}]}, + {"_id": 3, "a": [{"b": 9}, {"c": 9}]}, + ], + expected=[ + {"_id": 1, "a": [{"b": 1}, {"c": 9}]}, + {"_id": 2, "a": [{"b": 9}, {"c": 2}]}, + ], + msg="$or with dot notation into array of objects", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_or_field_lookup(collection, test): + """Test $or with array and embedded document field lookups.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, msg=test.msg, ignore_doc_order=True)