From c2878e5371eea863a201640e11f401679ae555c3 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 11:24:16 -0700 Subject: [PATCH 01/10] migrate filter, in, indexOfArray, isArray Co-authored-by: Leszek Kurzyna Co-authored-by: Sean Oczkowski Signed-off-by: Alina (Xi) Li --- .../operator/expressions/array/__init__.py | 0 .../expressions/array/filter/__init__.py | 0 .../test_expression_filter_as_errors.py | 126 ++++ .../test_expression_filter_bson_types.py | 329 +++++++++ .../test_expression_filter_core_behavior.py | 439 ++++++++++++ .../filter/test_expression_filter_errors.py | 457 +++++++++++++ .../test_expression_filter_expressions.py | 184 +++++ ...test_expression_filter_structure_errors.py | 113 ++++ .../filter/test_smoke_expression_filter.py | 2 +- .../operator/expressions/array/in/__init__.py | 0 .../array/in/test_expression_in_bson_types.py | 265 ++++++++ .../in/test_expression_in_core_behavior.py | 250 +++++++ .../array/in/test_expression_in_errors.py | 201 ++++++ .../in/test_expression_in_expressions.py | 105 +++ .../in/test_expression_in_nested_arrays.py | 175 +++++ .../in/test_expression_in_null_missing.py | 84 +++ .../array/in/test_smoke_expression_in.py | 2 +- .../array/indexOfArray/__init__.py | 0 ...test_expression_indexOfArray_bson_types.py | 420 ++++++++++++ ...t_expression_indexOfArray_core_behavior.py | 606 +++++++++++++++++ .../test_expression_indexOfArray_errors.py | 630 ++++++++++++++++++ ...est_expression_indexOfArray_expressions.py | 114 ++++ ...st_expression_indexOfArray_null_missing.py | 115 ++++ .../test_smoke_expression_indexOfArray.py | 2 +- .../array/indexOfArray/utils/__init__.py | 0 .../indexOfArray/utils/indexOfArray_common.py | 41 ++ .../expressions/array/isArray/__init__.py | 0 .../test_expression_isArray_bson_types.py | 307 +++++++++ .../test_expression_isArray_core_behavior.py | 191 ++++++ .../isArray/test_expression_isArray_errors.py | 29 + .../test_expression_isArray_expressions.py | 177 +++++ .../isArray/test_smoke_expression_isArray.py | 2 +- .../array/isArray/utils/__init__.py | 0 .../array/isArray/utils/isArray_common.py | 15 + .../expressions/array/utils/__init__.py | 0 .../array/utils/arrays_in_common.py | 19 + ...array_arrayElemAt_indexOfArray_in_slice.py | 376 +++++++++++ .../test_expressions_combination_filter.py | 65 ++ .../test_expressions_combination_isArray.py | 54 ++ 39 files changed, 5891 insertions(+), 4 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_as_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_structure_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_null_missing.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/indexOfArray_common.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/isArray_common.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/arrays_in_common.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_filter.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_isArray.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_as_errors.py new file mode 100644 index 000000000..ed33c776d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_as_errors.py @@ -0,0 +1,126 @@ +""" +Error tests for $filter 'as' parameter. + +Tests invalid 'as' types. +""" + +from datetime import datetime + +import pytest +from bson import Binary, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, +) +from documentdb_tests.framework.error_codes import FAILED_TO_PARSE_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +INVALID_AS_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="type_int", + expression={"$filter": {"input": [1, 2, 3], "as": 1, "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=int should error", + ), + ExpressionTestCase( + id="type_long", + expression={"$filter": {"input": [1, 2, 3], "as": Int64(1), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=Int64 should error", + ), + ExpressionTestCase( + id="type_object", + expression={"$filter": {"input": [1, 2, 3], "as": {"a": 1}, "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=object should error", + ), + ExpressionTestCase( + id="type_array", + expression={"$filter": {"input": [1, 2, 3], "as": [1], "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=array should error", + ), + ExpressionTestCase( + id="type_minkey", + expression={"$filter": {"input": [1, 2, 3], "as": MinKey(), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=MinKey should error", + ), + ExpressionTestCase( + id="type_maxkey", + expression={"$filter": {"input": [1, 2, 3], "as": MaxKey(), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=MaxKey should error", + ), + ExpressionTestCase( + id="type_bindata", + expression={"$filter": {"input": [1, 2, 3], "as": Binary(b"\x00", 0), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=Binary should error", + ), + ExpressionTestCase( + id="type_objectid", + expression={"$filter": {"input": [1, 2, 3], "as": ObjectId(), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=ObjectId should error", + ), + ExpressionTestCase( + id="type_date", + expression={"$filter": {"input": [1, 2, 3], "as": datetime(2024, 1, 1), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=datetime should error", + ), + ExpressionTestCase( + id="type_timestamp", + expression={"$filter": {"input": [1, 2, 3], "as": Timestamp(0, 0), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=Timestamp should error", + ), + ExpressionTestCase( + id="type_regex", + expression={"$filter": {"input": [1, 2, 3], "as": Regex("pattern"), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=Regex should error", + ), + ExpressionTestCase( + id="type_bool_true", + expression={"$filter": {"input": [1, 2, 3], "as": True, "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=True should error", + ), + ExpressionTestCase( + id="type_null", + expression={"$filter": {"input": [1, 2, 3], "as": None, "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=None should error", + ), + ExpressionTestCase( + id="type_empty_string", + expression={"$filter": {"input": [1, 2, 3], "as": "", "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as='' should error", + ), + ExpressionTestCase( + id="type_nan", + expression={"$filter": {"input": [1, 2, 3], "as": float("nan"), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=NaN should error", + ), + ExpressionTestCase( + id="type_infinity", + expression={"$filter": {"input": [1, 2, 3], "as": float("inf"), "cond": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=inf should error", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_AS_TYPE_TESTS)) +def test_filter_invalid_as(collection, test): + """Test $filter with invalid 'as' parameter values.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_bson_types.py new file mode 100644 index 000000000..34f520d83 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_bson_types.py @@ -0,0 +1,329 @@ +""" +BSON type element preservation tests for $filter expression. + +Tests that various BSON types are preserved when filtering arrays +(using a cond that keeps all elements), including special numeric values +and boundary values. +""" + +from datetime import datetime, timezone +from uuid import UUID + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DOUBLE_NEGATIVE_ZERO, + FLOAT_INFINITY, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +# --------------------------------------------------------------------------- +# BSON types preserved +# --------------------------------------------------------------------------- +BSON_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="int64_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [Int64(1), Int64(2), Int64(3)]}, + expected=[Int64(1), Int64(2), Int64(3)], + msg="Should preserve Int64 values", + ), + ExpressionTestCase( + id="decimal128_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [Decimal128("1.5"), Decimal128("2.5")]}, + expected=[Decimal128("1.5"), Decimal128("2.5")], + msg="Should preserve Decimal128 values", + ), + ExpressionTestCase( + id="datetime_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={ + "arr": [ + datetime(2024, 1, 1, tzinfo=timezone.utc), + datetime(2024, 6, 1, tzinfo=timezone.utc), + ] + }, + expected=[ + datetime(2024, 1, 1, tzinfo=timezone.utc), + datetime(2024, 6, 1, tzinfo=timezone.utc), + ], + msg="Should preserve datetime values", + ), + ExpressionTestCase( + id="objectid_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]}, + expected=[ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")], + msg="Should preserve ObjectId values", + ), + ExpressionTestCase( + id="binary_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [Binary(b"\x01", 0), Binary(b"\x02", 0)]}, + expected=[b"\x01", b"\x02"], + msg="Should preserve Binary values", + ), + ExpressionTestCase( + id="binary_subtype_preservation", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [Binary(b"\x01", 128), Binary(b"\x02", 128)]}, + expected=[Binary(b"\x01", 128), Binary(b"\x02", 128)], + msg="Should preserve Binary subtype", + ), + ExpressionTestCase( + id="regex_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [Regex("^a", "i"), Regex("^b", "i")]}, + expected=[Regex("^a", "i"), Regex("^b", "i")], + msg="Should preserve Regex values", + ), + ExpressionTestCase( + id="timestamp_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [Timestamp(1, 0), Timestamp(2, 0)]}, + expected=[Timestamp(1, 0), Timestamp(2, 0)], + msg="Should preserve Timestamp values", + ), + ExpressionTestCase( + id="minkey_maxkey", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [MinKey(), MaxKey()]}, + expected=[MinKey(), MaxKey()], + msg="Should preserve MinKey/MaxKey values", + ), + ExpressionTestCase( + id="uuid_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={ + "arr": [ + Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), + Binary.from_uuid(UUID("fedcba98-7654-3210-0123-456789abcdef")), + ] + }, + expected=[ + Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), + Binary.from_uuid(UUID("fedcba98-7654-3210-0123-456789abcdef")), + ], + msg="Should preserve UUID binary values", + ), +] + +# --------------------------------------------------------------------------- +# Mixed BSON types +# --------------------------------------------------------------------------- +MIXED_BSON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="mixed_bson_types", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [1, "two", Int64(3), Decimal128("4"), True, None, MinKey()]}, + expected=[1, "two", Int64(3), Decimal128("4"), True, None, MinKey()], + msg="Should preserve mixed BSON types", + ), + ExpressionTestCase( + id="mixed_dates_and_ids", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={ + "arr": [ + datetime(2024, 1, 1, tzinfo=timezone.utc), + ObjectId("000000000000000000000001"), + Timestamp(1, 0), + ] + }, + expected=[ + datetime(2024, 1, 1, tzinfo=timezone.utc), + ObjectId("000000000000000000000001"), + Timestamp(1, 0), + ], + msg="Should preserve dates, ObjectIds, timestamps", + ), +] + +# --------------------------------------------------------------------------- +# Special numeric values as elements +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="infinity_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY]}, + expected=[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY], + msg="Should preserve infinity values", + ), + ExpressionTestCase( + id="decimal128_infinity", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY]}, + expected=[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY], + msg="Should preserve Decimal128 infinity values", + ), + ExpressionTestCase( + id="boundary_values", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [INT32_MIN, INT32_MAX, INT64_MIN, INT64_MAX]}, + expected=[INT32_MIN, INT32_MAX, INT64_MIN, INT64_MAX], + msg="Should preserve numeric boundary values", + ), + ExpressionTestCase( + id="negative_zero", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [DOUBLE_NEGATIVE_ZERO, DECIMAL128_NEGATIVE_ZERO]}, + expected=[DOUBLE_NEGATIVE_ZERO, DECIMAL128_NEGATIVE_ZERO], + msg="Should preserve negative zero values", + ), +] + +# --------------------------------------------------------------------------- +# Decimal128 precision preservation +# --------------------------------------------------------------------------- +DECIMAL128_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="decimal128_trailing_zeros", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [Decimal128("1.0"), Decimal128("1.00"), Decimal128("1.000")]}, + expected=[Decimal128("1.0"), Decimal128("1.00"), Decimal128("1.000")], + msg="Decimal128 trailing zeros preserved", + ), + ExpressionTestCase( + id="decimal128_nan", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [DECIMAL128_NAN, Decimal128("1")]}, + expected=[DECIMAL128_NAN, Decimal128("1")], + msg="Decimal128 NaN preserved", + ), +] + +# --------------------------------------------------------------------------- +# BSON type filtering with $eq condition +# --------------------------------------------------------------------------- +BSON_FILTER_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="filter_int64", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", Int64(2)]}}}, + doc={"arr": [Int64(1), Int64(2), Int64(3)]}, + expected=[Int64(2)], + msg="Should filter and preserve Int64", + ), + ExpressionTestCase( + id="filter_decimal128", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", Decimal128("2.5")]}}}, + doc={"arr": [Decimal128("1.5"), Decimal128("2.5")]}, + expected=[Decimal128("2.5")], + msg="Should filter and preserve Decimal128", + ), + ExpressionTestCase( + id="filter_datetime", + expression={ + "$filter": { + "input": "$arr", + "cond": {"$eq": ["$$this", datetime(2024, 6, 1, tzinfo=timezone.utc)]}, + } + }, + doc={ + "arr": [ + datetime(2024, 1, 1, tzinfo=timezone.utc), + datetime(2024, 6, 1, tzinfo=timezone.utc), + ] + }, + expected=[datetime(2024, 6, 1, tzinfo=timezone.utc)], + msg="Should filter and preserve datetime", + ), + ExpressionTestCase( + id="filter_objectid", + expression={ + "$filter": { + "input": "$arr", + "cond": {"$eq": ["$$this", ObjectId("000000000000000000000001")]}, + } + }, + doc={"arr": [ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]}, + expected=[ObjectId("000000000000000000000001")], + msg="Should filter and preserve ObjectId", + ), + ExpressionTestCase( + id="filter_binary_subtype", + expression={ + "$filter": {"input": "$arr", "cond": {"$eq": ["$$this", Binary(b"\x02", 128)]}} + }, + doc={"arr": [Binary(b"\x01", 128), Binary(b"\x02", 128)]}, + expected=[Binary(b"\x02", 128)], + msg="Should filter and preserve Binary subtype", + ), + ExpressionTestCase( + id="filter_timestamp", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", Timestamp(2, 0)]}}}, + doc={"arr": [Timestamp(1, 0), Timestamp(2, 0)]}, + expected=[Timestamp(2, 0)], + msg="Should filter and preserve Timestamp", + ), + ExpressionTestCase( + id="filter_regex", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", Regex("^b", "i")]}}}, + doc={"arr": [Regex("^a", "i"), Regex("^b", "i")]}, + expected=[Regex("^b", "i")], + msg="Should filter and preserve Regex", + ), + ExpressionTestCase( + id="filter_minkey", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", MinKey()]}}}, + doc={"arr": [MinKey(), MaxKey()]}, + expected=[MinKey()], + msg="Should filter and preserve MinKey", + ), + ExpressionTestCase( + id="filter_decimal128_nan_not_gte", + expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 1]}}}, + doc={"arr": [Decimal128("NaN")]}, + expected=[], + msg="Decimal128 NaN not >= 1", + ), + ExpressionTestCase( + id="filter_decimal128_neg_inf_not_gte", + expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 1]}}}, + doc={"arr": [Decimal128("-Infinity")]}, + expected=[], + msg="Decimal128 -Infinity not >= 1", + ), + ExpressionTestCase( + id="filter_decimal128_inf_gte", + expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 1]}}}, + doc={"arr": [Decimal128("Infinity")]}, + expected=[Decimal128("Infinity")], + msg="Decimal128 Infinity >= 1", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_BSON_TESTS = ( + BSON_TYPE_TESTS + + MIXED_BSON_TESTS + + SPECIAL_NUMERIC_TESTS + + DECIMAL128_PRECISION_TESTS + + BSON_FILTER_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) +def test_filter_bson_insert(collection, test): + """Test $filter BSON types with values from inserted documents.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_core_behavior.py new file mode 100644 index 000000000..1a6acd11e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_core_behavior.py @@ -0,0 +1,439 @@ +""" +Core behavior tests for $filter expression. + +Tests basic filtering, empty arrays, null propagation, custom 'as' variable, +various condition expressions, nested arrays, limit parameter, objects as +elements, and large arrays. +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import INT32_MAX + +# --------------------------------------------------------------------------- +# Success: basic filtering +# --------------------------------------------------------------------------- +BASIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="gt_filter", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 3]}}}, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[4, 5], + msg="Should keep elements greater than 3", + ), + ExpressionTestCase( + id="gte_filter", + expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 3]}}}, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[3, 4, 5], + msg="Should keep elements >= 3", + ), + ExpressionTestCase( + id="lt_filter", + expression={"$filter": {"input": "$arr", "cond": {"$lt": ["$$this", 3]}}}, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[1, 2], + msg="Should keep elements less than 3", + ), + ExpressionTestCase( + id="eq_filter", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", 2]}}}, + doc={"arr": [1, 2, 3, 2, 1]}, + expected=[2, 2], + msg="Should keep elements equal to 2", + ), + ExpressionTestCase( + id="ne_filter", + expression={"$filter": {"input": "$arr", "cond": {"$ne": ["$$this", 2]}}}, + doc={"arr": [1, 2, 3, 2, 1]}, + expected=[1, 3, 1], + msg="Should keep elements not equal to 2", + ), + ExpressionTestCase( + id="none_match", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 100]}}}, + doc={"arr": [1, 2, 3]}, + expected=[], + msg="Should return empty when none match", + ), + ExpressionTestCase( + id="string_filter", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", "abcd"]}}}, + doc={"arr": ["abcd", "efgh", "abcd", "zyz"]}, + expected=["abcd", "abcd"], + msg="Should filter strings abcd", + ), + ExpressionTestCase( + id="bool_cond_true", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [1, 2, 3]}, + expected=[1, 2, 3], + msg="Literal true cond should keep all elements", + ), + ExpressionTestCase( + id="bool_cond_false", + expression={"$filter": {"input": "$arr", "cond": False}}, + doc={"arr": [1, 2, 3]}, + expected=[], + msg="Literal false cond should keep no elements", + ), + ExpressionTestCase( + id="empty_array", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 0]}}}, + doc={"arr": []}, + expected=[], + msg="Should return empty array for empty input", + ), + ExpressionTestCase( + id="null_input", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 0]}}}, + doc={"arr": None}, + expected=None, + msg="Should return null when input is null", + ), + ExpressionTestCase( + id="custom_as_var", + expression={"$filter": {"input": "$arr", "as": "item", "cond": {"$gt": ["$$item", 3]}}}, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[4, 5], + msg="Should use custom 'as' variable name", + ), + ExpressionTestCase( + id="single_element_match", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 0]}}}, + doc={"arr": [42]}, + expected=[42], + msg="Should keep single matching element", + ), + ExpressionTestCase( + id="large_array_1000", + expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 500]}}}, + doc={"arr": list(range(1000))}, + expected=list(range(500, 1000)), + msg="Should filter large array", + ), +] + +# --------------------------------------------------------------------------- +# Success: nested arrays (filter does not recurse) +# --------------------------------------------------------------------------- +NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_arrays_by_size", + expression={"$filter": {"input": "$arr", "cond": {"$gt": [{"$size": "$$this"}, 1]}}}, + doc={"arr": [[1, 2], [3, 4, 5], [], [6]]}, + expected=[[1, 2], [3, 4, 5]], + msg="Should filter subarrays by size", + ), + ExpressionTestCase( + id="nested_arrays_identity", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": [[1], [2], [3]]}, + expected=[[1], [2], [3]], + msg="Should preserve nested arrays when all match", + ), +] + +# --------------------------------------------------------------------------- +# Success: elements with null +# --------------------------------------------------------------------------- +NULL_ELEMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="filter_out_nulls", + expression={"$filter": {"input": "$arr", "cond": {"$ne": ["$$this", None]}}}, + doc={"arr": [1, None, 2, None, 3]}, + expected=[1, 2, 3], + msg="Should filter out null elements", + ), + ExpressionTestCase( + id="keep_only_nulls", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", None]}}}, + doc={"arr": [1, None, 2, None]}, + expected=[None, None], + msg="Should keep only null elements", + ), +] + +# --------------------------------------------------------------------------- +# Success: objects as elements +# --------------------------------------------------------------------------- +OBJECT_ELEMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="filter_objects_by_nested_field", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this.a.b", 2]}}}, + doc={"arr": [{"a": {"b": 1}}, {"a": {"b": 5}}, {"a": {"b": 3}}]}, + expected=[{"a": {"b": 5}}, {"a": {"b": 3}}], + msg="Should filter objects by nested field", + ), + ExpressionTestCase( + id="duplicate_objects", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this.a", 1]}}}, + doc={"arr": [{"a": 1}, {"a": 1}, {"a": 2}]}, + expected=[{"a": 1}, {"a": 1}], + msg="Should return all matching duplicate objects", + ), + ExpressionTestCase( + id="single_object_no_match", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this.a", 10]}}}, + doc={"arr": [{"a": 1}]}, + expected=[], + msg="Should return empty array when single object does not match", + ), +] + +# --------------------------------------------------------------------------- +# Success: limit parameter +# --------------------------------------------------------------------------- +LIMIT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="limit_1", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 2]}, "limit": 1}}, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[3], + msg="Should return at most 1 matching element", + ), + ExpressionTestCase( + id="limit_2", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 2]}, "limit": 2}}, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[3, 4], + msg="Should return at most 2 matching elements", + ), + ExpressionTestCase( + id="limit_exceeds_matches", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 3]}, "limit": 10}}, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[4, 5], + msg="Limit > matches should return all matches", + ), + ExpressionTestCase( + id="limit_on_empty", + expression={"$filter": {"input": "$arr", "cond": True, "limit": 5}}, + doc={"arr": []}, + expected=[], + msg="Limit on empty array returns empty", + ), + ExpressionTestCase( + id="limit_with_none_matching", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 100]}, "limit": 2}}, + doc={"arr": [1, 2, 3]}, + expected=[], + msg="Limit with no matches returns empty", + ), + ExpressionTestCase( + id="limit_long", + expression={ + "$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 2]}, "limit": Int64(1)} + }, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[3], + msg="Long limit should work", + ), + ExpressionTestCase( + id="limit_double_whole", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 2]}, "limit": 1.0}}, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[3], + msg="Whole-number double limit should work", + ), + ExpressionTestCase( + id="limit_decimal128", + expression={ + "$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 2]}, "limit": Decimal128("1")} + }, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[3], + msg="Decimal128 limit should work", + ), + ExpressionTestCase( + id="limit_with_duplicates", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", 2]}, "limit": 2}}, + doc={"arr": [2, 2, 2]}, + expected=[2, 2], + msg="Limit 2 with duplicates returns first 2 matches", + ), + ExpressionTestCase( + id="limit_int32_max", + expression={"$filter": {"input": "$arr", "cond": True, "limit": INT32_MAX}}, + doc={"arr": [1, 2, 3]}, + expected=[1, 2, 3], + msg="INT32_MAX limit should return all matches", + ), + ExpressionTestCase( + id="limit_null", + expression={"$filter": {"input": "$arr", "cond": True, "limit": None}}, + doc={"arr": [1, 2, 3]}, + expected=[1, 2, 3], + msg="Null limit should return all matches", + ), + ExpressionTestCase( + id="limit_missing_field_ref", + expression={"$filter": {"input": "$arr", "cond": True, "limit": "$nonexistent"}}, + doc={"arr": [1, 2, 3]}, + expected=[1, 2, 3], + msg="Missing field reference for limit should return all matches", + ), +] + +# --------------------------------------------------------------------------- +# Success: type-based filtering +# --------------------------------------------------------------------------- +TYPE_FILTER_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="filter_by_type", + expression={"$filter": {"input": "$arr", "cond": {"$eq": [{"$type": "$$this"}, "int"]}}}, + doc={"arr": [1, "two", True, None, [5], {"a": 1}]}, + expected=[1], + msg="Should filter by BSON type", + ), + ExpressionTestCase( + id="filter_strings_only", + expression={"$filter": {"input": "$arr", "cond": {"$eq": [{"$type": "$$this"}, "string"]}}}, + doc={"arr": [1, "good", 2, "morning", True]}, + expected=["good", "morning"], + msg="Should keep only string elements", + ), +] + +# --------------------------------------------------------------------------- +# Success: cond true or false +# --------------------------------------------------------------------------- +COND_FALSY_TRUTHY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="cond_nonzero_truthy", + expression={"$filter": {"input": "$arr", "cond": "$$this"}}, + doc={"arr": [0, 1, 2, 0, 3]}, + expected=[1, 2, 3], + msg="Non-zero values are truthy", + ), + ExpressionTestCase( + id="cond_null_falsy", + expression={"$filter": {"input": "$arr", "cond": "$$this"}}, + doc={"arr": [1, None, 2, None]}, + expected=[1, 2], + msg="Null is falsy", + ), + ExpressionTestCase( + id="cond_zero_falsy", + expression={"$filter": {"input": "$arr", "cond": 0}}, + doc={"arr": [1, 2]}, + expected=[], + msg="0 is falsy", + ), + ExpressionTestCase( + id="cond_empty_string_truthy", + expression={"$filter": {"input": "$arr", "cond": ""}}, + doc={"arr": [1, 2]}, + expected=[1, 2], + msg="Empty string is truthy in MongoDB", + ), + ExpressionTestCase( + id="cond_object_truthy", + expression={"$filter": {"input": "$arr", "cond": {"x": 10}}}, + doc={"arr": [1, 2]}, + expected=[1, 2], + msg="Object is truthy", + ), + ExpressionTestCase( + id="cond_empty_object_truthy", + expression={"$filter": {"input": "$arr", "cond": {}}}, + doc={"arr": [1, 2]}, + expected=[1, 2], + msg="Empty object is truthy", + ), + ExpressionTestCase( + id="cond_empty_array_truthy", + expression={"$filter": {"input": "$arr", "cond": []}}, + doc={"arr": [1, 2]}, + expected=[1, 2], + msg="Empty array is truthy", + ), +] + + +# --------------------------------------------------------------------------- +# Success: type strict equality +# --------------------------------------------------------------------------- +TYPE_STRICT_EQUALITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="false_vs_zero", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", False]}}}, + doc={"arr": [False, 0, 1, True]}, + expected=[False], + msg="$eq false should match only false, not 0", + ), + ExpressionTestCase( + id="true_vs_one", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", True]}}}, + doc={"arr": [True, 1, 0, False]}, + expected=[True], + msg="$eq true should match only true, not 1", + ), + ExpressionTestCase( + id="empty_string_vs_null", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", ""]}}}, + doc={"arr": ["", None, "a"]}, + expected=[""], + msg="$eq '' should match only empty string, not null", + ), + ExpressionTestCase( + id="null_vs_zero_false_empty_string", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", None]}}}, + doc={"arr": [None, 0, False, ""]}, + expected=[None], + msg="$eq null should match only null, not 0, false, or empty string", + ), +] + +# --------------------------------------------------------------------------- +# Success: numeric equivalence +# --------------------------------------------------------------------------- +NUMERIC_EQUIVALENCE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="numeric_equivalence_one", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", 1]}}}, + doc={"arr": [1, Int64(1), 1.0, Decimal128("1")]}, + expected=[1, Int64(1), 1.0, Decimal128("1")], + msg="All numeric representations of 1 should match", + ), + ExpressionTestCase( + id="numeric_equivalence_zero", + expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", 0]}}}, + doc={"arr": [0, Int64(0), 0.0, Decimal128("0")]}, + expected=[0, Int64(0), 0.0, Decimal128("0")], + msg="All numeric representations of 0 should match", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + BASIC_TESTS + + NESTED_ARRAY_TESTS + + NULL_ELEMENT_TESTS + + OBJECT_ELEMENT_TESTS + + LIMIT_TESTS + + TYPE_FILTER_TESTS + + COND_FALSY_TRUTHY_TESTS + + TYPE_STRICT_EQUALITY_TESTS + + NUMERIC_EQUIVALENCE_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_filter_insert(collection, test): + """Test $filter with values from inserted documents.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_errors.py new file mode 100644 index 000000000..100fb100b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_errors.py @@ -0,0 +1,457 @@ +""" +Error tests for $filter expression. + +Tests non-array input (all BSON types, special numeric values, boundary values) +and limit parameter validation. +Note: $filter propagates null — null input returns null (tested in core_behavior). +""" + +from datetime import datetime + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + FILTER_INPUT_NOT_ARRAY_ERROR, + FILTER_LIMIT_NOT_INTEGRAL_ERROR, + FILTER_LIMIT_NOT_POSITIVE_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DOUBLE_NEGATIVE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +# --------------------------------------------------------------------------- +# Error: non-array input — standard BSON types +# --------------------------------------------------------------------------- +NOT_ARRAY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="string_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": "hello"}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject string input", + ), + ExpressionTestCase( + id="int_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": 42}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject int input", + ), + ExpressionTestCase( + id="negative_int_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": -42}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject negative int input", + ), + ExpressionTestCase( + id="bool_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": True}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject bool input", + ), + ExpressionTestCase( + id="bool_false_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": False}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject bool false input", + ), + ExpressionTestCase( + id="object_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": {"a": 1}}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject object input", + ), + ExpressionTestCase( + id="double_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": 3.14}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject double input", + ), + ExpressionTestCase( + id="negative_double_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": -3.14}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject negative double input", + ), + ExpressionTestCase( + id="decimal128_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": Decimal128("1")}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject decimal128 input", + ), + ExpressionTestCase( + id="int64_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": Int64(1)}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject int64 input", + ), + ExpressionTestCase( + id="objectid_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": ObjectId()}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject objectid input", + ), + ExpressionTestCase( + id="datetime_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": datetime(2024, 1, 1)}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject datetime input", + ), + ExpressionTestCase( + id="binary_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": Binary(b"x", 0)}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject binary input", + ), + ExpressionTestCase( + id="regex_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": Regex("x")}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject regex input", + ), + ExpressionTestCase( + id="maxkey_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": MaxKey()}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject maxkey input", + ), + ExpressionTestCase( + id="minkey_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": MinKey()}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject minkey input", + ), + ExpressionTestCase( + id="timestamp_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": Timestamp(0, 0)}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject timestamp input", + ), +] + +# --------------------------------------------------------------------------- +# Error: special float/Decimal128 values +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nan_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": FLOAT_NAN}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject NaN input", + ), + ExpressionTestCase( + id="inf_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": FLOAT_INFINITY}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Infinity input", + ), + ExpressionTestCase( + id="neg_inf_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": FLOAT_NEGATIVE_INFINITY}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject -Infinity input", + ), + ExpressionTestCase( + id="neg_zero_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": DOUBLE_NEGATIVE_ZERO}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject negative zero input", + ), + ExpressionTestCase( + id="decimal128_nan_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": DECIMAL128_NAN}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 NaN input", + ), + ExpressionTestCase( + id="decimal128_neg_nan_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": Decimal128("-NaN")}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 -NaN input", + ), + ExpressionTestCase( + id="decimal128_inf_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": DECIMAL128_INFINITY}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 Infinity input", + ), + ExpressionTestCase( + id="decimal128_neg_inf_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": DECIMAL128_NEGATIVE_INFINITY}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 -Infinity input", + ), + ExpressionTestCase( + id="decimal128_neg_zero_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": DECIMAL128_NEGATIVE_ZERO}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 -0 input", + ), +] + +# --------------------------------------------------------------------------- +# Error: numeric boundary values +# --------------------------------------------------------------------------- +BOUNDARY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="int32_max_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": INT32_MAX}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject INT32_MAX input", + ), + ExpressionTestCase( + id="int32_min_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": INT32_MIN}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject INT32_MIN input", + ), + ExpressionTestCase( + id="int64_max_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": INT64_MAX}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject INT64_MAX input", + ), + ExpressionTestCase( + id="int64_min_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": INT64_MIN}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject INT64_MIN input", + ), + ExpressionTestCase( + id="decimal128_max_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": DECIMAL128_MAX}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject DECIMAL128_MAX input", + ), + ExpressionTestCase( + id="decimal128_min_input", + expression={"$filter": {"input": "$arr", "cond": True}}, + doc={"arr": DECIMAL128_MIN}, + error_code=FILTER_INPUT_NOT_ARRAY_ERROR, + msg="Should reject DECIMAL128_MIN input", + ), +] + +# --------------------------------------------------------------------------- +# Error: invalid limit types +# --------------------------------------------------------------------------- +INVALID_LIMIT_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="string_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": "1"}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="String limit should error", + ), + ExpressionTestCase( + id="bool_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": True}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="Bool limit should error", + ), + ExpressionTestCase( + id="object_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": {"a": 1}}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="Object limit should error", + ), + ExpressionTestCase( + id="array_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": [1]}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="Array limit should error", + ), +] + +# --------------------------------------------------------------------------- +# Error: invalid limit numeric values +# --------------------------------------------------------------------------- +INVALID_LIMIT_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="zero_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": 0}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_POSITIVE_ERROR, + msg="limit 0 should error", + ), + ExpressionTestCase( + id="neg_int_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": -1}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_POSITIVE_ERROR, + msg="Negative int should error", + ), + ExpressionTestCase( + id="neg_long_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": Int64(-1)}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_POSITIVE_ERROR, + msg="Negative long should error", + ), + ExpressionTestCase( + id="neg_double_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": -1.0}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_POSITIVE_ERROR, + msg="Negative double should error", + ), + ExpressionTestCase( + id="neg_decimal128_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": Decimal128("-1")}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_POSITIVE_ERROR, + msg="Negative decimal128 should error", + ), + ExpressionTestCase( + id="fractional_1_5_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": 1.5}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="Fractional 1.5 should error", + ), + ExpressionTestCase( + id="fractional_dec_0_5_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": Decimal128("0.5")}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="Fractional decimal128 0.5 should error", + ), + ExpressionTestCase( + id="nan_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": float("nan")}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="NaN should error", + ), + ExpressionTestCase( + id="inf_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": float("inf")}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="Infinity should error", + ), + ExpressionTestCase( + id="neg_inf_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": float("-inf")}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="-Infinity should error", + ), + ExpressionTestCase( + id="decimal128_nan_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": Decimal128("NaN")}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="Decimal128 NaN should error", + ), + ExpressionTestCase( + id="decimal128_inf_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": Decimal128("Infinity")}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, + msg="Decimal128 Infinity should error", + ), + ExpressionTestCase( + id="neg_zero_double_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": -0.0}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_POSITIVE_ERROR, + msg="-0.0 limit should error", + ), + ExpressionTestCase( + id="neg_zero_decimal128_limit", + expression={"$filter": {"input": "$arr", "cond": True, "limit": Decimal128("-0")}}, + doc={"arr": [1, 2, 3]}, + error_code=FILTER_LIMIT_NOT_POSITIVE_ERROR, + msg="Decimal128 -0 limit should error", + ), +] + +# --------------------------------------------------------------------------- +# Error: cond evaluation errors +# --------------------------------------------------------------------------- +COND_EVALUATION_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="cond_divide_by_zero", + expression={"$filter": {"input": "$arr", "cond": {"$divide": [1, 0]}}}, + doc={"arr": [1, 2]}, + error_code=BAD_VALUE_ERROR, + msg="Division by zero in cond should error", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + NOT_ARRAY_ERROR_TESTS + + SPECIAL_NUMERIC_ERROR_TESTS + + BOUNDARY_ERROR_TESTS + + INVALID_LIMIT_TYPE_TESTS + + INVALID_LIMIT_NUMERIC_TESTS + + COND_EVALUATION_ERROR_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_filter_error_insert(collection, test): + """Test $filter error with values from inserted documents.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_expressions.py new file mode 100644 index 000000000..e21ec9be7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_expressions.py @@ -0,0 +1,184 @@ +""" +Expression and field path tests for $filter expression. + +Tests field path lookups, composite paths, system variables, +null/missing propagation via expressions, nested $filter, and +access to outer document fields in cond. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_field_path", + expression={"$filter": {"input": "$a.b", "cond": {"$gt": ["$$this", 2]}}}, + doc={"a": {"b": [1, 2, 3, 4]}}, + expected=[3, 4], + msg="Should resolve nested field path", + ), + ExpressionTestCase( + id="deeply_nested_field", + expression={"$filter": {"input": "$a.b.c", "cond": True}}, + doc={"a": {"b": {"c": [10, 20]}}}, + expected=[10, 20], + msg="Should resolve deeply nested field path", + ), + ExpressionTestCase( + id="composite_array_path", + expression={"$filter": {"input": "$a.b", "cond": {"$gt": ["$$this", 1]}}}, + doc={"a": [{"b": 1}, {"b": 2}, {"b": 3}]}, + expected=[2, 3], + msg="Composite array path should resolve to array", + ), + ExpressionTestCase( + id="index_path_on_object_key", + expression={"$filter": {"input": "$a.0.b", "cond": True}}, + doc={"a": {"0": {"b": [1, 2, 3]}}}, + expected=[1, 2, 3], + msg="Object key '0' resolves correctly", + ), + ExpressionTestCase( + id="object_key_zero", + expression={"$filter": {"input": "$a.0", "cond": True}}, + doc={"a": {"0": [1, 2, 3]}}, + expected=[1, 2, 3], + msg="$a.0 resolves as field named '0' on object", + ), + ExpressionTestCase( + id="access_outer_field", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", "$threshold"]}}}, + doc={"arr": [1, 2, 3, 4, 5], "threshold": 3}, + expected=[4, 5], + msg="Should access outer document field in cond", + ), +] + +# --------------------------------------------------------------------------- +# $let and system variables +# --------------------------------------------------------------------------- +LET_AND_VARIABLE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="let_variable", + expression={ + "$let": { + "vars": {"arr": "$values"}, + "in": {"$filter": {"input": "$$arr", "cond": {"$gt": ["$$this", 2]}}}, + } + }, + doc={"values": [1, 2, 3, 4]}, + expected=[3, 4], + msg="Should work with $let variables", + ), + ExpressionTestCase( + id="root_variable", + expression={"$filter": {"input": "$$ROOT.values", "cond": {"$gt": ["$$this", 2]}}}, + doc={"_id": 1, "values": [1, 2, 3, 4]}, + expected=[3, 4], + msg="Should work with $$ROOT", + ), + ExpressionTestCase( + id="current_variable", + expression={"$filter": {"input": "$$CURRENT.values", "cond": {"$gt": ["$$this", 2]}}}, + doc={"_id": 2, "values": [1, 2, 3, 4]}, + expected=[3, 4], + msg="$$CURRENT should be equivalent to field path", + ), +] + +# --------------------------------------------------------------------------- +# Null/missing via expression +# --------------------------------------------------------------------------- +NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="missing_field", + expression={"$filter": {"input": "$nonexistent", "cond": True}}, + doc={"other": 1}, + expected=None, + msg="Missing field should return null", + ), + ExpressionTestCase( + id="remove_variable", + expression={"$filter": {"input": "$$REMOVE", "cond": True}}, + doc={"x": 1}, + expected=None, + msg="$$REMOVE propagates null", + ), +] + +# --------------------------------------------------------------------------- +# Nested $filter +# --------------------------------------------------------------------------- +NESTED_FILTER_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="filter_then_filter", + expression={ + "$filter": { + "input": {"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 1]}}}, + "cond": {"$lt": ["$$this", 5]}, + } + }, + doc={"arr": [1, 2, 3, 4, 5, 6]}, + expected=[2, 3, 4], + msg="Nested $filter should chain conditions", + ), +] + + +# --------------------------------------------------------------------------- +# Limit with field reference +# --------------------------------------------------------------------------- +LIMIT_EXPR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="limit_from_field", + expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 0]}, "limit": "$n"}}, + doc={"arr": [1, 2, 3, 4, 5], "n": 2}, + expected=[1, 2], + msg="Limit from field reference", + ), +] + +# --------------------------------------------------------------------------- +# Literal array input (not field path) +# --------------------------------------------------------------------------- +LITERAL_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="literal_array_input", + expression={"$filter": {"input": [1, 2, 3, 4, 5], "cond": {"$gt": ["$$this", 3]}}}, + doc={"x": 1}, + expected=[4, 5], + msg="Should filter literal array input", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_EXPR_TESTS = ( + FIELD_LOOKUP_TESTS + + LET_AND_VARIABLE_TESTS + + NULL_MISSING_EXPR_TESTS + + NESTED_FILTER_TESTS + + LIMIT_EXPR_TESTS + + LITERAL_INPUT_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) +def test_filter_expression(collection, test): + """Test $filter with field paths and expressions.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_structure_errors.py new file mode 100644 index 000000000..7f588a403 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_structure_errors.py @@ -0,0 +1,113 @@ +""" +Structural error tests for $filter expression. + +Tests invalid $filter argument structure: non-object argument, unknown/misspelled fields, +and missing required fields (input, cond). +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, +) +from documentdb_tests.framework.error_codes import ( + FILTER_MISSING_COND_ERROR, + FILTER_MISSING_INPUT_ERROR, + FILTER_NON_OBJECT_ARG_ERROR, + FILTER_UNKNOWN_FIELD_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Error: non-object argument +# --------------------------------------------------------------------------- +NON_OBJECT_ARG_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="null_arg", + expression={"$filter": None}, + error_code=FILTER_NON_OBJECT_ARG_ERROR, + msg="Null arg should error", + ), + ExpressionTestCase( + id="int_arg", + expression={"$filter": 1}, + error_code=FILTER_NON_OBJECT_ARG_ERROR, + msg="Int arg should error", + ), + ExpressionTestCase( + id="string_arg", + expression={"$filter": "string"}, + error_code=FILTER_NON_OBJECT_ARG_ERROR, + msg="String arg should error", + ), + ExpressionTestCase( + id="array_arg", + expression={"$filter": []}, + error_code=FILTER_NON_OBJECT_ARG_ERROR, + msg="Array arg should error", + ), + ExpressionTestCase( + id="bool_arg", + expression={"$filter": True}, + error_code=FILTER_NON_OBJECT_ARG_ERROR, + msg="Bool arg should error", + ), +] + +# --------------------------------------------------------------------------- +# Error: unknown fields +# --------------------------------------------------------------------------- +UNKNOWN_FIELD_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="extra_unknown", + expression={"$filter": {"input": [1], "cond": True, "unknown": 1}}, + error_code=FILTER_UNKNOWN_FIELD_ERROR, + msg="Extra unknown field should error", + ), + ExpressionTestCase( + id="only_unknown", + expression={"$filter": {"dummy": 124}}, + error_code=FILTER_UNKNOWN_FIELD_ERROR, + msg="Only unknown field should error", + ), +] + +# --------------------------------------------------------------------------- +# Error: missing required fields +# --------------------------------------------------------------------------- +MISSING_REQUIRED_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="missing_input", + expression={"$filter": {"as": "x", "cond": True}}, + error_code=FILTER_MISSING_INPUT_ERROR, + msg="Missing input should error", + ), + ExpressionTestCase( + id="missing_cond", + expression={"$filter": {"input": [1, 2, 3]}}, + error_code=FILTER_MISSING_COND_ERROR, + msg="Missing cond should error", + ), + ExpressionTestCase( + id="empty_object", + expression={"$filter": {}}, + error_code=FILTER_MISSING_INPUT_ERROR, + msg="Empty object should error", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_STRUCTURE_TESTS = NON_OBJECT_ARG_TESTS + UNKNOWN_FIELD_TESTS + MISSING_REQUIRED_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_STRUCTURE_TESTS)) +def test_filter_structure_error(collection, test): + """Test $filter argument structure validation.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_expression_filter.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_expression_filter.py index 49048c03b..add4d436f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_expression_filter.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_expression_filter.py @@ -40,4 +40,4 @@ def test_smoke_expression_filter(collection): ) expected = [{"_id": 1, "filtered": [4, 5]}, {"_id": 2, "filtered": [10, 15, 20, 25]}] - assertSuccess(result, expected, msg="Should support $filter expression") + assertSuccess(result, expected, "Should support $filter expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py new file mode 100644 index 000000000..004620a75 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py @@ -0,0 +1,265 @@ +""" +BSON type and numeric equivalence tests for $in expression. + +Tests searching for various BSON types and cross-type numeric matching. +""" + +from datetime import datetime +from uuid import UUID + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 + InTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +# --------------------------------------------------------------------------- +# Success: search for various BSON types +# --------------------------------------------------------------------------- +BSON_TYPE_TESTS: list[InTest] = [ + InTest( + id="bson_int64", + value=Int64(99), + array=[Int64(99), 1], + expected=True, + msg="Should find Int64 in array", + ), + InTest( + id="bson_decimal128", + value=Decimal128("1.5"), + array=[Decimal128("1.5"), 2], + expected=True, + msg="Should find Decimal128 in array", + ), + InTest( + id="bson_datetime", + value=datetime(2024, 1, 1), + array=[datetime(2024, 1, 1), 1], + expected=True, + msg="Should find datetime in array", + ), + InTest( + id="bson_objectid", + value=ObjectId("000000000000000000000001"), + array=[ObjectId("000000000000000000000001"), 1], + expected=True, + msg="Should find ObjectId in array", + ), + InTest( + id="bson_binary", + value=Binary(b"\x01\x02", 0), + array=[Binary(b"\x01\x02", 0), 1], + expected=True, + msg="Should find Binary in array", + ), + InTest( + id="bson_regex", + value=Regex("^abc", "i"), + array=[Regex("^abc", "i"), 1], + expected=True, + msg="Should find Regex in array", + ), + InTest( + id="bson_timestamp", + value=Timestamp(1, 1), + array=[Timestamp(1, 1), 1], + expected=True, + msg="Should find Timestamp in array", + ), + InTest( + id="bson_minkey", + value=MinKey(), + array=[MinKey(), 1], + expected=True, + msg="Should find MinKey in array", + ), + InTest( + id="bson_maxkey", + value=MaxKey(), + array=[1, MaxKey()], + expected=True, + msg="Should find MaxKey in array", + ), + InTest( + id="bson_uuid", + value=Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), + array=[Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), 1], + expected=True, + msg="Should find UUID binary in array", + ), + # Special float values + InTest( + id="float_infinity_in_array", + value=FLOAT_INFINITY, + array=[FLOAT_INFINITY, 1], + expected=True, + msg="Should find Infinity in array", + ), + InTest( + id="float_neg_infinity_in_array", + value=FLOAT_NEGATIVE_INFINITY, + array=[FLOAT_NEGATIVE_INFINITY, 1], + expected=True, + msg="Should find -Infinity in array", + ), + InTest( + id="float_infinity_not_in_array", + value=FLOAT_INFINITY, + array=[1, 2, 3], + expected=False, + msg="Should not find Infinity in non-Infinity array", + ), + # Special Decimal128 values + InTest( + id="decimal128_infinity_in_array", + value=DECIMAL128_INFINITY, + array=[DECIMAL128_INFINITY, 1], + expected=True, + msg="Should find Decimal128 Infinity in array", + ), + InTest( + id="decimal128_neg_infinity_in_array", + value=DECIMAL128_NEGATIVE_INFINITY, + array=[DECIMAL128_NEGATIVE_INFINITY, 1], + expected=True, + msg="Should find Decimal128 -Infinity in array", + ), + # NaN equality: NaN == NaN in BSON comparison (unlike IEEE 754) + InTest( + id="float_nan_found", + value=FLOAT_NAN, + array=[FLOAT_NAN, 1], + expected=True, + msg="Should find NaN in array (BSON equality)", + ), + InTest( + id="decimal128_nan_found", + value=DECIMAL128_NAN, + array=[DECIMAL128_NAN, 1], + expected=True, + msg="Should find Decimal128 NaN in array (BSON equality)", + ), + InTest( + id="float_nan_matches_decimal128_nan", + value=FLOAT_NAN, + array=[DECIMAL128_NAN, 1], + expected=True, + msg="float NaN should match Decimal128 NaN cross-type", + ), + InTest( + id="decimal128_nan_matches_float_nan", + value=DECIMAL128_NAN, + array=[FLOAT_NAN, 1], + expected=True, + msg="Decimal128 NaN should match float NaN cross-type", + ), + # Aggregation $in does NOT pattern-match regex against strings (unlike query $in) + InTest( + id="regex_no_pattern_match", + value=Regex("^a"), + array=["abc", "def"], + expected=False, + msg="Regex should not pattern-match strings in aggregation $in", + ), +] + +# --------------------------------------------------------------------------- +# Success: numeric type equivalence +# --------------------------------------------------------------------------- +NUMERIC_EQUIVALENCE_TESTS: list[InTest] = [ + InTest( + id="int_in_doubles", + value=1, + array=[1.0, 2.0], + expected=True, + msg="Should find int in doubles via numeric equivalence", + ), + InTest( + id="int_in_longs", + value=1, + array=[Int64(1), 2], + expected=True, + msg="Should find int in longs via numeric equivalence", + ), + InTest( + id="int_in_decimal128s", + value=1, + array=[Decimal128("1"), 2], + expected=True, + msg="Should find int in decimal128s via numeric equivalence", + ), + InTest( + id="double_in_ints", + value=1.0, + array=[1, 2], + expected=True, + msg="Should find double in ints via numeric equivalence", + ), + InTest( + id="long_in_ints", + value=Int64(1), + array=[1, 2], + expected=True, + msg="Should find long in ints via numeric equivalence", + ), + InTest( + id="decimal128_in_ints", + value=Decimal128("1"), + array=[1, 2], + expected=True, + msg="Should find decimal128 in ints via numeric equivalence", + ), + InTest( + id="decimal128_in_doubles", + value=Decimal128("1.5"), + array=[1.5, 2.5], + expected=True, + msg="Should find decimal128 in doubles via numeric equivalence", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = BSON_TYPE_TESTS + NUMERIC_EQUIVALENCE_TESTS + +TEST_SUBSET_FOR_LITERAL = [ + BSON_TYPE_TESTS[0], # bson_int64 + BSON_TYPE_TESTS[-1], # decimal128_nan_found + NUMERIC_EQUIVALENCE_TESTS[0], # int_in_doubles +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_in_literal(collection, test): + """Test $in BSON types with literal values.""" + result = execute_expression(collection, {"$in": [test.value, test.array]}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_in_insert(collection, test): + """Test $in BSON types with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$in": ["$val", "$arr"]}, {"val": test.value, "arr": test.array} + ) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py new file mode 100644 index 000000000..ebce36d03 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py @@ -0,0 +1,250 @@ +""" +Core behavior tests for $in expression. + +Tests value found/not found, mixed types, and large arrays. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 + InTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: value found in array → True +# --------------------------------------------------------------------------- +FOUND_TESTS: list[InTest] = [ + InTest(id="found_int", value=2, array=[1, 2, 3], expected=True, msg="Should find int in array"), + InTest( + id="found_first", value=1, array=[1, 2, 3], expected=True, msg="Should find first element" + ), + InTest( + id="found_last", value=3, array=[1, 2, 3], expected=True, msg="Should find last element" + ), + InTest( + id="found_string", + value="b", + array=["a", "b", "c"], + expected=True, + msg="Should find string in array", + ), + InTest( + id="found_bool_true", + value=True, + array=[True, False], + expected=True, + msg="Should find true in array", + ), + InTest( + id="found_bool_false", + value=False, + array=[True, False], + expected=True, + msg="Should find false in array", + ), + InTest( + id="found_null", + value=None, + array=[None, 1, 2], + expected=True, + msg="Should find null in array", + ), + InTest( + id="found_nested_array", + value=[3, 4], + array=[[1, 2], [3, 4]], + expected=True, + msg="Should find nested array", + ), + InTest( + id="found_object", + value={"a": 1}, + array=[{"a": 1}, {"b": 2}], + expected=True, + msg="Should find object in array", + ), + InTest( + id="found_single_element", + value=42, + array=[42], + expected=True, + msg="Should find value in single-element array", + ), + InTest( + id="found_duplicate", + value=5, + array=[5, 5, 5], + expected=True, + msg="Should find value in array of duplicates", + ), +] + +# --------------------------------------------------------------------------- +# Success: value not found → False +# --------------------------------------------------------------------------- +NOT_FOUND_TESTS: list[InTest] = [ + InTest( + id="not_found_int", + value=4, + array=[1, 2, 3], + expected=False, + msg="Should not find absent int", + ), + InTest( + id="not_found_string", + value="z", + array=["a", "b"], + expected=False, + msg="Should not find absent string", + ), + InTest( + id="not_found_empty_array", + value=1, + array=[], + expected=False, + msg="Should not find value in empty array", + ), + InTest( + id="not_found_type_mismatch", + value="1", + array=[1, 2, 3], + expected=False, + msg="Should not find string '1' in int array", + ), + InTest( + id="not_found_bool_vs_int", + value=True, + array=[1, 0], + expected=False, + msg="Should not find bool in int array", + ), + InTest( + id="not_found_null", + value=None, + array=[1, 2, 3], + expected=False, + msg="Should not find null in non-null array", + ), + InTest( + id="not_found_partial_array", + value=[1], + array=[[1, 2], [3, 4]], + expected=False, + msg="Should not find partial array match", + ), + InTest( + id="not_found_partial_object", + value={"a": 1}, + array=[{"a": 1, "b": 2}], + expected=False, + msg="Should not find partial object match", + ), +] + +# --------------------------------------------------------------------------- +# Success: mixed types in array +# --------------------------------------------------------------------------- +MIXED_TYPE_TESTS: list[InTest] = [ + InTest( + id="mixed_find_string", + value="2", + array=[1, "2", True, None, [1]], + expected=True, + msg="Should find string in mixed-type array", + ), + InTest( + id="mixed_find_null", + value=None, + array=[1, "2", True, None, [1]], + expected=True, + msg="Should find null in mixed-type array", + ), + InTest( + id="mixed_find_array", + value=[1], + array=[1, "2", True, None, [1]], + expected=True, + msg="Should find array in mixed-type array", + ), + InTest( + id="mixed_not_found", + value="x", + array=[1, "2", True, None, [1]], + expected=False, + msg="Should not find absent value in mixed-type array", + ), +] + +# --------------------------------------------------------------------------- +# Success: large array +# --------------------------------------------------------------------------- +_LARGE_ARRAY_SIZE = 20_000 +_LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) + +LARGE_ARRAY_TESTS: list[InTest] = [ + InTest( + id="large_array_found_first", + value=0, + array=_LARGE_ARRAY, + expected=True, + msg="Should find first element in large array", + ), + InTest( + id="large_array_found_last", + value=_LARGE_ARRAY_SIZE - 1, + array=_LARGE_ARRAY, + expected=True, + msg="Should find last element in large array", + ), + InTest( + id="large_array_found_middle", + value=_LARGE_ARRAY_SIZE // 2, + array=_LARGE_ARRAY, + expected=True, + msg="Should find middle element in large array", + ), + InTest( + id="large_array_not_found", + value=-1, + array=_LARGE_ARRAY, + expected=False, + msg="Should not find absent value in large array", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = FOUND_TESTS + NOT_FOUND_TESTS + MIXED_TYPE_TESTS + LARGE_ARRAY_TESTS + +TEST_SUBSET_FOR_LITERAL = [ + FOUND_TESTS[0], # found_int + NOT_FOUND_TESTS[0], # not_found_int + LARGE_ARRAY_TESTS[0], # large_array_found_first +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_in_literal(collection, test): + """Test $in with literal values.""" + result = execute_expression(collection, {"$in": [test.value, test.array]}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_in_insert(collection, test): + """Test $in with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$in": ["$val", "$arr"]}, {"val": test.value, "arr": test.array} + ) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py new file mode 100644 index 000000000..eec4c9082 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py @@ -0,0 +1,201 @@ +""" +Error tests for $in expression. + +Tests non-array second argument and wrong arity errors. +""" + +from datetime import datetime + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 + InTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + EXPRESSION_IN_NOT_ARRAY_ERROR, + EXPRESSION_TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import MISSING + +# --------------------------------------------------------------------------- +# Error: second argument not an array (runs both literal and insert) +# --------------------------------------------------------------------------- +NOT_ARRAY_ERROR_TESTS: list[InTest] = [ + InTest( + id="string_as_array", + value=1, + array="hello", + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject string as array arg", + ), + InTest( + id="int_as_array", + value=1, + array=42, + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject int as array arg", + ), + InTest( + id="double_as_array", + value=1, + array=3.14, + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject double as array arg", + ), + InTest( + id="bool_true_as_array", + value=1, + array=True, + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject bool true as array arg", + ), + InTest( + id="bool_false_as_array", + value=1, + array=False, + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject bool false as array arg", + ), + InTest( + id="object_as_array", + value=1, + array={"a": 1}, + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject object as array arg", + ), + InTest( + id="decimal128_as_array", + value=1, + array=Decimal128("1"), + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject decimal128 as array arg", + ), + InTest( + id="int64_as_array", + value=1, + array=Int64(1), + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject int64 as array arg", + ), + InTest( + id="binary_as_array", + value=1, + array=Binary(b"x", 0), + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject binary as array arg", + ), + InTest( + id="datetime_as_array", + value=1, + array=datetime(2024, 1, 1), + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject datetime as array arg", + ), + InTest( + id="objectid_as_array", + value=1, + array=ObjectId(), + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject objectid as array arg", + ), + InTest( + id="regex_as_array", + value=1, + array=Regex("x"), + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject regex as array arg", + ), + InTest( + id="maxkey_as_array", + value=1, + array=MaxKey(), + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject maxkey as array arg", + ), + InTest( + id="minkey_as_array", + value=1, + array=MinKey(), + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject minkey as array arg", + ), + InTest( + id="timestamp_as_array", + value=1, + array=Timestamp(0, 0), + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject timestamp as array arg", + ), + InTest( + id="null_as_array", + value=1, + array=None, + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject null as array arg", + ), +] + +# --------------------------------------------------------------------------- +# Error: missing as array (literal only, MISSING is a field ref) +# --------------------------------------------------------------------------- +LITERAL_ONLY_TESTS: list[InTest] = [ + InTest( + id="missing_as_array", + value=1, + array=MISSING, + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="Should reject missing as array arg", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +TEST_SUBSET_FOR_LITERAL = [ + NOT_ARRAY_ERROR_TESTS[0], # string_as_array + NOT_ARRAY_ERROR_TESTS[-1], # null_as_array +] + LITERAL_ONLY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_in_literal(collection, test): + """Test $in error cases with literal values.""" + result = execute_expression(collection, {"$in": [test.value, test.array]}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(NOT_ARRAY_ERROR_TESTS)) +def test_in_insert(collection, test): + """Test $in error cases with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$in": ["$val", "$arr"]}, {"val": test.value, "arr": test.array} + ) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +# --------------------------------------------------------------------------- +# Error: wrong arity +# --------------------------------------------------------------------------- +ARITY_ERROR_TESTS = [ + pytest.param({"$in": []}, id="zero_args"), + pytest.param({"$in": [[1, 2, 3]]}, id="one_arg"), + pytest.param({"$in": [1, [1, 2], 3]}, id="three_args"), +] + + +@pytest.mark.parametrize("expr", ARITY_ERROR_TESTS) +def test_in_arity_error(collection, expr): + """Test $in errors with wrong number of arguments.""" + result = execute_expression(collection, expr) + assert_expression_result(result, error_code=EXPRESSION_TYPE_MISMATCH_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py new file mode 100644 index 000000000..564847e6e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py @@ -0,0 +1,105 @@ +""" +Expression and field path tests for $in expression. + +Tests nested expressions, field path lookups, composite paths, +and non-existent field handling. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import EXPRESSION_IN_NOT_ARRAY_ERROR + + +# --------------------------------------------------------------------------- +# Nested expressions +# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "expression,expected", + [ + # Nested $in: result of inner $in used as search value in outer $in + ({"$in": [{"$in": [2, [1, 2, 3]]}, [True, False]]}, True), + ], + ids=["nested_in_in"], +) +def test_in_nested_expression(collection, expression, expected): + """Test $in composed with other expressions.""" + result = execute_expression(collection, expression) + assert_expression_result(result, expected=expected) + + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "document,value,array_ref,expected", + [ + ({"a": {"b": [10, 20, 30]}}, 20, "$a.b", True), + ({"a": {"b": [10, 20, 30]}}, 99, "$a.b", False), + ({"a": {"b": {"c": [5, 6, 7]}}}, 7, "$a.b.c", True), + ], + ids=["nested_field_found", "nested_field_not_found", "deeply_nested_field"], +) +def test_in_field_lookup(collection, document, value, array_ref, expected): + """Test $in with field path lookups from inserted documents.""" + result = execute_expression_with_insert(collection, {"$in": [value, array_ref]}, document) + assert_expression_result(result, expected=expected) + + +# --------------------------------------------------------------------------- +# Field path: path through array of objects +# --------------------------------------------------------------------------- +def test_in_path_through_array_of_objects(collection): + """Test $in where field path traverses array of objects.""" + result = execute_expression_with_insert( + collection, {"$in": [20, "$a.b"]}, {"a": [{"b": 10}, {"b": 20}]} + ) + assert_expression_result(result, expected=True) + + +# --------------------------------------------------------------------------- +# Non-existent field as array → error (missing resolves to non-array) +# --------------------------------------------------------------------------- +def test_in_nonexistent_array_field(collection): + """Test $in where array field does not exist (resolves to missing).""" + result = execute_expression_with_insert(collection, {"$in": [1, "$nonexistent"]}, {"other": 1}) + assert_expression_result(result, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR) + + +# --------------------------------------------------------------------------- +# Non-existent field as value (resolves to missing/null) +# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "document,expected", + [ + ({"arr": [1, None, 3]}, False), + ({"arr": [1, 2, 3]}, False), + ], + ids=["null_in_array", "null_not_in_array"], +) +def test_in_nonexistent_value_field(collection, document, expected): + """Test $in where value field does not exist (missing vs null).""" + result = execute_expression_with_insert(collection, {"$in": ["$nonexistent", "$arr"]}, document) + assert_expression_result(result, expected=expected) + + +def test_in_composite_array_as_array(collection): + """Test $in with composite array from $x.y as the array argument.""" + result = execute_expression_with_insert( + collection, {"$in": [20, "$x.y"]}, {"x": [{"y": 10}, {"y": 20}, {"y": 30}]} + ) + assert_expression_result(result, expected=True) + + +def test_in_composite_array_as_value(collection): + """Test $in with composite array from $x.y as the search value.""" + result = execute_expression_with_insert( + collection, + {"$in": ["$x.y", [[10, 20, 30], "other"]]}, + {"x": [{"y": 10}, {"y": 20}, {"y": 30}]}, + ) + assert_expression_result(result, expected=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py new file mode 100644 index 000000000..780de00c0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py @@ -0,0 +1,175 @@ +""" +Nested array search tests for $in expression. + +Tests searching for complex elements in nested mixed arrays and deeply nested structures. +""" + +from datetime import datetime + +import pytest +from bson import Binary, Decimal128, MaxKey, MinKey, ObjectId + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 + InTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: nested mixed arrays as search targets +# --------------------------------------------------------------------------- +NESTED_MIXED_ARRAY_TESTS: list[InTest] = [ + InTest( + id="nested_find_object_in_mixed", + value={"a": 1}, + array=[1, "two", {"a": 1}, [3, 4], True], + expected=True, + msg="Should find object in nested mixed array", + ), + InTest( + id="nested_find_array_in_mixed", + value=[3, 4], + array=[1, "two", {"a": 1}, [3, 4], True], + expected=True, + msg="Should find array in nested mixed array", + ), + InTest( + id="nested_find_deep_object", + value={"a": {"b": 3}}, + array=[[1, 2], {"a": {"b": 3}}, "x"], + expected=True, + msg="Should find deep object in array", + ), + InTest( + id="nested_find_array_with_mixed_types", + value=[None, "a", 2], + array=[1, [None, "a", 2], "b"], + expected=True, + msg="Should find mixed-type subarray", + ), + InTest( + id="nested_find_empty_object", + value={}, + array=[1, {}, [2], "a"], + expected=True, + msg="Should find empty object in array", + ), + InTest( + id="nested_find_empty_array", + value=[], + array=[1, {}, [], "a"], + expected=True, + msg="Should find empty array in array", + ), + InTest( + id="nested_find_subarray_binary_decimal128", + value=[Binary(b"\x01\x02", 0), Decimal128("3.14")], + array=[1, [Binary(b"\x01\x02", 0), Decimal128("3.14")], "x", [3]], + expected=True, + msg="Should find subarray with binary and decimal128", + ), + InTest( + id="nested_find_subarray_object_array", + value=[{"k": 1}, [2, 3]], + array=["a", [{"k": 1}, [2, 3]], None, 4], + expected=True, + msg="Should find subarray with object and array", + ), + InTest( + id="nested_find_subarray_datetime_objectid", + value=[datetime(2024, 1, 1), ObjectId("000000000000000000000001")], + array=[0, [datetime(2024, 1, 1), ObjectId("000000000000000000000001")], "end"], + expected=True, + msg="Should find subarray with datetime and objectid", + ), + InTest( + id="nested_find_subarray_minkey_maxkey", + value=[MinKey(), MaxKey()], + array=[[MinKey(), MaxKey()], 1, "a"], + expected=True, + msg="Should find subarray with minkey and maxkey", + ), +] + +# --------------------------------------------------------------------------- +# Success: deeply nested search targets (3-5 levels) +# --------------------------------------------------------------------------- +DEEPLY_NESTED_TESTS: list[InTest] = [ + InTest( + id="nested_3_levels", + value=[[2, 3], [4, 5]], + array=[1, [[2, 3], [4, 5]], "end"], + expected=True, + msg="Should find 3-level nested array", + ), + InTest( + id="nested_4_levels", + value=[[[1, 2], 3], 4], + array=["a", [[[1, 2], 3], 4], None], + expected=True, + msg="Should find 4-level nested array", + ), + InTest( + id="nested_deep_mixed_bson", + value=[[MinKey(), {"a": [Decimal128("1.5")]}], True], + array=[0, [[MinKey(), {"a": [Decimal128("1.5")]}], True], "x"], + expected=True, + msg="Should find deeply nested mixed BSON", + ), + InTest( + id="nested_inner_not_outer", + value=[2, 3], + array=[[1, [2, 3]], [2, 3], 4], + expected=True, + msg="Should find inner array match", + ), + InTest( + id="nested_5_levels", + value=[[[[99]]]], + array=[[[[[99]]]], "other"], + expected=True, + msg="Should find 5-level nested array", + ), + InTest( + id="nested_deep_not_found", + value=[2, 3], + array=[[1, [2, 3]], [4, 5]], + expected=False, + msg="Should not find array at wrong nesting level", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = NESTED_MIXED_ARRAY_TESTS + DEEPLY_NESTED_TESTS + +TEST_SUBSET_FOR_LITERAL = [ + NESTED_MIXED_ARRAY_TESTS[0], # nested_find_object_in_mixed + DEEPLY_NESTED_TESTS[0], # nested_3_levels + DEEPLY_NESTED_TESTS[-1], # nested_deep_not_found +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_in_literal(collection, test): + """Test $in nested arrays with literal values.""" + result = execute_expression(collection, {"$in": [test.value, test.array]}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_in_insert(collection, test): + """Test $in nested arrays with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$in": ["$val", "$arr"]}, {"val": test.value, "arr": test.array} + ) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py new file mode 100644 index 000000000..54947ce7e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py @@ -0,0 +1,84 @@ +""" +Null and missing field handling tests for $in expression. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 + InTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import MISSING + +# --------------------------------------------------------------------------- +# Success: null/missing handling (runs both literal and insert) +# --------------------------------------------------------------------------- +NULL_TESTS: list[InTest] = [ + InTest( + id="null_value_in_array", + value=None, + array=[1, None, 3], + expected=True, + msg="Should find null value in array containing null", + ), + InTest( + id="null_value_not_in_array", + value=None, + array=[1, 2, 3], + expected=False, + msg="Should not find null in array without null", + ), +] + +# --------------------------------------------------------------------------- +# Success: missing value handling (literal only, MISSING is a field ref) +# --------------------------------------------------------------------------- +LITERAL_ONLY_TESTS: list[InTest] = [ + InTest( + id="missing_value", + value=MISSING, + array=[1, 2, 3], + expected=False, + msg="Should not find missing value in array", + ), + InTest( + id="missing_value_null_in_array", + value=MISSING, + array=[1, None, 3], + expected=False, + msg="Should not find missing value even with null in array", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +TEST_SUBSET_FOR_LITERAL = [ + NULL_TESTS[0], # null_value_in_array + NULL_TESTS[1], # null_value_not_in_array +] + LITERAL_ONLY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_in_literal(collection, test): + """Test $in null/missing with literal values.""" + result = execute_expression(collection, {"$in": [test.value, test.array]}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(NULL_TESTS)) +def test_in_insert(collection, test): + """Test $in null with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$in": ["$val", "$arr"]}, {"val": test.value, "arr": test.array} + ) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_expression_in.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_expression_in.py index 46e97f208..0220f4c01 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_expression_in.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_expression_in.py @@ -28,4 +28,4 @@ def test_smoke_expression_in(collection): ) expected = [{"_id": 1, "found": True}, {"_id": 2, "found": False}] - assertSuccess(result, expected, msg="Should support $in expression") + assertSuccess(result, expected, "Should support $in expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_bson_types.py new file mode 100644 index 000000000..73d69bb0b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_bson_types.py @@ -0,0 +1,420 @@ +""" +BSON type search and nested array tests for $indexOfArray expression. + +Tests searching for various BSON types, numeric equivalence, and +complex nested mixed arrays. +""" + +from datetime import datetime +from uuid import UUID + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.indexOfArray.utils.indexOfArray_common import ( # noqa: E501 + IndexOfArrayTest, + build_args, + build_insert_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +# --------------------------------------------------------------------------- +# Success: search for various BSON types +# --------------------------------------------------------------------------- +BSON_TYPE_SEARCH_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="search_int64", + array=[Int64(99), 1], + search=Int64(99), + expected=0, + msg="Should find Int64 value", + ), + IndexOfArrayTest( + id="search_decimal128", + array=[Decimal128("1.5"), 2], + search=Decimal128("1.5"), + expected=0, + msg="Should find Decimal128 value", + ), + IndexOfArrayTest( + id="search_datetime", + array=[datetime(2024, 1, 1), 1], + search=datetime(2024, 1, 1), + expected=0, + msg="Should find datetime value", + ), + IndexOfArrayTest( + id="search_objectid", + array=[ObjectId("000000000000000000000001"), 1], + search=ObjectId("000000000000000000000001"), + expected=0, + msg="Should find ObjectId value", + ), + IndexOfArrayTest( + id="search_binary", + array=[Binary(b"\x01\x02", 0), 1], + search=Binary(b"\x01\x02", 0), + expected=0, + msg="Should find Binary value", + ), + IndexOfArrayTest( + id="search_regex", + array=[Regex("^abc", "i"), 1], + search=Regex("^abc", "i"), + expected=0, + msg="Should find Regex value", + ), + IndexOfArrayTest( + id="search_timestamp", + array=[Timestamp(1, 1), 1], + search=Timestamp(1, 1), + expected=0, + msg="Should find Timestamp value", + ), + IndexOfArrayTest( + id="search_minkey", + array=[MinKey(), 1], + search=MinKey(), + expected=0, + msg="Should find MinKey value", + ), + IndexOfArrayTest( + id="search_maxkey", + array=[1, MaxKey()], + search=MaxKey(), + expected=1, + msg="Should find MaxKey value", + ), + IndexOfArrayTest( + id="search_uuid", + array=[Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), 1], + search=Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), + expected=0, + msg="Should find UUID binary value", + ), + # Special float values + IndexOfArrayTest( + id="search_infinity", + array=[FLOAT_INFINITY, 1], + search=FLOAT_INFINITY, + expected=0, + msg="Should find Infinity", + ), + IndexOfArrayTest( + id="search_neg_infinity", + array=[FLOAT_NEGATIVE_INFINITY, 1], + search=FLOAT_NEGATIVE_INFINITY, + expected=0, + msg="Should find -Infinity", + ), + IndexOfArrayTest( + id="search_infinity_not_found", + array=[1, 2, 3], + search=FLOAT_INFINITY, + expected=-1, + msg="Should not find Infinity in regular array", + ), + # Special Decimal128 values + IndexOfArrayTest( + id="search_decimal128_infinity", + array=[DECIMAL128_INFINITY, 1], + search=DECIMAL128_INFINITY, + expected=0, + msg="Should find Decimal128 Infinity", + ), + IndexOfArrayTest( + id="search_decimal128_neg_infinity", + array=[DECIMAL128_NEGATIVE_INFINITY, 1], + search=DECIMAL128_NEGATIVE_INFINITY, + expected=0, + msg="Should find Decimal128 -Infinity", + ), + # NaN equality: NaN == NaN in BSON comparison (unlike IEEE 754) + IndexOfArrayTest( + id="search_nan_found", + array=[FLOAT_NAN, 1], + search=FLOAT_NAN, + expected=0, + msg="Should find NaN (BSON equality)", + ), + IndexOfArrayTest( + id="search_decimal128_nan_found", + array=[DECIMAL128_NAN, 1], + search=DECIMAL128_NAN, + expected=0, + msg="Should find Decimal128 NaN (BSON equality)", + ), + # Cross-type NaN matching + IndexOfArrayTest( + id="search_decimal128_nan_matches_float_nan", + array=[FLOAT_NAN, 1], + search=DECIMAL128_NAN, + expected=0, + msg="Decimal128 NaN should match float NaN cross-type", + ), + IndexOfArrayTest( + id="search_decimal128_neg_nan_matches_nan", + array=[DECIMAL128_NAN, 1], + search=Decimal128("-NaN"), + expected=0, + msg="Decimal128 -NaN should match Decimal128 NaN", + ), +] + +# --------------------------------------------------------------------------- +# Success: numeric type equivalence in search +# --------------------------------------------------------------------------- +NUMERIC_EQUIVALENCE_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="int_matches_double", + array=[1.0, 2.0], + search=1, + expected=0, + msg="Should match int to double", + ), + IndexOfArrayTest( + id="int_matches_long", + array=[Int64(1), 2], + search=1, + expected=0, + msg="Should match int to long", + ), + IndexOfArrayTest( + id="int_matches_decimal128", + array=[Decimal128("1"), 2], + search=1, + expected=0, + msg="Should match int to decimal128", + ), + IndexOfArrayTest( + id="double_matches_int", + array=[1, 2], + search=1.0, + expected=0, + msg="Should match double to int", + ), + IndexOfArrayTest( + id="long_matches_int", + array=[1, 2], + search=Int64(1), + expected=0, + msg="Should match long to int", + ), + IndexOfArrayTest( + id="decimal128_matches_int", + array=[1, 2], + search=Decimal128("1"), + expected=0, + msg="Should match decimal128 to int", + ), +] + +# --------------------------------------------------------------------------- +# Success: nested mixed arrays +# --------------------------------------------------------------------------- +NESTED_MIXED_ARRAY_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="nested_find_object_in_mixed", + array=[1, "two", {"a": 1}, [3, 4], True], + search={"a": 1}, + expected=2, + msg="Should find object in mixed array", + ), + IndexOfArrayTest( + id="nested_find_array_in_mixed", + array=[1, "two", {"a": 1}, [3, 4], True], + search=[3, 4], + expected=3, + msg="Should find array in mixed array", + ), + IndexOfArrayTest( + id="nested_find_bool_in_mixed", + array=[1, "two", {"a": 1}, [3, 4], True], + search=True, + expected=4, + msg="Should find bool in mixed array", + ), + IndexOfArrayTest( + id="nested_find_null_in_mixed", + array=[1, None, "three", [4], {"b": 2}], + search=None, + expected=1, + msg="Should find null in mixed array", + ), + IndexOfArrayTest( + id="nested_find_deep_object", + array=[[1, 2], {"a": {"b": 3}}, "x"], + search={"a": {"b": 3}}, + expected=1, + msg="Should find deep object", + ), + IndexOfArrayTest( + id="nested_find_array_with_mixed_types", + array=[1, [None, "a", 2], "b"], + search=[None, "a", 2], + expected=1, + msg="Should find mixed-type subarray", + ), + IndexOfArrayTest( + id="nested_find_empty_object_in_mixed", + array=[1, {}, [2], "a"], + search={}, + expected=1, + msg="Should find empty object", + ), + IndexOfArrayTest( + id="nested_find_empty_array_in_mixed", + array=[1, {}, [], "a"], + search=[], + expected=2, + msg="Should find empty array", + ), + IndexOfArrayTest( + id="nested_find_datetime_in_mixed", + array=["a", datetime(2024, 1, 1), 3, [4]], + search=datetime(2024, 1, 1), + expected=1, + msg="Should find datetime in mixed array", + ), + IndexOfArrayTest( + id="nested_find_objectid_in_mixed", + array=[1, ObjectId("000000000000000000000001"), "x", [2]], + search=ObjectId("000000000000000000000001"), + expected=1, + msg="Should find objectid in mixed array", + ), + IndexOfArrayTest( + id="nested_find_decimal128_in_mixed", + array=["a", [1], Decimal128("3.14"), None], + search=Decimal128("3.14"), + expected=2, + msg="Should find decimal128 in mixed array", + ), + IndexOfArrayTest( + id="nested_find_binary_in_mixed", + array=[1, Binary(b"\x01\x02", 0), "x", [3]], + search=Binary(b"\x01\x02", 0), + expected=1, + msg="Should find binary in mixed array", + ), + IndexOfArrayTest( + id="nested_find_subarray_binary_decimal128", + array=[1, [Binary(b"\x01\x02", 0), Decimal128("3.14")], "x", [3]], + search=[Binary(b"\x01\x02", 0), Decimal128("3.14")], + expected=1, + msg="Should find subarray with binary and decimal128", + ), + IndexOfArrayTest( + id="nested_find_subarray_object_array", + array=["a", [{"k": 1}, [2, 3]], None, 4], + search=[{"k": 1}, [2, 3]], + expected=1, + msg="Should find subarray with object and array", + ), + IndexOfArrayTest( + id="nested_find_subarray_null_bool_int", + array=[[None, True, 42], "x", 1], + search=[None, True, 42], + expected=0, + msg="Should find subarray with null bool int", + ), + IndexOfArrayTest( + id="nested_find_subarray_datetime_objectid", + array=[0, [datetime(2024, 1, 1), ObjectId("000000000000000000000001")], "end"], + search=[datetime(2024, 1, 1), ObjectId("000000000000000000000001")], + expected=1, + msg="Should find subarray with datetime and objectid", + ), + IndexOfArrayTest( + id="nested_find_subarray_minkey_maxkey", + array=[[MinKey(), MaxKey()], 1, "a"], + search=[MinKey(), MaxKey()], + expected=0, + msg="Should find subarray with minkey and maxkey", + ), + IndexOfArrayTest( + id="nested_3_levels_deep", + array=[1, [[2, 3], [4, 5]], "end"], + search=[[2, 3], [4, 5]], + expected=1, + msg="Should find 3-level nested array", + ), + IndexOfArrayTest( + id="nested_4_levels_deep", + array=["a", [[[1, 2], 3], 4], None], + search=[[[1, 2], 3], 4], + expected=1, + msg="Should find 4-level nested array", + ), + IndexOfArrayTest( + id="nested_deep_mixed_bson", + array=[0, [[MinKey(), {"a": [Decimal128("1.5")]}], True], "x"], + search=[[MinKey(), {"a": [Decimal128("1.5")]}], True], + expected=1, + msg="Should find deeply nested mixed BSON", + ), + IndexOfArrayTest( + id="nested_inner_not_outer", + array=[[1, [2, 3]], [2, 3], 4], + search=[2, 3], + expected=1, + msg="Should find inner array match", + ), + IndexOfArrayTest( + id="nested_5_levels_deep", + array=[[[[[99]]]], "other"], + search=[[[[99]]]], + expected=0, + msg="Should find 5-level nested array", + ), + IndexOfArrayTest( + id="nested_deep_not_found", + array=[[1, [2, 3]], [4, 5]], + search=[2, 3], + expected=-1, + msg="Should not find array at wrong nesting level", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = BSON_TYPE_SEARCH_TESTS + NUMERIC_EQUIVALENCE_TESTS + NESTED_MIXED_ARRAY_TESTS + +TEST_SUBSET_FOR_LITERAL = [ + BSON_TYPE_SEARCH_TESTS[0], # search_int64 + NUMERIC_EQUIVALENCE_TESTS[0], # int_matches_double + NESTED_MIXED_ARRAY_TESTS[0], # nested_find_object_in_mixed +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_indexOfArray_literal(collection, test): + """Test $indexOfArray BSON types with literal values.""" + result = execute_expression(collection, {"$indexOfArray": build_args(test)}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_indexOfArray_insert(collection, test): + """Test $indexOfArray BSON types with values from inserted documents.""" + args, doc = build_insert_args(test) + result = execute_expression_with_insert(collection, {"$indexOfArray": args}, doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py new file mode 100644 index 000000000..b1756a27c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py @@ -0,0 +1,606 @@ +""" +Core behavior tests for $indexOfArray expression. + +Tests basic search, not found, start/end index ranges, degenerate cases, +mixed types, and large arrays. +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.indexOfArray.utils.indexOfArray_common import ( # noqa: E501 + IndexOfArrayTest, + build_args, + build_insert_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: basic search — value found +# --------------------------------------------------------------------------- +BASIC_FOUND_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="found_first", + array=[1, 2, 3], + search=1, + expected=0, + msg="Should find first element at index 0", + ), + IndexOfArrayTest( + id="found_middle", + array=[1, 2, 3], + search=2, + expected=1, + msg="Should find middle element at index 1", + ), + IndexOfArrayTest( + id="found_last", + array=[1, 2, 3], + search=3, + expected=2, + msg="Should find last element at index 2", + ), + IndexOfArrayTest( + id="found_string", + array=["a", "b", "c"], + search="b", + expected=1, + msg="Should find string in array", + ), + IndexOfArrayTest( + id="found_bool_true", + array=[True, False], + search=True, + expected=0, + msg="Should find true at index 0", + ), + IndexOfArrayTest( + id="found_bool_false", + array=[True, False], + search=False, + expected=1, + msg="Should find false at index 1", + ), + IndexOfArrayTest( + id="found_null", + array=[None, 1, 2], + search=None, + expected=0, + msg="Should find null at index 0", + ), + IndexOfArrayTest( + id="found_nested_array", + array=[[1, 2], [3, 4]], + search=[3, 4], + expected=1, + msg="Should find nested array", + ), + IndexOfArrayTest( + id="found_object", + array=[{"a": 1}, {"b": 2}], + search={"a": 1}, + expected=0, + msg="Should find object in array", + ), + IndexOfArrayTest( + id="found_single_element", + array=[42], + search=42, + expected=0, + msg="Should find value in single-element array", + ), + IndexOfArrayTest( + id="first_occurrence", + array=[1, 2, 1, 2], + search=1, + expected=0, + msg="Should return first occurrence index", + ), + IndexOfArrayTest( + id="duplicate_values", + array=[5, 5, 5], + search=5, + expected=0, + msg="Should return first index for duplicates", + ), +] + +# --------------------------------------------------------------------------- +# Success: value not found → -1 +# --------------------------------------------------------------------------- +NOT_FOUND_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="not_found_int", + array=[1, 2, 3], + search=4, + expected=-1, + msg="Should return -1 for absent int", + ), + IndexOfArrayTest( + id="not_found_string", + array=["a", "b"], + search="z", + expected=-1, + msg="Should return -1 for absent string", + ), + IndexOfArrayTest( + id="empty_array", array=[], search=1, expected=-1, msg="Should return -1 for empty array" + ), + IndexOfArrayTest( + id="type_mismatch_search", + array=[1, 2, 3], + search="1", + expected=-1, + msg="Should return -1 for type mismatch", + ), + IndexOfArrayTest( + id="bool_vs_int", + array=[1, 0], + search=True, + expected=-1, + msg="Should return -1 for bool vs int", + ), + IndexOfArrayTest( + id="not_found_null", + array=[1, 2, 3], + search=None, + expected=-1, + msg="Should return -1 for null not in array", + ), + IndexOfArrayTest( + id="not_found_partial_array", + array=[[1, 2], [3, 4]], + search=[1], + expected=-1, + msg="Should return -1 for partial array match", + ), + IndexOfArrayTest( + id="not_found_partial_object", + array=[{"a": 1, "b": 2}], + search={"a": 1}, + expected=-1, + msg="Should return -1 for partial object match", + ), +] + +# --------------------------------------------------------------------------- +# Success: with start index +# --------------------------------------------------------------------------- +START_INDEX_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="start_skips_first", + array=[1, 2, 1, 2], + search=1, + start=1, + expected=2, + msg="Should skip first match with start index", + ), + IndexOfArrayTest( + id="start_at_match", + array=[10, 20, 30], + search=20, + start=1, + expected=1, + msg="Should find at start index", + ), + IndexOfArrayTest( + id="start_past_match", + array=[10, 20, 30], + search=10, + start=1, + expected=-1, + msg="Should return -1 when start is past match", + ), + IndexOfArrayTest( + id="start_at_zero", + array=[1, 2, 3], + search=1, + start=0, + expected=0, + msg="Should find with start at zero", + ), + IndexOfArrayTest( + id="start_beyond_array", + array=[1, 2, 3], + search=1, + start=20, + expected=-1, + msg="Should return -1 when start beyond array", + ), + IndexOfArrayTest( + id="start_int64", + array=[10, 20, 30], + search=20, + start=Int64(1), + expected=1, + msg="Should accept Int64 start index", + ), + IndexOfArrayTest( + id="start_double_integral", + array=[10, 20, 30], + search=30, + start=2.0, + expected=2, + msg="Should accept integral double start index", + ), + IndexOfArrayTest( + id="start_decimal128_integral", + array=[10, 20, 30], + search=20, + start=Decimal128("1"), + expected=1, + msg="Should accept Decimal128 start index", + ), + IndexOfArrayTest( + id="start_decimal128_10E_neg1", + array=[10, 20, 30], + search=20, + start=Decimal128("10E-1"), + expected=1, + msg="Should accept Decimal128 10E-1 as start index 1", + ), + IndexOfArrayTest( + id="end_decimal128_30E_neg1", + array=[10, 20, 30], + search=20, + start=0, + end=Decimal128("30E-1"), + expected=1, + msg="Should accept Decimal128 30E-1 as end index 3", + ), +] + +# --------------------------------------------------------------------------- +# Success: with start and end index +# --------------------------------------------------------------------------- +START_END_INDEX_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="range_found", + array=["a", "b", "c", "b"], + search="b", + start=1, + end=3, + expected=1, + msg="Should find in range", + ), + IndexOfArrayTest( + id="range_not_found_exclusive_end", + array=["a", "b", "c"], + search="c", + start=0, + end=2, + expected=-1, + msg="Should not find at exclusive end", + ), + IndexOfArrayTest( + id="range_end_equals_start", + array=[1, 2, 3], + search=1, + start=0, + end=0, + expected=-1, + msg="Should return -1 when end equals start", + ), + IndexOfArrayTest( + id="range_end_less_than_start", + array=["a", "b", "c"], + search="b", + start=2, + end=1, + expected=-1, + msg="Should return -1 when end less than start", + ), + IndexOfArrayTest( + id="range_end_beyond_array", + array=[1, 2, 3], + search=3, + start=0, + end=100, + expected=2, + msg="Should find when end beyond array", + ), + IndexOfArrayTest( + id="range_full_array", + array=[1, 2, 3], + search=2, + start=0, + end=3, + expected=1, + msg="Should find in full array range", + ), + IndexOfArrayTest( + id="range_int64_bounds", + array=[10, 20, 30], + search=20, + start=Int64(0), + end=Int64(3), + expected=1, + msg="Should accept Int64 bounds", + ), + IndexOfArrayTest( + id="range_double_integral_bounds", + array=[10, 20, 30], + search=20, + start=0.0, + end=3.0, + expected=1, + msg="Should accept integral double bounds", + ), + IndexOfArrayTest( + id="range_decimal128_bounds", + array=[10, 20, 30], + search=20, + start=Decimal128("0"), + end=Decimal128("3"), + expected=1, + msg="Should accept Decimal128 bounds", + ), +] + +# --------------------------------------------------------------------------- +# Success: first occurrence from start with multiple duplicates +# --------------------------------------------------------------------------- +FIRST_FROM_START_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="dup_skip_to_third_occurrence", + array=[5, 3, 5, 3, 5], + search=5, + start=3, + expected=4, + msg="Should find third occurrence of 5 when start=3", + ), + IndexOfArrayTest( + id="dup_skip_different_value", + array=[5, 3, 5, 3, 5], + search=3, + start=2, + expected=3, + msg="Should find second 3 when start=2", + ), +] + +# --------------------------------------------------------------------------- +# Success: detailed range semantics +# --------------------------------------------------------------------------- +RANGE_SEMANTICS_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="end_exclusive_includes_before_boundary", + array=["a", "b", "c"], + search="b", + start=0, + end=2, + expected=1, + msg="Element before end boundary should be included in [0,2)", + ), + IndexOfArrayTest( + id="end_exclusive_excludes_at_boundary", + array=["a", "b", "c"], + search="b", + start=0, + end=1, + expected=-1, + msg="Element at end boundary should be excluded from [0,1)", + ), + IndexOfArrayTest( + id="empty_range_at_array_length", + array=[1, 2, 3], + search=3, + start=3, + end=3, + expected=-1, + msg="Empty range [len,len) at array boundary should return -1", + ), + IndexOfArrayTest( + id="range_finds_dup_in_subrange", + array=[1, 2, 3, 2, 1, 2, 3], + search=2, + start=2, + end=4, + expected=3, + msg="Should find 2 at index 3 within range [2,4) skipping earlier occurrences", + ), + IndexOfArrayTest( + id="start_at_array_length", + array=["a", "b", "c"], + search="a", + start=3, + expected=-1, + msg="Should return -1 when start equals array length", + ), + IndexOfArrayTest( + id="start_and_end_both_beyond_array", + array=["a", "b", "c"], + search="a", + start=100, + end=200, + expected=-1, + msg="Should return -1 when both start and end are beyond array", + ), + IndexOfArrayTest( + id="end_before_match_position", + array=["a", "abc", "b"], + search="b", + start=0, + end=1, + expected=-1, + msg="Should return -1 when end is before the matching element", + ), +] + +# --------------------------------------------------------------------------- +# Success: degenerate and single-element edge cases +# --------------------------------------------------------------------------- +DEGENERATE_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="single_not_found", + array=[1], + search=2, + expected=-1, + msg="Should return -1 when single element doesn't match", + ), + IndexOfArrayTest( + id="single_found_in_range", + array=[42], + search=42, + start=0, + end=1, + expected=0, + msg="Single element found in range [0,1)", + ), + IndexOfArrayTest( + id="single_empty_range", + array=[42], + search=42, + start=0, + end=0, + expected=-1, + msg="Single element not found in empty range [0,0)", + ), + IndexOfArrayTest( + id="single_start_past_element", + array=[42], + search=42, + start=1, + expected=-1, + msg="Single element not found when start past it", + ), + IndexOfArrayTest( + id="all_null_from_start", + array=[None, None, None], + search=None, + start=1, + expected=1, + msg="Should find null at index 1 from start=1 in all-null array", + ), + IndexOfArrayTest( + id="all_null_search_different_type", + array=[None, None, None], + search=1, + expected=-1, + msg="Should not find int in all-null array", + ), + IndexOfArrayTest( + id="all_true_search_false", + array=[True, True, True], + search=False, + expected=-1, + msg="Should not find false in all-true array", + ), +] + +# --------------------------------------------------------------------------- +# Success: mixed types in array +# --------------------------------------------------------------------------- +MIXED_TYPE_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="mixed_find_string", + array=[1, "2", True, None, [1]], + search="2", + expected=1, + msg="Should find string in mixed array", + ), + IndexOfArrayTest( + id="mixed_find_null", + array=[1, "2", True, None, [1]], + search=None, + expected=3, + msg="Should find null in mixed array", + ), + IndexOfArrayTest( + id="mixed_find_array", + array=[1, "2", True, None, [1]], + search=[1], + expected=4, + msg="Should find array in mixed array", + ), +] + +# --------------------------------------------------------------------------- +# Success: large array +# --------------------------------------------------------------------------- +_LARGE_ARRAY_SIZE = 20_000 +_LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) + +LARGE_ARRAY_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="large_array_first", + array=_LARGE_ARRAY, + search=0, + expected=0, + msg="Should find first in large array", + ), + IndexOfArrayTest( + id="large_array_last", + array=_LARGE_ARRAY, + search=_LARGE_ARRAY_SIZE - 1, + expected=_LARGE_ARRAY_SIZE - 1, + msg="Should find last in large array", + ), + IndexOfArrayTest( + id="large_array_middle", + array=_LARGE_ARRAY, + search=_LARGE_ARRAY_SIZE // 2, + expected=_LARGE_ARRAY_SIZE // 2, + msg="Should find middle in large array", + ), + IndexOfArrayTest( + id="large_array_not_found", + array=_LARGE_ARRAY, + search=-1, + expected=-1, + msg="Should return -1 for absent value in large array", + ), + IndexOfArrayTest( + id="large_array_with_start", + array=_LARGE_ARRAY, + search=_LARGE_ARRAY_SIZE - 1, + start=_LARGE_ARRAY_SIZE - 2, + expected=_LARGE_ARRAY_SIZE - 1, + msg="Should find with start in large array", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + BASIC_FOUND_TESTS + + NOT_FOUND_TESTS + + START_INDEX_TESTS + + START_END_INDEX_TESTS + + FIRST_FROM_START_TESTS + + RANGE_SEMANTICS_TESTS + + DEGENERATE_TESTS + + MIXED_TYPE_TESTS + + LARGE_ARRAY_TESTS +) + +TEST_SUBSET_FOR_LITERAL = [ + BASIC_FOUND_TESTS[0], # found_first + NOT_FOUND_TESTS[0], # not_found_int + START_END_INDEX_TESTS[0], # range_found +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_indexOfArray_literal(collection, test): + """Test $indexOfArray with literal values.""" + result = execute_expression(collection, {"$indexOfArray": build_args(test)}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_indexOfArray_insert(collection, test): + """Test $indexOfArray with values from inserted documents.""" + args, doc = build_insert_args(test) + result = execute_expression_with_insert(collection, {"$indexOfArray": args}, doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py new file mode 100644 index 000000000..36f5e2d1b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py @@ -0,0 +1,630 @@ +""" +Error tests for $indexOfArray expression. + +Tests non-array first argument, non-integral start/end, negative start/end, +boundary values, negative zero, and wrong arity errors. +""" + +from datetime import datetime + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.indexOfArray.utils.indexOfArray_common import ( # noqa: E501 + IndexOfArrayTest, + build_args, + build_insert_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + EXPRESSION_ARITY_ERROR, + INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + INDEX_OF_ARRAY_NOT_ARRAY_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_NEGATIVE_ZERO, + INT32_MAX, + INT64_MAX, + MISSING, +) + +# --------------------------------------------------------------------------- +# Success: boundary values for start/end indices +# --------------------------------------------------------------------------- +BOUNDARY_INDEX_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="start_int32_max", + array=[1, 2, 3], + search=1, + start=INT32_MAX, + expected=-1, + msg="Should return -1 with INT32_MAX start", + ), + IndexOfArrayTest( + id="start_int64_max", + array=[1, 2, 3], + search=1, + start=INT64_MAX, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject INT64_MAX start", + ), + IndexOfArrayTest( + id="end_int32_max", + array=[1, 2, 3], + search=2, + start=0, + end=INT32_MAX, + expected=1, + msg="Should find with INT32_MAX end", + ), + IndexOfArrayTest( + id="end_int64_max", + array=[1, 2, 3], + search=2, + start=0, + end=INT64_MAX, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject INT64_MAX end", + ), + IndexOfArrayTest( + id="start_and_end_int32_max", + array=[1, 2, 3], + search=1, + start=INT32_MAX, + end=INT32_MAX, + expected=-1, + msg="Should return -1 with both INT32_MAX", + ), + IndexOfArrayTest( + id="start_int32_max_minus_1", + array=[1, 2, 3], + search=1, + start=INT32_MAX - 1, + expected=-1, + msg="Should return -1 with INT32_MAX-1 start", + ), +] + +# --------------------------------------------------------------------------- +# Success: negative zero as start index +# --------------------------------------------------------------------------- +NEGATIVE_ZERO_START_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="double_neg_zero_start", + array=[10, 20, 30], + search=10, + start=-0.0, + expected=0, + msg="Should treat -0.0 start as 0", + ), + IndexOfArrayTest( + id="decimal128_neg_zero_start", + array=[10, 20, 30], + search=10, + start=DECIMAL128_NEGATIVE_ZERO, + expected=0, + msg="Should treat decimal128 -0 start as 0", + ), + IndexOfArrayTest( + id="double_neg_zero_end", + array=[10, 20, 30], + search=10, + start=0, + end=-0.0, + expected=-1, + msg="Should treat -0.0 end as 0", + ), +] + +# --------------------------------------------------------------------------- +# Error: first argument not an array (and not null) +# --------------------------------------------------------------------------- +NOT_ARRAY_ERROR_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="string_as_array", + array="hello", + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject string as array", + ), + IndexOfArrayTest( + id="int_as_array", + array=42, + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject int as array", + ), + IndexOfArrayTest( + id="double_as_array", + array=3.14, + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject double as array", + ), + IndexOfArrayTest( + id="bool_true_as_array", + array=True, + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject bool true as array", + ), + IndexOfArrayTest( + id="bool_false_as_array", + array=False, + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject bool false as array", + ), + IndexOfArrayTest( + id="object_as_array", + array={"a": 1}, + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject object as array", + ), + IndexOfArrayTest( + id="decimal128_as_array", + array=Decimal128("1"), + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject decimal128 as array", + ), + IndexOfArrayTest( + id="int64_as_array", + array=Int64(1), + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject int64 as array", + ), + IndexOfArrayTest( + id="binary_as_array", + array=Binary(b"x", 0), + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject binary as array", + ), + IndexOfArrayTest( + id="datetime_as_array", + array=datetime(2024, 1, 1), + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject datetime as array", + ), + IndexOfArrayTest( + id="objectid_as_array", + array=ObjectId(), + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject objectid as array", + ), + IndexOfArrayTest( + id="regex_as_array", + array=Regex("x"), + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject regex as array", + ), + IndexOfArrayTest( + id="maxkey_as_array", + array=MaxKey(), + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject maxkey as array", + ), + IndexOfArrayTest( + id="minkey_as_array", + array=MinKey(), + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject minkey as array", + ), + IndexOfArrayTest( + id="timestamp_as_array", + array=Timestamp(0, 0), + search=1, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="Should reject timestamp as array", + ), +] + +# --------------------------------------------------------------------------- +# Error: start index not integral +# --------------------------------------------------------------------------- +START_NOT_INTEGRAL_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="start_fractional_double", + array=[1, 2, 3], + search=1, + start=1.5, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject fractional double start", + ), + IndexOfArrayTest( + id="start_fractional_decimal128", + array=[1, 2, 3], + search=1, + start=Decimal128("0.5"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject fractional decimal128 start", + ), + IndexOfArrayTest( + id="start_nan", + array=[1, 2, 3], + search=1, + start=float("nan"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject NaN start", + ), + IndexOfArrayTest( + id="start_inf", + array=[1, 2, 3], + search=1, + start=float("inf"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject infinity start", + ), + IndexOfArrayTest( + id="start_neg_inf", + array=[1, 2, 3], + search=1, + start=float("-inf"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject -infinity start", + ), + IndexOfArrayTest( + id="start_decimal128_nan", + array=[1, 2, 3], + search=1, + start=Decimal128("NaN"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject decimal128 NaN start", + ), + IndexOfArrayTest( + id="start_decimal128_inf", + array=[1, 2, 3], + search=1, + start=Decimal128("Infinity"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject decimal128 infinity start", + ), + IndexOfArrayTest( + id="start_string", + array=[1, 2, 3], + search=1, + start="0", + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject string start", + ), + IndexOfArrayTest( + id="start_bool", + array=[1, 2, 3], + search=1, + start=True, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject bool start", + ), + IndexOfArrayTest( + id="start_array", + array=[1, 2, 3], + search=1, + start=[0], + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject array start", + ), + IndexOfArrayTest( + id="start_object", + array=[1, 2, 3], + search=1, + start={"a": 0}, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject object start", + ), +] + +# --------------------------------------------------------------------------- +# Error: end index not integral +# --------------------------------------------------------------------------- +END_NOT_INTEGRAL_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="end_fractional_double", + array=[1, 2, 3], + search=1, + start=0, + end=1.5, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject fractional double end", + ), + IndexOfArrayTest( + id="end_fractional_decimal128", + array=[1, 2, 3], + search=1, + start=0, + end=Decimal128("0.5"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject fractional decimal128 end", + ), + IndexOfArrayTest( + id="end_nan", + array=[1, 2, 3], + search=1, + start=0, + end=float("nan"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject NaN end", + ), + IndexOfArrayTest( + id="end_inf", + array=[1, 2, 3], + search=1, + start=0, + end=float("inf"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject infinity end", + ), + IndexOfArrayTest( + id="end_string", + array=[1, 2, 3], + search=1, + start=0, + end="3", + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject string end", + ), + IndexOfArrayTest( + id="end_bool", + array=[1, 2, 3], + search=1, + start=0, + end=True, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject bool end", + ), + IndexOfArrayTest( + id="end_neg_inf", + array=[1, 2, 3], + search=1, + start=0, + end=float("-inf"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject -infinity end", + ), + IndexOfArrayTest( + id="end_decimal128_nan", + array=[1, 2, 3], + search=1, + start=0, + end=Decimal128("NaN"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject decimal128 NaN end", + ), + IndexOfArrayTest( + id="end_decimal128_inf", + array=[1, 2, 3], + search=1, + start=0, + end=Decimal128("Infinity"), + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject decimal128 infinity end", + ), + IndexOfArrayTest( + id="end_array", + array=[1, 2, 3], + search=1, + start=0, + end=[3], + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject array end", + ), + IndexOfArrayTest( + id="end_object", + array=[1, 2, 3], + search=1, + start=0, + end={"a": 0}, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject object end", + ), +] + +# --------------------------------------------------------------------------- +# Error: negative start index +# --------------------------------------------------------------------------- +START_NEGATIVE_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="start_neg_one", + array=[1, 2, 3], + search=1, + start=-1, + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative start -1", + ), + IndexOfArrayTest( + id="start_neg_large", + array=[1, 2, 3], + search=1, + start=-100, + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative start -100", + ), + IndexOfArrayTest( + id="start_neg_int64", + array=[1, 2, 3], + search=1, + start=Int64(-1), + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative Int64 start", + ), + IndexOfArrayTest( + id="start_neg_double", + array=[1, 2, 3], + search=1, + start=-1.0, + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative double start", + ), + IndexOfArrayTest( + id="start_neg_decimal128", + array=[1, 2, 3], + search=1, + start=Decimal128("-1"), + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative decimal128 start", + ), +] + +# --------------------------------------------------------------------------- +# Error: negative end index +# --------------------------------------------------------------------------- +END_NEGATIVE_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="end_neg_one", + array=[1, 2, 3], + search=1, + start=0, + end=-1, + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative end -1", + ), + IndexOfArrayTest( + id="end_neg_large", + array=[1, 2, 3], + search=1, + start=0, + end=-100, + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative end -100", + ), + IndexOfArrayTest( + id="end_neg_int64", + array=[1, 2, 3], + search=1, + start=0, + end=Int64(-1), + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative Int64 end", + ), + IndexOfArrayTest( + id="end_neg_double", + array=[1, 2, 3], + search=1, + start=0, + end=-1.0, + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative double end", + ), + IndexOfArrayTest( + id="end_neg_decimal128", + array=[1, 2, 3], + search=1, + start=0, + end=Decimal128("-1"), + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="Should reject negative decimal128 end", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + BOUNDARY_INDEX_TESTS + + NEGATIVE_ZERO_START_TESTS + + NOT_ARRAY_ERROR_TESTS + + START_NOT_INTEGRAL_TESTS + + END_NOT_INTEGRAL_TESTS + + START_NEGATIVE_TESTS + + END_NEGATIVE_TESTS +) + +LITERAL_ONLY_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="start_missing_field", + array=[1, 2, 3], + search=1, + start=MISSING, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject missing field as start", + ), + IndexOfArrayTest( + id="end_missing_field", + array=[1, 2, 3], + search=1, + start=0, + end=MISSING, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject missing field as end", + ), +] + +TEST_SUBSET_FOR_LITERAL = [ + NOT_ARRAY_ERROR_TESTS[0], # string_as_array + START_NOT_INTEGRAL_TESTS[0], # start_fractional_double + START_NEGATIVE_TESTS[0], # start_neg_one +] + LITERAL_ONLY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_indexOfArray_literal(collection, test): + """Test $indexOfArray error cases with literal values.""" + result = execute_expression(collection, {"$indexOfArray": build_args(test)}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_indexOfArray_insert(collection, test): + """Test $indexOfArray error cases with values from inserted documents.""" + args, doc = build_insert_args(test) + result = execute_expression_with_insert(collection, {"$indexOfArray": args}, doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +# --------------------------------------------------------------------------- +# Error: wrong arity +# --------------------------------------------------------------------------- +ARITY_ERROR_TESTS = [ + pytest.param({"$indexOfArray": []}, id="zero_args"), + pytest.param({"$indexOfArray": [[1, 2, 3]]}, id="one_arg"), + pytest.param({"$indexOfArray": [[1, 2, 3], 1, 0, 3, 99]}, id="five_args"), +] + + +@pytest.mark.parametrize("expr", ARITY_ERROR_TESTS) +def test_indexOfArray_arity_error(collection, expr): + """Test $indexOfArray errors with wrong number of arguments.""" + result = execute_expression(collection, expr) + assert_expression_result(result, error_code=EXPRESSION_ARITY_ERROR) + + +# --------------------------------------------------------------------------- +# Error: null as literal start/end index +# Standalone test because end=None in IndexOfArrayTest means "no end argument", +# so null-as-end cannot be expressed via the dataclass. +# --------------------------------------------------------------------------- +def test_indexOfArray_null_end(collection): + """Test $indexOfArray with null as end index errors.""" + result = execute_expression(collection, {"$indexOfArray": [[1, 2, 3], 1, 0, None]}) + assert_expression_result( + result, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="Null end should fail" + ) + + +def test_indexOfArray_null_start(collection): + """Test $indexOfArray with null as start index errors.""" + + result = execute_expression(collection, {"$indexOfArray": [[1, 2, 3], 1, None]}) + + assert_expression_result( + result, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="Null start should fail", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py new file mode 100644 index 000000000..b264d9543 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py @@ -0,0 +1,114 @@ +""" +Expression and field path tests for $indexOfArray expression. + +Tests nested expressions, field path lookups, composite paths, +and path through array of objects. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) + + +# --------------------------------------------------------------------------- +# Nested expressions +# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "expression,expected", + [ + # 2-level: inner result used as search value + ( + { + "$indexOfArray": [ + [0, 1, 2, 3], + {"$indexOfArray": [[10, 20, 30], 30]}, + ] + }, + 2, + ), + # 3-level: triple nested, each result feeds the next as search value + ( + { + "$indexOfArray": [ + [0, 1, 2], + { + "$indexOfArray": [ + [0, 1, 2], + {"$indexOfArray": [[5, 10, 15], 10]}, + ] + }, + ] + }, + 1, + ), + # 2-level: inner result used as start index + ( + { + "$indexOfArray": [ + [1, 2, 1, 2], + 1, + {"$indexOfArray": [[10, 20, 30], 20]}, + ] + }, + 2, + ), + ], + ids=["nested_2_level", "nested_3_level", "nested_start_index"], +) +def test_indexOfArray_nested_expression(collection, expression, expected): + """Test $indexOfArray composed with other expressions.""" + result = execute_expression(collection, expression) + assert_expression_result(result, expected=expected) + + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "document,array_ref,search,expected", + [ + ({"a": {"b": [10, 20, 30]}}, "$a.b", 20, 1), + ({"a": {"missing": 1}}, "$a.nonexistent", 0, None), + ({"a": {"b": {"c": [5, 6, 7]}}}, "$a.b.c", 7, 2), + ], + ids=["nested_field_path", "nonexistent_field_null", "deeply_nested_field"], +) +def test_indexOfArray_field_lookup(collection, document, array_ref, search, expected): + """Test $indexOfArray with field path lookups from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$indexOfArray": [array_ref, search]}, document + ) + assert_expression_result(result, expected=expected) + + +# --------------------------------------------------------------------------- +# Field path: path through array of objects +# --------------------------------------------------------------------------- +def test_indexOfArray_path_through_array_of_objects(collection): + """Test $indexOfArray where field path traverses array of objects.""" + result = execute_expression_with_insert( + collection, {"$indexOfArray": ["$a.b", 20]}, {"a": [{"b": 10}, {"b": 20}]} + ) + assert_expression_result(result, expected=1) + + +def test_indexOfArray_composite_array_as_array(collection): + """Test $indexOfArray with composite array from $x.y as the array argument.""" + result = execute_expression_with_insert( + collection, {"$indexOfArray": ["$x.y", 20]}, {"x": [{"y": 10}, {"y": 20}, {"y": 30}]} + ) + assert_expression_result(result, expected=1) + + +def test_indexOfArray_composite_array_as_search(collection): + """Test $indexOfArray with composite array from $x.y as the search value.""" + result = execute_expression_with_insert( + collection, + {"$indexOfArray": [[[10, 20], [30, 40]], "$x.y"]}, + {"x": [{"y": 30}, {"y": 40}]}, + ) + assert_expression_result(result, expected=1) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_null_missing.py new file mode 100644 index 000000000..af5aa3aa1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_null_missing.py @@ -0,0 +1,115 @@ +""" +Null and missing field handling tests for $indexOfArray expression. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.indexOfArray.utils.indexOfArray_common import ( # noqa: E501 + IndexOfArrayTest, + build_args, + build_insert_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import MISSING + +# --------------------------------------------------------------------------- +# Success: null/missing array → null (runs both literal and insert) +# --------------------------------------------------------------------------- +NULL_ARRAY_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="null_array", + array=None, + search=1, + expected=None, + msg="Should return null for null array", + ), + IndexOfArrayTest( + id="null_array_with_start", + array=None, + search=1, + start=0, + expected=None, + msg="Should return null for null array with start", + ), +] + +# --------------------------------------------------------------------------- +# Success: null/missing as search value (runs both literal and insert) +# --------------------------------------------------------------------------- +NULL_SEARCH_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="null_value_in_array", + array=[1, None, 3], + search=None, + expected=1, + msg="Should find null in array", + ), + IndexOfArrayTest( + id="null_value_not_in_array", + array=[1, 2, 3], + search=None, + expected=-1, + msg="Should return -1 for null not in array", + ), +] + +# --------------------------------------------------------------------------- +# Literal only: MISSING field refs +# --------------------------------------------------------------------------- +LITERAL_ONLY_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="missing_array", + array=MISSING, + search=1, + expected=None, + msg="Should return null for missing array", + ), + IndexOfArrayTest( + id="missing_value", + array=[1, 2, 3], + search=MISSING, + expected=-1, + msg="Should return -1 for missing search value", + ), + IndexOfArrayTest( + id="missing_value_null_in_array", + array=[1, None, 3], + search=MISSING, + expected=-1, + msg="Should return -1 for missing search even with null in array", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = NULL_ARRAY_TESTS + NULL_SEARCH_TESTS + +TEST_SUBSET_FOR_LITERAL = [ + NULL_ARRAY_TESTS[0], # null_array + NULL_SEARCH_TESTS[0], # null_value_in_array +] + LITERAL_ONLY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_indexOfArray_literal(collection, test): + """Test $indexOfArray null/missing with literal values.""" + result = execute_expression(collection, {"$indexOfArray": build_args(test)}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_indexOfArray_insert(collection, test): + """Test $indexOfArray null with values from inserted documents.""" + args, doc = build_insert_args(test) + result = execute_expression_with_insert(collection, {"$indexOfArray": args}, doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_expression_indexOfArray.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_expression_indexOfArray.py index d92b4fb31..1959985d7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_expression_indexOfArray.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_expression_indexOfArray.py @@ -31,4 +31,4 @@ def test_smoke_expression_indexOfArray(collection): ) expected = [{"_id": 1, "index": 1}, {"_id": 2, "index": 1}] - assertSuccess(result, expected, msg="Should support $indexOfArray expression") + assertSuccess(result, expected, "Should support $indexOfArray expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/indexOfArray_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/indexOfArray_common.py new file mode 100644 index 000000000..55876abf1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/indexOfArray_common.py @@ -0,0 +1,41 @@ +""" +Shared test infrastructure for $indexOfArray expression tests. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class IndexOfArrayTest(BaseTestCase): + """Test case for $indexOfArray operator.""" + + array: Any = None + search: Any = None + start: Any = None + end: Any = None + + +def build_args(test: IndexOfArrayTest): + """Build the argument list for $indexOfArray from a test case.""" + args = [test.array, test.search] + if test.start is not None: + args.append(test.start) + if test.end is not None: + args.append(test.end) + return args + + +def build_insert_args(test: IndexOfArrayTest): + """Build field-reference argument list and document for insert-based tests.""" + args = ["$arr", "$search"] + doc = {"arr": test.array, "search": test.search} + if test.start is not None: + args.append("$start") + doc["start"] = test.start + if test.end is not None: + args.append("$end") + doc["end"] = test.end + return args, doc diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py new file mode 100644 index 000000000..cb08e86ff --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py @@ -0,0 +1,307 @@ +""" +BSON type tests for $isArray expression. + +Tests arrays containing specific BSON types return true, +non-array BSON types return false, special numeric values, +and boundary values. +""" + +from datetime import datetime + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.isArray.utils.isArray_common import ( # noqa: E501 + IsArrayTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DOUBLE_NEGATIVE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +# --------------------------------------------------------------------------- +# Arrays containing specific BSON types → true +# --------------------------------------------------------------------------- +BSON_ARRAY_TRUE_TESTS: list[IsArrayTest] = [ + IsArrayTest( + id="bindata_array", + value=[Binary(b"\x00", 0)], + expected=True, + msg="Should return true for BinData array", + ), + IsArrayTest( + id="timestamp_array", + value=[Timestamp(0, 0)], + expected=True, + msg="Should return true for Timestamp array", + ), + IsArrayTest( + id="int64_array", value=[Int64(1)], expected=True, msg="Should return true for Int64 array" + ), + IsArrayTest( + id="decimal128_array", + value=[Decimal128("1")], + expected=True, + msg="Should return true for Decimal128 array", + ), + IsArrayTest( + id="objectid_array", + value=[ObjectId()], + expected=True, + msg="Should return true for ObjectId array", + ), + IsArrayTest( + id="datetime_array", + value=[datetime(2024, 1, 1)], + expected=True, + msg="Should return true for datetime array", + ), + IsArrayTest( + id="minkey_array", + value=[MinKey()], + expected=True, + msg="Should return true for MinKey array", + ), + IsArrayTest( + id="maxkey_array", + value=[MaxKey()], + expected=True, + msg="Should return true for MaxKey array", + ), + IsArrayTest( + id="regex_array", + value=[Regex(".*")], + expected=True, + msg="Should return true for Regex array", + ), + IsArrayTest( + id="nan_array", value=[float("nan")], expected=True, msg="Should return true for NaN array" + ), + IsArrayTest( + id="inf_array", + value=[float("inf")], + expected=True, + msg="Should return true for Infinity array", + ), + IsArrayTest( + id="decimal128_nan_array", + value=[Decimal128("NaN")], + expected=True, + msg="Should return true for Decimal128 NaN array", + ), + IsArrayTest( + id="decimal128_inf_array", + value=[Decimal128("Infinity")], + expected=True, + msg="Should return true for Decimal128 Infinity array", + ), + IsArrayTest( + id="decimal128_neg_nan_array", + value=[Decimal128("-NaN")], + expected=True, + msg="Should return true for Decimal128 -NaN array", + ), + IsArrayTest( + id="decimal128_neg_inf_array", + value=[DECIMAL128_NEGATIVE_INFINITY], + expected=True, + msg="Should return true for Decimal128 -Infinity array", + ), + IsArrayTest( + id="neg_inf_array", + value=[FLOAT_NEGATIVE_INFINITY], + expected=True, + msg="Should return true for -Infinity array", + ), + IsArrayTest( + id="neg_zero_array", + value=[DOUBLE_NEGATIVE_ZERO], + expected=True, + msg="Should return true for negative zero array", + ), + IsArrayTest( + id="decimal128_neg_zero_array", + value=[DECIMAL128_NEGATIVE_ZERO], + expected=True, + msg="Should return true for Decimal128 -0 array", + ), + IsArrayTest( + id="nested_mixed_bson_array", + value=[ + MinKey(), + {"a": [Decimal128("1.5")]}, + Int64(1), + datetime(2024, 1, 1), + Binary(b"\x01", 0), + ], + expected=True, + msg="Should return true for nested mixed BSON array", + ), +] + +# --------------------------------------------------------------------------- +# Non-array BSON types → false +# --------------------------------------------------------------------------- +BSON_FALSE_TESTS: list[IsArrayTest] = [ + IsArrayTest(id="int64", value=Int64(1), expected=False, msg="Should return false for Int64"), + IsArrayTest( + id="decimal128", + value=Decimal128("1"), + expected=False, + msg="Should return false for Decimal128", + ), + IsArrayTest( + id="objectid", + value=ObjectId("000000000000000000000001"), + expected=False, + msg="Should return false for ObjectId", + ), + IsArrayTest( + id="datetime", + value=datetime(2024, 1, 1), + expected=False, + msg="Should return false for datetime", + ), + IsArrayTest( + id="binary", value=Binary(b"\x01", 0), expected=False, msg="Should return false for Binary" + ), + IsArrayTest( + id="regex", value=Regex("^abc"), expected=False, msg="Should return false for Regex" + ), + IsArrayTest( + id="timestamp", + value=Timestamp(1, 1), + expected=False, + msg="Should return false for Timestamp", + ), + IsArrayTest(id="minkey", value=MinKey(), expected=False, msg="Should return false for MinKey"), + IsArrayTest(id="maxkey", value=MaxKey(), expected=False, msg="Should return false for MaxKey"), +] + +# --------------------------------------------------------------------------- +# Special numeric values → false +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_TESTS: list[IsArrayTest] = [ + IsArrayTest(id="nan", value=FLOAT_NAN, expected=False, msg="Should return false for NaN"), + IsArrayTest( + id="inf", value=FLOAT_INFINITY, expected=False, msg="Should return false for Infinity" + ), + IsArrayTest( + id="neg_inf", + value=FLOAT_NEGATIVE_INFINITY, + expected=False, + msg="Should return false for -Infinity", + ), + IsArrayTest( + id="neg_zero", + value=DOUBLE_NEGATIVE_ZERO, + expected=False, + msg="Should return false for negative zero", + ), + IsArrayTest( + id="decimal128_nan", + value=DECIMAL128_NAN, + expected=False, + msg="Should return false for Decimal128 NaN", + ), + IsArrayTest( + id="decimal128_neg_nan", + value=Decimal128("-NaN"), + expected=False, + msg="Should return false for Decimal128 -NaN", + ), + IsArrayTest( + id="decimal128_inf", + value=DECIMAL128_INFINITY, + expected=False, + msg="Should return false for Decimal128 Infinity", + ), + IsArrayTest( + id="decimal128_neg_inf", + value=DECIMAL128_NEGATIVE_INFINITY, + expected=False, + msg="Should return false for Decimal128 -Infinity", + ), + IsArrayTest( + id="decimal128_neg_zero", + value=DECIMAL128_NEGATIVE_ZERO, + expected=False, + msg="Should return false for Decimal128 -0", + ), +] + +# --------------------------------------------------------------------------- +# Boundary values → false +# --------------------------------------------------------------------------- +BOUNDARY_TESTS: list[IsArrayTest] = [ + IsArrayTest( + id="int32_max", value=INT32_MAX, expected=False, msg="Should return false for INT32_MAX" + ), + IsArrayTest( + id="int32_min", value=INT32_MIN, expected=False, msg="Should return false for INT32_MIN" + ), + IsArrayTest( + id="int64_max", value=INT64_MAX, expected=False, msg="Should return false for INT64_MAX" + ), + IsArrayTest( + id="int64_min", value=INT64_MIN, expected=False, msg="Should return false for INT64_MIN" + ), + IsArrayTest( + id="decimal128_max", + value=DECIMAL128_MAX, + expected=False, + msg="Should return false for DECIMAL128_MAX", + ), + IsArrayTest( + id="decimal128_min", + value=DECIMAL128_MIN, + expected=False, + msg="Should return false for DECIMAL128_MIN", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_BSON_TESTS = BSON_ARRAY_TRUE_TESTS + BSON_FALSE_TESTS + SPECIAL_NUMERIC_TESTS + BOUNDARY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) +def test_isArray_bson_insert(collection, test): + """Test $isArray BSON types with values from inserted documents.""" + result = execute_expression_with_insert(collection, {"$isArray": "$val"}, {"val": test.value}) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +TEST_SUBSET_FOR_LITERAL = [ + BSON_ARRAY_TRUE_TESTS[0], # bindata_array + BSON_ARRAY_TRUE_TESTS[-1], # nested_mixed_bson_array + BSON_FALSE_TESTS[0], # int64 + SPECIAL_NUMERIC_TESTS[0], # nan +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_isArray_bson_literal(collection, test): + """Test $isArray BSON types with literal values.""" + expr = {"$literal": test.value} if isinstance(test.value, list) else test.value + result = execute_expression(collection, {"$isArray": [expr]}) + assert_expression_result(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py new file mode 100644 index 000000000..d16951ae5 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py @@ -0,0 +1,191 @@ +""" +Core behavior tests for $isArray expression. + +Tests that arrays return true, non-arrays return false, +with basic types via both literal and insert paths. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.isArray.utils.isArray_common import ( # noqa: E501 + IsArrayTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: arrays → true +# --------------------------------------------------------------------------- +IS_ARRAY_TRUE_TESTS: list[IsArrayTest] = [ + IsArrayTest( + id="simple_array", + value=[1, 2, 3], + expected=True, + msg="Should return true for simple array", + ), + IsArrayTest( + id="empty_array", + value=[], + expected=True, + msg="Should return true for empty array", + ), + IsArrayTest( + id="single_element", + value=[42], + expected=True, + msg="Should return true for single-element array", + ), + IsArrayTest( + id="nested_array", + value=[[1, 2], [3, 4]], + expected=True, + msg="Should return true for nested array", + ), + IsArrayTest( + id="mixed_type_array", + value=[1, "two", True, None, {"a": 1}], + expected=True, + msg="Should return true for mixed-type array", + ), + IsArrayTest( + id="array_of_objects", + value=[{"a": 1}, {"b": 2}], + expected=True, + msg="Should return true for array of objects", + ), + IsArrayTest( + id="array_of_nulls", + value=[None, None], + expected=True, + msg="Should return true for array of nulls", + ), + IsArrayTest( + id="string_array", + value=["a", "b", "c"], + expected=True, + msg="Should return true for string array", + ), + IsArrayTest( + id="bool_array", + value=[True], + expected=True, + msg="Should return true for bool array", + ), + IsArrayTest( + id="large_array_10k", + value=list(range(10000)), + expected=True, + msg="10K element array returns true", + ), + IsArrayTest( + id="deeply_nested_array", + value=[[[[[[1]]]]]], + expected=True, + msg="Deeply nested array returns true", + ), + IsArrayTest( + id="large_array_of_arrays", + value=[[i] for i in range(10000)], + expected=True, + msg="10K nested arrays returns true", + ), +] + +# --------------------------------------------------------------------------- +# Success: non-arrays → false +# --------------------------------------------------------------------------- +IS_ARRAY_FALSE_TESTS: list[IsArrayTest] = [ + IsArrayTest(id="string", value="hello", expected=False, msg="Should return false for string"), + IsArrayTest(id="int", value=42, expected=False, msg="Should return false for int"), + IsArrayTest(id="double", value=3.14, expected=False, msg="Should return false for double"), + IsArrayTest(id="bool_true", value=True, expected=False, msg="Should return false for true"), + IsArrayTest(id="bool_false", value=False, expected=False, msg="Should return false for false"), + IsArrayTest(id="null", value=None, expected=False, msg="Should return false for null"), + IsArrayTest(id="object", value={"a": 1}, expected=False, msg="Should return false for object"), + IsArrayTest( + id="empty_string", value="", expected=False, msg="Should return false for empty string" + ), + IsArrayTest( + id="empty_object", value={}, expected=False, msg="Should return false for empty object" + ), + IsArrayTest(id="zero", value=0, expected=False, msg="Should return false for zero"), + IsArrayTest( + id="negative_int", value=-123, expected=False, msg="Should return false for negative int" + ), + IsArrayTest( + id="negative_double", + value=-1.23, + expected=False, + msg="Should return false for negative double", + ), +] + +# --------------------------------------------------------------------------- +# Array-like edge cases → false +# --------------------------------------------------------------------------- +ARRAY_LIKE_TESTS: list[IsArrayTest] = [ + IsArrayTest( + id="string_brackets", value="[]", expected=False, msg="Should return false for string '[]'" + ), + IsArrayTest( + id="string_array_repr", + value="[1, 2, 3]", + expected=False, + msg="Should return false for string '[1, 2, 3]'", + ), + IsArrayTest( + id="array_like_object", + value={"0": "a", "1": "b"}, + expected=False, + msg="Should return false for array-like object", + ), + IsArrayTest( + id="length_object", + value={"length": 3}, + expected=False, + msg="Should return false for object with length key", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +INSERT_TESTS = IS_ARRAY_TRUE_TESTS + IS_ARRAY_FALSE_TESTS + ARRAY_LIKE_TESTS + + +@pytest.mark.parametrize("test", pytest_params(INSERT_TESTS)) +def test_isArray_insert(collection, test): + """Test $isArray with values from inserted documents.""" + result = execute_expression_with_insert(collection, {"$isArray": "$val"}, {"val": test.value}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +TEST_SUBSET_FOR_LITERAL = [ + IS_ARRAY_TRUE_TESTS[0], # simple_array + IS_ARRAY_TRUE_TESTS[1], # empty_array + IS_ARRAY_TRUE_TESTS[3], # nested_array + IS_ARRAY_TRUE_TESTS[5], # array_of_objects + IS_ARRAY_TRUE_TESTS[6], # array_of_nulls + IS_ARRAY_FALSE_TESTS[0], # string + IS_ARRAY_FALSE_TESTS[1], # int + IS_ARRAY_FALSE_TESTS[3], # bool_true + IS_ARRAY_FALSE_TESTS[4], # bool_false + IS_ARRAY_FALSE_TESTS[5], # null + IS_ARRAY_FALSE_TESTS[6], # object +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_isArray_literal(collection, test): + """Test $isArray with literal values.""" + expr = {"$literal": test.value} if isinstance(test.value, list) else test.value + result = execute_expression(collection, {"$isArray": [expr]}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_errors.py new file mode 100644 index 000000000..d10723cb9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_errors.py @@ -0,0 +1,29 @@ +""" +Error and argument handling tests for $isArray expression. + +Tests arity errors. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, +) +from documentdb_tests.framework.error_codes import EXPRESSION_TYPE_MISMATCH_ERROR + +# --------------------------------------------------------------------------- +# Arity errors +# --------------------------------------------------------------------------- +ARITY_ERROR_TESTS = [ + pytest.param({"$isArray": []}, id="zero_args"), + pytest.param({"$isArray": [1, 2]}, id="two_args"), + pytest.param({"$isArray": [1, 2, 3]}, id="three_args"), +] + + +@pytest.mark.parametrize("expr", ARITY_ERROR_TESTS) +def test_isArray_arity_error(collection, expr): + """Test $isArray errors with wrong number of arguments.""" + result = execute_expression(collection, expr) + assert_expression_result(result, error_code=EXPRESSION_TYPE_MISMATCH_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py new file mode 100644 index 000000000..c1144c496 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py @@ -0,0 +1,177 @@ +""" +Expression, field path, and variable tests for $isArray expression. + +Tests nested expressions, field path lookups, composite paths, +null/missing handling, self-nesting, system variables, +and large input. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +SELF_NESTING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="self_nested", + expression={"$isArray": [{"$isArray": "$arr"}]}, + doc={"arr": [1, 2]}, + expected=False, + msg="Inner returns bool, outer returns false", + ), +] + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_array", + expression={"$isArray": "$a.b"}, + doc={"a": {"b": [1, 2]}}, + expected=True, + msg="Nested array field", + ), + ExpressionTestCase( + id="deeply_nested_array", + expression={"$isArray": "$a.b.c"}, + doc={"a": {"b": {"c": [1]}}}, + expected=True, + msg="Deeply nested array field", + ), + ExpressionTestCase( + id="numeric_index_on_object_key", + expression={"$isArray": "$a.0.b"}, + doc={"a": {"0": {"b": [1]}}}, + expected=True, + msg="Numeric key on object resolves to array", + ), +] + +# --------------------------------------------------------------------------- +# Composite array paths +# --------------------------------------------------------------------------- +COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="composite_array_path", + expression={"$isArray": "$a.b"}, + doc={"a": [{"b": 1}, {"b": 2}]}, + expected=True, + msg="Composite array path should resolve to array", + ), + ExpressionTestCase( + id="composite_empty_array", + expression={"$isArray": "$a.b"}, + doc={"a": []}, + expected=True, + msg="Composite on empty array resolves to empty array", + ), + ExpressionTestCase( + id="composite_three_elements", + expression={"$isArray": "$a.b"}, + doc={"a": [{"b": 1}, {"b": 2}, {"b": 3}]}, + expected=True, + msg="Three-element composite resolves to array", + ), +] + +# --------------------------------------------------------------------------- +# Deep composite array traversal +# --------------------------------------------------------------------------- +DEEP_COMPOSITE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="array_at_leaf", + expression={"$isArray": "$a.b.c.d"}, + doc={"a": {"b": {"c": {"d": [1, 2, 3]}}}}, + expected=True, + msg="Array at leaf level", + ), + ExpressionTestCase( + id="array_at_c", + expression={"$isArray": "$a.b.c.d"}, + doc={"a": {"b": {"c": [{"d": 1}]}}}, + expected=True, + msg="Array at c level", + ), +] + +# --------------------------------------------------------------------------- +# Null and missing handling +# --------------------------------------------------------------------------- +NULL_MISSING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="missing_field", + expression={"$isArray": "$nonexistent"}, + doc={"other": 1}, + expected=False, + msg="Missing field should return false", + ), + ExpressionTestCase( + id="missing_nested_partial_path", + expression={"$isArray": "$a.b"}, + doc={"a": 1}, + expected=False, + msg="Nested field on scalar parent returns false", + ), + ExpressionTestCase( + id="missing_nested_empty_doc", + expression={"$isArray": "$a.b"}, + doc={"x": 1}, + expected=False, + msg="Missing nested field returns false", + ), +] + +# --------------------------------------------------------------------------- +# System variables +# --------------------------------------------------------------------------- +SYSTEM_VAR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="root_variable", + expression={"$isArray": "$$ROOT"}, + doc={"a": 1}, + expected=False, + msg="$$ROOT is object, returns false", + ), + ExpressionTestCase( + id="current_variable", + expression={"$isArray": "$$CURRENT"}, + doc={"a": 1}, + expected=False, + msg="$$CURRENT is object, returns false", + ), + ExpressionTestCase( + id="let_array_variable", + expression={"$let": {"vars": {"x": "$arr"}, "in": {"$isArray": "$$x"}}}, + doc={"arr": [1, 2]}, + expected=True, + msg="$let var bound to array returns true", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_EXPR_TESTS = ( + FIELD_LOOKUP_TESTS + + COMPOSITE_PATH_TESTS + + DEEP_COMPOSITE_TESTS + + NULL_MISSING_TESTS + + SYSTEM_VAR_TESTS + + SELF_NESTING_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) +def test_isArray_expression(collection, test): + """Test $isArray with field paths and expressions.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_expression_isArray.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_expression_isArray.py index c16ab7d55..54608b372 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_expression_isArray.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_expression_isArray.py @@ -26,4 +26,4 @@ def test_smoke_expression_isArray(collection): ) expected = [{"_id": 1, "isArray": True}, {"_id": 2, "isArray": False}] - assertSuccess(result, expected, msg="Should support $isArray expression") + assertSuccess(result, expected, "Should support $isArray expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/isArray_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/isArray_common.py new file mode 100644 index 000000000..8d3a010d9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/isArray_common.py @@ -0,0 +1,15 @@ +""" +Shared test infrastructure for $isArray expression tests. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class IsArrayTest(BaseTestCase): + """Test case for $isArray operator.""" + + value: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/arrays_in_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/arrays_in_common.py new file mode 100644 index 000000000..ecce25890 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/arrays_in_common.py @@ -0,0 +1,19 @@ +""" +Shared test infrastructure for $in expression tests. + +Note: This file lives in array/utils/ rather than array/in/utils/ because +"in" is a Python keyword and cannot be used in import paths. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class InTest(BaseTestCase): + """Test case for $in operator.""" + + value: Any = None + array: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py new file mode 100644 index 000000000..a542e6461 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py @@ -0,0 +1,376 @@ +""" +Combination tests for array expression operators: $arrayElemAt, $indexOfArray, $in, $slice. + +Tests that verify these operators work correctly when composed with each other +and with other operators like $concatArrays, $reverseArray, $filter, $map, $size, etc. +""" + +from dataclasses import dataclass +from typing import Any + +import pytest +from bson import Decimal128, MaxKey, MinKey, Regex + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class CombinationTest(BaseTestCase): + """Test case for combination expression tests.""" + + expr: Any = None + + +# --------------------------------------------------------------------------- +# $arrayElemAt combinations +# --------------------------------------------------------------------------- +ARRAY_ELEM_AT_COMBINATION_TESTS: list[CombinationTest] = [ + CombinationTest( + id="arrayElemAt_index_from_indexOfArray", + expr={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 30]}]}, + expected=30, + msg="Should use $indexOfArray result as index", + ), + CombinationTest( + id="arrayElemAt_last_element_via_size", + expr={"$arrayElemAt": [[10, 20, 30], {"$subtract": [{"$size": [[10, 20, 30]]}, 1]}]}, + expected=30, + msg="Should access last element via $size - 1", + ), + CombinationTest( + id="arrayElemAt_elem_from_slice", + expr={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], -2]}, 0]}, + expected=30, + msg="Should access element from $slice result", + ), + CombinationTest( + id="arrayElemAt_elem_from_slice_3arg", + expr={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], 1, 2]}, 1]}, + expected=30, + msg="Should access element from $slice 3-arg result", + ), + CombinationTest( + id="arrayElemAt_elem_from_reverseArray", + expr={"$arrayElemAt": [{"$reverseArray": [[10, 20, 30]]}, 0]}, + expected=30, + msg="Should access element from $reverseArray result", + ), + CombinationTest( + id="arrayElemAt_elem_from_concatArrays", + expr={"$arrayElemAt": [{"$concatArrays": [[10, 20], [30, 40]]}, 2]}, + expected=30, + msg="Should access element from $concatArrays result", + ), + CombinationTest( + id="arrayElemAt_computed_index", + expr={"$arrayElemAt": [[10, 20, 30], {"$subtract": [3, 1]}]}, + expected=30, + msg="Should use computed index from $subtract", + ), +] + +# --------------------------------------------------------------------------- +# $in combinations +# --------------------------------------------------------------------------- +IN_COMBINATION_TESTS: list[CombinationTest] = [ + CombinationTest( + id="in_value_from_add", + expr={"$in": [{"$add": [1, 1]}, [1, 2, 3]]}, + expected=True, + msg="Should find value computed by $add", + ), + CombinationTest( + id="in_array_from_concatArrays", + expr={"$in": [3, {"$concatArrays": [[1, 2], [3, 4]]}]}, + expected=True, + msg="Should search in $concatArrays result", + ), + CombinationTest( + id="in_value_from_arrayElemAt", + expr={"$in": [{"$arrayElemAt": [[10, 20, 30], 1]}, [5, 20, 35]]}, + expected=True, + msg="Should find value from $arrayElemAt", + ), + CombinationTest( + id="in_array_from_filter", + expr={ + "$in": [ + 4, + { + "$filter": { + "input": [1, 2, 3, 4, 5], + "as": "n", + "cond": {"$gte": ["$$n", 3]}, + } + }, + ] + }, + expected=True, + msg="Should search in $filter result", + ), + CombinationTest( + id="in_array_from_map", + expr={ + "$in": [ + 20, + { + "$map": { + "input": [1, 2, 3], + "as": "n", + "in": {"$multiply": ["$$n", 10]}, + } + }, + ] + }, + expected=True, + msg="Should search in $map result", + ), + CombinationTest( + id="in_array_from_reverseArray", + expr={"$in": [1, {"$reverseArray": [[1, 2, 3]]}]}, + expected=True, + msg="Should search in $reverseArray result", + ), + CombinationTest( + id="in_cond_with_inner_in", + expr={"$in": [5, {"$cond": [{"$in": ["a", ["a", "b"]]}, [5, 6], [7, 8]]}]}, + expected=True, + msg="Should search in $cond-selected array", + ), + CombinationTest( + id="in_inside_cond", + expr={"$cond": [{"$in": [2, [1, 2, 3]]}, "found", "not_found"]}, + expected="found", + msg="Should use $in result in $cond", + ), + CombinationTest( + id="in_value_from_indexOfArray", + expr={"$in": [{"$indexOfArray": [[10, 20, 30], 20]}, [0, 1, 2]]}, + expected=True, + msg="Should find $indexOfArray result in array", + ), + CombinationTest( + id="in_nested_decimal128", + expr={ + "$in": [ + {"$arrayElemAt": [[Decimal128("1.1"), Decimal128("2.2")], 1]}, + [Decimal128("2.2"), Decimal128("3.3")], + ] + }, + expected=True, + msg="Should find Decimal128 from $arrayElemAt in array", + ), +] + +# --------------------------------------------------------------------------- +# $indexOfArray combinations +# --------------------------------------------------------------------------- +INDEX_OF_ARRAY_COMBINATION_TESTS: list[CombinationTest] = [ + CombinationTest( + id="indexOfArray_result_as_arrayElemAt_index", + expr={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 20]}]}, + expected=20, + msg="Should use $indexOfArray result as $arrayElemAt index", + ), + CombinationTest( + id="indexOfArray_search_from_add", + expr={"$indexOfArray": [[1, 2, 3], {"$add": [1, 1]}]}, + expected=1, + msg="Should search for value computed by $add", + ), + CombinationTest( + id="indexOfArray_array_from_concatArrays", + expr={"$indexOfArray": [{"$concatArrays": [[1, 2], [3, 4]]}, 3]}, + expected=2, + msg="Should search in $concatArrays result", + ), + CombinationTest( + id="indexOfArray_array_from_filter", + expr={ + "$indexOfArray": [ + {"$filter": {"input": [1, 2, 3, 4, 5], "cond": {"$gt": ["$$this", 2]}}}, + 4, + ] + }, + expected=1, + msg="Should search in $filter result", + ), + CombinationTest( + id="indexOfArray_result_in_cond", + expr={"$cond": [{"$gte": [{"$indexOfArray": [[1, 2, 3], 2]}, 0]}, "found", "not_found"]}, + expected="found", + msg="Should use $indexOfArray result in $cond", + ), + CombinationTest( + id="indexOfArray_start_from_subtract", + expr={"$indexOfArray": [[1, 2, 1, 2], 1, {"$subtract": [3, 1]}]}, + expected=2, + msg="Should use $subtract result as start index", + ), + CombinationTest( + id="indexOfArray_via_arrayElemAt", + expr={ + "$indexOfArray": [ + ["a", "b", "c", "d"], + { + "$arrayElemAt": [ + ["a", "b", "c", "d"], + {"$indexOfArray": [[10, 20, 30], 20]}, + ] + }, + ] + }, + expected=1, + msg="Should search for value from nested $arrayElemAt/$indexOfArray", + ), + CombinationTest( + id="indexOfArray_subarray_mixed_bson", + expr={ + "$indexOfArray": [ + [[MinKey(), MaxKey()], [1, 2], "x"], + { + "$arrayElemAt": [ + [[MinKey(), MaxKey()], [1, 2], "x"], + {"$indexOfArray": [[[MinKey(), MaxKey()], [1, 2], "x"], [1, 2]]}, + ] + }, + ] + }, + expected=1, + msg="Should find mixed BSON subarray via nested operators", + ), + CombinationTest( + id="indexOfArray_triple_nested_decimal128", + expr={ + "$indexOfArray": [ + [Decimal128("1.1"), Decimal128("2.2"), Decimal128("3.3")], + { + "$arrayElemAt": [ + [Decimal128("1.1"), Decimal128("2.2"), Decimal128("3.3")], + { + "$indexOfArray": [ + [Decimal128("1.1"), Decimal128("2.2"), Decimal128("3.3")], + Decimal128("3.3"), + ] + }, + ] + }, + ] + }, + expected=2, + msg="Should resolve triple-nested Decimal128 operators", + ), +] + +# --------------------------------------------------------------------------- +# $slice combinations +# --------------------------------------------------------------------------- +SLICE_COMBINATION_TESTS: list[CombinationTest] = [ + CombinationTest( + id="slice_array_from_concatArrays", + expr={"$slice": [{"$concatArrays": [[1, 2], [3, 4, 5]]}, 3]}, + expected=[1, 2, 3], + msg="Should slice $concatArrays result", + ), + CombinationTest( + id="slice_n_from_subtract", + expr={"$slice": [[1, 2, 3, 4, 5], {"$subtract": [5, 2]}]}, + expected=[1, 2, 3], + msg="Should use $subtract result as n", + ), + CombinationTest( + id="slice_array_from_filter", + expr={ + "$slice": [ + { + "$filter": { + "input": [1, 2, 3, 4, 5], + "as": "n", + "cond": {"$gte": ["$$n", 3]}, + } + }, + 2, + ] + }, + expected=[3, 4], + msg="Should slice $filter result", + ), + CombinationTest( + id="slice_position_from_indexOfArray", + expr={ + "$slice": [ + [10, 20, 30, 40, 50], + {"$indexOfArray": [[10, 20, 30, 40, 50], 30]}, + 2, + ] + }, + expected=[30, 40], + msg="Should use $indexOfArray result as position", + ), + CombinationTest( + id="slice_array_from_map", + expr={ + "$slice": [ + { + "$map": { + "input": [1, 2, 3], + "as": "n", + "in": {"$multiply": ["$$n", 10]}, + } + }, + 2, + ] + }, + expected=[10, 20], + msg="Should slice $map result", + ), + CombinationTest( + id="slice_array_from_reverseArray", + expr={"$slice": [{"$reverseArray": [[1, 2, 3, 4, 5]]}, 3]}, + expected=[5, 4, 3], + msg="Should slice $reverseArray result", + ), + CombinationTest( + id="slice_n_from_size", + expr={"$slice": [[10, 20, 30, 40], {"$subtract": [{"$size": [[10, 20, 30, 40]]}, 1]}]}, + expected=[10, 20, 30], + msg="Should use $size-based computation as n", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate all combination tests +# --------------------------------------------------------------------------- +ALL_COMBINATION_TESTS = ( + ARRAY_ELEM_AT_COMBINATION_TESTS + + IN_COMBINATION_TESTS + + INDEX_OF_ARRAY_COMBINATION_TESTS + + SLICE_COMBINATION_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_COMBINATION_TESTS)) +def test_combination_expression(collection, test): + """Test array operators composed with other operators.""" + result = execute_expression(collection, test.expr) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# --------------------------------------------------------------------------- +# Standalone tests for behavior that doesn't fit the dataclass pattern +# --------------------------------------------------------------------------- +def test_arrayElemAt_oob_is_missing_not_null(collection): + """Test out-of-bounds result is truly MISSING (field absent), not null.""" + result = execute_expression(collection, {"$type": {"$arrayElemAt": [[1, 2, 3], 10]}}) + assert_expression_result(result, expected="missing") + + +def test_arrayElemAt_regex_type_preserved(collection): + """Test $arrayElemAt preserves regex element type.""" + result = execute_expression(collection, {"$type": {"$arrayElemAt": [[Regex("abc")], 0]}}) + assert_expression_result(result, expected="regex") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_filter.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_filter.py new file mode 100644 index 000000000..87a7d2cba --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_filter.py @@ -0,0 +1,65 @@ +""" +Combination tests for $filter composed with other operators. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +FILTER_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="filter_then_size", + expression={"$size": {"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 2]}}}}, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=3, + msg="Size of filtered array", + ), + ExpressionTestCase( + id="map_then_filter", + expression={ + "$filter": { + "input": {"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, + "cond": {"$gt": ["$$this", 5]}, + } + }, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[6, 8, 10], + msg="Should filter mapped array", + ), + ExpressionTestCase( + id="isArray_on_filter_result", + expression={"$isArray": {"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 0]}}}}, + doc={"arr": [1, 2, 3]}, + expected=True, + msg="$isArray on $filter result should return true", + ), + ExpressionTestCase( + id="filter_result_into_reduce", + expression={ + "$reduce": { + "input": {"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 3]}}}, + "initialValue": 0, + "in": {"$add": ["$$value", "$$this"]}, + } + }, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=9, + msg="$reduce of filtered result (4+5=9)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FILTER_COMBINATION_TESTS)) +def test_filter_combination(collection, test): + """Test $filter composed with other operators.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_isArray.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_isArray.py new file mode 100644 index 000000000..5c7566b68 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_isArray.py @@ -0,0 +1,54 @@ +""" +Combination tests for $isArray composed with other operators. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +ISARRAY_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="isarray_guard_array", + expression={"$cond": {"if": {"$isArray": "$arr"}, "then": {"$size": "$arr"}, "else": "NA"}}, + doc={"arr": [1, 2]}, + expected=2, + msg="$isArray guard should allow $size on array", + ), + ExpressionTestCase( + id="isarray_on_concatArrays", + expression={"$isArray": {"$concatArrays": ["$a", "$b"]}}, + doc={"a": [1], "b": [2]}, + expected=True, + msg="$isArray on $concatArrays result should return true", + ), + ExpressionTestCase( + id="isarray_on_objectToArray", + expression={"$isArray": {"$objectToArray": "$obj"}}, + doc={"obj": {"a": 1}}, + expected=True, + msg="$isArray on $objectToArray result should return true", + ), + ExpressionTestCase( + id="isarray_on_non_array_expression", + expression={"$isArray": {"$add": ["$x", "$y"]}}, + doc={"x": 1, "y": 2}, + expected=False, + msg="$isArray on $add result should return false", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ISARRAY_COMBINATION_TESTS)) +def test_isArray_combination(collection, test): + """Test $isArray composed with other operators.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) From 20d662c679d0ca1f54fc94cabf66b5c564be825e Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 11:40:23 -0700 Subject: [PATCH 02/10] Replace InTest with ArrayTestClass Signed-off-by: Alina (Xi) Li --- .../array/in/test_expression_in_bson_types.py | 62 ++++++++--------- .../in/test_expression_in_core_behavior.py | 68 ++++++++++--------- .../array/in/test_expression_in_errors.py | 42 ++++++------ .../in/test_expression_in_nested_arrays.py | 40 +++++------ .../in/test_expression_in_null_missing.py | 16 ++--- .../array/utils/array_test_case.py | 28 ++++++++ .../array/utils/arrays_in_common.py | 19 ------ 7 files changed, 143 insertions(+), 132 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/arrays_in_common.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py index 004620a75..04850fe61 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py @@ -10,8 +10,8 @@ import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp -from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 - InTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -31,71 +31,71 @@ # --------------------------------------------------------------------------- # Success: search for various BSON types # --------------------------------------------------------------------------- -BSON_TYPE_TESTS: list[InTest] = [ - InTest( +BSON_TYPE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="bson_int64", value=Int64(99), array=[Int64(99), 1], expected=True, msg="Should find Int64 in array", ), - InTest( + ArrayTestClass( id="bson_decimal128", value=Decimal128("1.5"), array=[Decimal128("1.5"), 2], expected=True, msg="Should find Decimal128 in array", ), - InTest( + ArrayTestClass( id="bson_datetime", value=datetime(2024, 1, 1), array=[datetime(2024, 1, 1), 1], expected=True, msg="Should find datetime in array", ), - InTest( + ArrayTestClass( id="bson_objectid", value=ObjectId("000000000000000000000001"), array=[ObjectId("000000000000000000000001"), 1], expected=True, msg="Should find ObjectId in array", ), - InTest( + ArrayTestClass( id="bson_binary", value=Binary(b"\x01\x02", 0), array=[Binary(b"\x01\x02", 0), 1], expected=True, msg="Should find Binary in array", ), - InTest( + ArrayTestClass( id="bson_regex", value=Regex("^abc", "i"), array=[Regex("^abc", "i"), 1], expected=True, msg="Should find Regex in array", ), - InTest( + ArrayTestClass( id="bson_timestamp", value=Timestamp(1, 1), array=[Timestamp(1, 1), 1], expected=True, msg="Should find Timestamp in array", ), - InTest( + ArrayTestClass( id="bson_minkey", value=MinKey(), array=[MinKey(), 1], expected=True, msg="Should find MinKey in array", ), - InTest( + ArrayTestClass( id="bson_maxkey", value=MaxKey(), array=[1, MaxKey()], expected=True, msg="Should find MaxKey in array", ), - InTest( + ArrayTestClass( id="bson_uuid", value=Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), array=[Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), 1], @@ -103,21 +103,21 @@ msg="Should find UUID binary in array", ), # Special float values - InTest( + ArrayTestClass( id="float_infinity_in_array", value=FLOAT_INFINITY, array=[FLOAT_INFINITY, 1], expected=True, msg="Should find Infinity in array", ), - InTest( + ArrayTestClass( id="float_neg_infinity_in_array", value=FLOAT_NEGATIVE_INFINITY, array=[FLOAT_NEGATIVE_INFINITY, 1], expected=True, msg="Should find -Infinity in array", ), - InTest( + ArrayTestClass( id="float_infinity_not_in_array", value=FLOAT_INFINITY, array=[1, 2, 3], @@ -125,14 +125,14 @@ msg="Should not find Infinity in non-Infinity array", ), # Special Decimal128 values - InTest( + ArrayTestClass( id="decimal128_infinity_in_array", value=DECIMAL128_INFINITY, array=[DECIMAL128_INFINITY, 1], expected=True, msg="Should find Decimal128 Infinity in array", ), - InTest( + ArrayTestClass( id="decimal128_neg_infinity_in_array", value=DECIMAL128_NEGATIVE_INFINITY, array=[DECIMAL128_NEGATIVE_INFINITY, 1], @@ -140,28 +140,28 @@ msg="Should find Decimal128 -Infinity in array", ), # NaN equality: NaN == NaN in BSON comparison (unlike IEEE 754) - InTest( + ArrayTestClass( id="float_nan_found", value=FLOAT_NAN, array=[FLOAT_NAN, 1], expected=True, msg="Should find NaN in array (BSON equality)", ), - InTest( + ArrayTestClass( id="decimal128_nan_found", value=DECIMAL128_NAN, array=[DECIMAL128_NAN, 1], expected=True, msg="Should find Decimal128 NaN in array (BSON equality)", ), - InTest( + ArrayTestClass( id="float_nan_matches_decimal128_nan", value=FLOAT_NAN, array=[DECIMAL128_NAN, 1], expected=True, msg="float NaN should match Decimal128 NaN cross-type", ), - InTest( + ArrayTestClass( id="decimal128_nan_matches_float_nan", value=DECIMAL128_NAN, array=[FLOAT_NAN, 1], @@ -169,7 +169,7 @@ msg="Decimal128 NaN should match float NaN cross-type", ), # Aggregation $in does NOT pattern-match regex against strings (unlike query $in) - InTest( + ArrayTestClass( id="regex_no_pattern_match", value=Regex("^a"), array=["abc", "def"], @@ -181,50 +181,50 @@ # --------------------------------------------------------------------------- # Success: numeric type equivalence # --------------------------------------------------------------------------- -NUMERIC_EQUIVALENCE_TESTS: list[InTest] = [ - InTest( +NUMERIC_EQUIVALENCE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="int_in_doubles", value=1, array=[1.0, 2.0], expected=True, msg="Should find int in doubles via numeric equivalence", ), - InTest( + ArrayTestClass( id="int_in_longs", value=1, array=[Int64(1), 2], expected=True, msg="Should find int in longs via numeric equivalence", ), - InTest( + ArrayTestClass( id="int_in_decimal128s", value=1, array=[Decimal128("1"), 2], expected=True, msg="Should find int in decimal128s via numeric equivalence", ), - InTest( + ArrayTestClass( id="double_in_ints", value=1.0, array=[1, 2], expected=True, msg="Should find double in ints via numeric equivalence", ), - InTest( + ArrayTestClass( id="long_in_ints", value=Int64(1), array=[1, 2], expected=True, msg="Should find long in ints via numeric equivalence", ), - InTest( + ArrayTestClass( id="decimal128_in_ints", value=Decimal128("1"), array=[1, 2], expected=True, msg="Should find decimal128 in ints via numeric equivalence", ), - InTest( + ArrayTestClass( id="decimal128_in_doubles", value=Decimal128("1.5"), array=[1.5, 2.5], diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py index ebce36d03..6ed013129 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py @@ -6,8 +6,8 @@ import pytest -from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 - InTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -19,64 +19,66 @@ # --------------------------------------------------------------------------- # Success: value found in array → True # --------------------------------------------------------------------------- -FOUND_TESTS: list[InTest] = [ - InTest(id="found_int", value=2, array=[1, 2, 3], expected=True, msg="Should find int in array"), - InTest( +FOUND_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( + id="found_int", value=2, array=[1, 2, 3], expected=True, msg="Should find int in array" + ), + ArrayTestClass( id="found_first", value=1, array=[1, 2, 3], expected=True, msg="Should find first element" ), - InTest( + ArrayTestClass( id="found_last", value=3, array=[1, 2, 3], expected=True, msg="Should find last element" ), - InTest( + ArrayTestClass( id="found_string", value="b", array=["a", "b", "c"], expected=True, msg="Should find string in array", ), - InTest( + ArrayTestClass( id="found_bool_true", value=True, array=[True, False], expected=True, msg="Should find true in array", ), - InTest( + ArrayTestClass( id="found_bool_false", value=False, array=[True, False], expected=True, msg="Should find false in array", ), - InTest( + ArrayTestClass( id="found_null", value=None, array=[None, 1, 2], expected=True, msg="Should find null in array", ), - InTest( + ArrayTestClass( id="found_nested_array", value=[3, 4], array=[[1, 2], [3, 4]], expected=True, msg="Should find nested array", ), - InTest( + ArrayTestClass( id="found_object", value={"a": 1}, array=[{"a": 1}, {"b": 2}], expected=True, msg="Should find object in array", ), - InTest( + ArrayTestClass( id="found_single_element", value=42, array=[42], expected=True, msg="Should find value in single-element array", ), - InTest( + ArrayTestClass( id="found_duplicate", value=5, array=[5, 5, 5], @@ -88,57 +90,57 @@ # --------------------------------------------------------------------------- # Success: value not found → False # --------------------------------------------------------------------------- -NOT_FOUND_TESTS: list[InTest] = [ - InTest( +NOT_FOUND_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="not_found_int", value=4, array=[1, 2, 3], expected=False, msg="Should not find absent int", ), - InTest( + ArrayTestClass( id="not_found_string", value="z", array=["a", "b"], expected=False, msg="Should not find absent string", ), - InTest( + ArrayTestClass( id="not_found_empty_array", value=1, array=[], expected=False, msg="Should not find value in empty array", ), - InTest( + ArrayTestClass( id="not_found_type_mismatch", value="1", array=[1, 2, 3], expected=False, msg="Should not find string '1' in int array", ), - InTest( + ArrayTestClass( id="not_found_bool_vs_int", value=True, array=[1, 0], expected=False, msg="Should not find bool in int array", ), - InTest( + ArrayTestClass( id="not_found_null", value=None, array=[1, 2, 3], expected=False, msg="Should not find null in non-null array", ), - InTest( + ArrayTestClass( id="not_found_partial_array", value=[1], array=[[1, 2], [3, 4]], expected=False, msg="Should not find partial array match", ), - InTest( + ArrayTestClass( id="not_found_partial_object", value={"a": 1}, array=[{"a": 1, "b": 2}], @@ -150,29 +152,29 @@ # --------------------------------------------------------------------------- # Success: mixed types in array # --------------------------------------------------------------------------- -MIXED_TYPE_TESTS: list[InTest] = [ - InTest( +MIXED_TYPE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="mixed_find_string", value="2", array=[1, "2", True, None, [1]], expected=True, msg="Should find string in mixed-type array", ), - InTest( + ArrayTestClass( id="mixed_find_null", value=None, array=[1, "2", True, None, [1]], expected=True, msg="Should find null in mixed-type array", ), - InTest( + ArrayTestClass( id="mixed_find_array", value=[1], array=[1, "2", True, None, [1]], expected=True, msg="Should find array in mixed-type array", ), - InTest( + ArrayTestClass( id="mixed_not_found", value="x", array=[1, "2", True, None, [1]], @@ -187,29 +189,29 @@ _LARGE_ARRAY_SIZE = 20_000 _LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) -LARGE_ARRAY_TESTS: list[InTest] = [ - InTest( +LARGE_ARRAY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="large_array_found_first", value=0, array=_LARGE_ARRAY, expected=True, msg="Should find first element in large array", ), - InTest( + ArrayTestClass( id="large_array_found_last", value=_LARGE_ARRAY_SIZE - 1, array=_LARGE_ARRAY, expected=True, msg="Should find last element in large array", ), - InTest( + ArrayTestClass( id="large_array_found_middle", value=_LARGE_ARRAY_SIZE // 2, array=_LARGE_ARRAY, expected=True, msg="Should find middle element in large array", ), - InTest( + ArrayTestClass( id="large_array_not_found", value=-1, array=_LARGE_ARRAY, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py index eec4c9082..99ee2031d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py @@ -9,8 +9,8 @@ import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp -from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 - InTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -27,113 +27,113 @@ # --------------------------------------------------------------------------- # Error: second argument not an array (runs both literal and insert) # --------------------------------------------------------------------------- -NOT_ARRAY_ERROR_TESTS: list[InTest] = [ - InTest( +NOT_ARRAY_ERROR_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="string_as_array", value=1, array="hello", error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject string as array arg", ), - InTest( + ArrayTestClass( id="int_as_array", value=1, array=42, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject int as array arg", ), - InTest( + ArrayTestClass( id="double_as_array", value=1, array=3.14, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject double as array arg", ), - InTest( + ArrayTestClass( id="bool_true_as_array", value=1, array=True, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject bool true as array arg", ), - InTest( + ArrayTestClass( id="bool_false_as_array", value=1, array=False, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject bool false as array arg", ), - InTest( + ArrayTestClass( id="object_as_array", value=1, array={"a": 1}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject object as array arg", ), - InTest( + ArrayTestClass( id="decimal128_as_array", value=1, array=Decimal128("1"), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject decimal128 as array arg", ), - InTest( + ArrayTestClass( id="int64_as_array", value=1, array=Int64(1), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject int64 as array arg", ), - InTest( + ArrayTestClass( id="binary_as_array", value=1, array=Binary(b"x", 0), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject binary as array arg", ), - InTest( + ArrayTestClass( id="datetime_as_array", value=1, array=datetime(2024, 1, 1), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject datetime as array arg", ), - InTest( + ArrayTestClass( id="objectid_as_array", value=1, array=ObjectId(), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject objectid as array arg", ), - InTest( + ArrayTestClass( id="regex_as_array", value=1, array=Regex("x"), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject regex as array arg", ), - InTest( + ArrayTestClass( id="maxkey_as_array", value=1, array=MaxKey(), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject maxkey as array arg", ), - InTest( + ArrayTestClass( id="minkey_as_array", value=1, array=MinKey(), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject minkey as array arg", ), - InTest( + ArrayTestClass( id="timestamp_as_array", value=1, array=Timestamp(0, 0), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject timestamp as array arg", ), - InTest( + ArrayTestClass( id="null_as_array", value=1, array=None, @@ -145,8 +145,8 @@ # --------------------------------------------------------------------------- # Error: missing as array (literal only, MISSING is a field ref) # --------------------------------------------------------------------------- -LITERAL_ONLY_TESTS: list[InTest] = [ - InTest( +LITERAL_ONLY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="missing_as_array", value=1, array=MISSING, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py index 780de00c0..168ba1cc2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py @@ -9,8 +9,8 @@ import pytest from bson import Binary, Decimal128, MaxKey, MinKey, ObjectId -from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 - InTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -22,71 +22,71 @@ # --------------------------------------------------------------------------- # Success: nested mixed arrays as search targets # --------------------------------------------------------------------------- -NESTED_MIXED_ARRAY_TESTS: list[InTest] = [ - InTest( +NESTED_MIXED_ARRAY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="nested_find_object_in_mixed", value={"a": 1}, array=[1, "two", {"a": 1}, [3, 4], True], expected=True, msg="Should find object in nested mixed array", ), - InTest( + ArrayTestClass( id="nested_find_array_in_mixed", value=[3, 4], array=[1, "two", {"a": 1}, [3, 4], True], expected=True, msg="Should find array in nested mixed array", ), - InTest( + ArrayTestClass( id="nested_find_deep_object", value={"a": {"b": 3}}, array=[[1, 2], {"a": {"b": 3}}, "x"], expected=True, msg="Should find deep object in array", ), - InTest( + ArrayTestClass( id="nested_find_array_with_mixed_types", value=[None, "a", 2], array=[1, [None, "a", 2], "b"], expected=True, msg="Should find mixed-type subarray", ), - InTest( + ArrayTestClass( id="nested_find_empty_object", value={}, array=[1, {}, [2], "a"], expected=True, msg="Should find empty object in array", ), - InTest( + ArrayTestClass( id="nested_find_empty_array", value=[], array=[1, {}, [], "a"], expected=True, msg="Should find empty array in array", ), - InTest( + ArrayTestClass( id="nested_find_subarray_binary_decimal128", value=[Binary(b"\x01\x02", 0), Decimal128("3.14")], array=[1, [Binary(b"\x01\x02", 0), Decimal128("3.14")], "x", [3]], expected=True, msg="Should find subarray with binary and decimal128", ), - InTest( + ArrayTestClass( id="nested_find_subarray_object_array", value=[{"k": 1}, [2, 3]], array=["a", [{"k": 1}, [2, 3]], None, 4], expected=True, msg="Should find subarray with object and array", ), - InTest( + ArrayTestClass( id="nested_find_subarray_datetime_objectid", value=[datetime(2024, 1, 1), ObjectId("000000000000000000000001")], array=[0, [datetime(2024, 1, 1), ObjectId("000000000000000000000001")], "end"], expected=True, msg="Should find subarray with datetime and objectid", ), - InTest( + ArrayTestClass( id="nested_find_subarray_minkey_maxkey", value=[MinKey(), MaxKey()], array=[[MinKey(), MaxKey()], 1, "a"], @@ -98,43 +98,43 @@ # --------------------------------------------------------------------------- # Success: deeply nested search targets (3-5 levels) # --------------------------------------------------------------------------- -DEEPLY_NESTED_TESTS: list[InTest] = [ - InTest( +DEEPLY_NESTED_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="nested_3_levels", value=[[2, 3], [4, 5]], array=[1, [[2, 3], [4, 5]], "end"], expected=True, msg="Should find 3-level nested array", ), - InTest( + ArrayTestClass( id="nested_4_levels", value=[[[1, 2], 3], 4], array=["a", [[[1, 2], 3], 4], None], expected=True, msg="Should find 4-level nested array", ), - InTest( + ArrayTestClass( id="nested_deep_mixed_bson", value=[[MinKey(), {"a": [Decimal128("1.5")]}], True], array=[0, [[MinKey(), {"a": [Decimal128("1.5")]}], True], "x"], expected=True, msg="Should find deeply nested mixed BSON", ), - InTest( + ArrayTestClass( id="nested_inner_not_outer", value=[2, 3], array=[[1, [2, 3]], [2, 3], 4], expected=True, msg="Should find inner array match", ), - InTest( + ArrayTestClass( id="nested_5_levels", value=[[[[99]]]], array=[[[[[99]]]], "other"], expected=True, msg="Should find 5-level nested array", ), - InTest( + ArrayTestClass( id="nested_deep_not_found", value=[2, 3], array=[[1, [2, 3]], [4, 5]], diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py index 54947ce7e..d937111ce 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py @@ -4,8 +4,8 @@ import pytest -from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.arrays_in_common import ( # noqa: E501 - InTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -18,15 +18,15 @@ # --------------------------------------------------------------------------- # Success: null/missing handling (runs both literal and insert) # --------------------------------------------------------------------------- -NULL_TESTS: list[InTest] = [ - InTest( +NULL_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="null_value_in_array", value=None, array=[1, None, 3], expected=True, msg="Should find null value in array containing null", ), - InTest( + ArrayTestClass( id="null_value_not_in_array", value=None, array=[1, 2, 3], @@ -38,15 +38,15 @@ # --------------------------------------------------------------------------- # Success: missing value handling (literal only, MISSING is a field ref) # --------------------------------------------------------------------------- -LITERAL_ONLY_TESTS: list[InTest] = [ - InTest( +LITERAL_ONLY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="missing_value", value=MISSING, array=[1, 2, 3], expected=False, msg="Should not find missing value in array", ), - InTest( + ArrayTestClass( id="missing_value_null_in_array", value=MISSING, array=[1, None, 3], diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py new file mode 100644 index 000000000..7c26bd337 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py @@ -0,0 +1,28 @@ +""" +Shared test case for array expression operator tests. + +Used across the $arrayElemAt, $arrayToObject, $concatArrays, and $in test files. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class ArrayTestClass(BaseTestCase): + """Test case for array expression operators. + + Attributes: + idx: An index argument (e.g. $arrayElemAt). + arrays: The array input. Holds a single array for $arrayElemAt and + $arrayToObject, or a list of arrays for $concatArrays. + value: A value argument (e.g. $in search value). + array: A single array argument (e.g. $in search array). + """ + + idx: Any = None + arrays: Any = None + value: Any = None + array: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/arrays_in_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/arrays_in_common.py deleted file mode 100644 index ecce25890..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/arrays_in_common.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Shared test infrastructure for $in expression tests. - -Note: This file lives in array/utils/ rather than array/in/utils/ because -"in" is a Python keyword and cannot be used in import paths. -""" - -from dataclasses import dataclass -from typing import Any - -from documentdb_tests.framework.test_case import BaseTestCase - - -@dataclass(frozen=True) -class InTest(BaseTestCase): - """Test case for $in operator.""" - - value: Any = None - array: Any = None From 87aadbdfd22795a96683b60d36eaf77033dd6c07 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 11:46:19 -0700 Subject: [PATCH 03/10] replace IsArrayTest with ArrayTestClass Signed-off-by: Alina (Xi) Li --- .../test_expression_isArray_bson_types.py | 102 +++++++++--------- .../test_expression_isArray_core_behavior.py | 72 +++++++------ .../array/isArray/utils/__init__.py | 0 .../array/isArray/utils/isArray_common.py | 15 --- 4 files changed, 92 insertions(+), 97 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/isArray_common.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py index cb08e86ff..4070637fd 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py @@ -11,8 +11,8 @@ import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp -from documentdb_tests.compatibility.tests.core.operator.expressions.array.isArray.utils.isArray_common import ( # noqa: E501 - IsArrayTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -40,110 +40,110 @@ # --------------------------------------------------------------------------- # Arrays containing specific BSON types → true # --------------------------------------------------------------------------- -BSON_ARRAY_TRUE_TESTS: list[IsArrayTest] = [ - IsArrayTest( +BSON_ARRAY_TRUE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="bindata_array", value=[Binary(b"\x00", 0)], expected=True, msg="Should return true for BinData array", ), - IsArrayTest( + ArrayTestClass( id="timestamp_array", value=[Timestamp(0, 0)], expected=True, msg="Should return true for Timestamp array", ), - IsArrayTest( + ArrayTestClass( id="int64_array", value=[Int64(1)], expected=True, msg="Should return true for Int64 array" ), - IsArrayTest( + ArrayTestClass( id="decimal128_array", value=[Decimal128("1")], expected=True, msg="Should return true for Decimal128 array", ), - IsArrayTest( + ArrayTestClass( id="objectid_array", value=[ObjectId()], expected=True, msg="Should return true for ObjectId array", ), - IsArrayTest( + ArrayTestClass( id="datetime_array", value=[datetime(2024, 1, 1)], expected=True, msg="Should return true for datetime array", ), - IsArrayTest( + ArrayTestClass( id="minkey_array", value=[MinKey()], expected=True, msg="Should return true for MinKey array", ), - IsArrayTest( + ArrayTestClass( id="maxkey_array", value=[MaxKey()], expected=True, msg="Should return true for MaxKey array", ), - IsArrayTest( + ArrayTestClass( id="regex_array", value=[Regex(".*")], expected=True, msg="Should return true for Regex array", ), - IsArrayTest( + ArrayTestClass( id="nan_array", value=[float("nan")], expected=True, msg="Should return true for NaN array" ), - IsArrayTest( + ArrayTestClass( id="inf_array", value=[float("inf")], expected=True, msg="Should return true for Infinity array", ), - IsArrayTest( + ArrayTestClass( id="decimal128_nan_array", value=[Decimal128("NaN")], expected=True, msg="Should return true for Decimal128 NaN array", ), - IsArrayTest( + ArrayTestClass( id="decimal128_inf_array", value=[Decimal128("Infinity")], expected=True, msg="Should return true for Decimal128 Infinity array", ), - IsArrayTest( + ArrayTestClass( id="decimal128_neg_nan_array", value=[Decimal128("-NaN")], expected=True, msg="Should return true for Decimal128 -NaN array", ), - IsArrayTest( + ArrayTestClass( id="decimal128_neg_inf_array", value=[DECIMAL128_NEGATIVE_INFINITY], expected=True, msg="Should return true for Decimal128 -Infinity array", ), - IsArrayTest( + ArrayTestClass( id="neg_inf_array", value=[FLOAT_NEGATIVE_INFINITY], expected=True, msg="Should return true for -Infinity array", ), - IsArrayTest( + ArrayTestClass( id="neg_zero_array", value=[DOUBLE_NEGATIVE_ZERO], expected=True, msg="Should return true for negative zero array", ), - IsArrayTest( + ArrayTestClass( id="decimal128_neg_zero_array", value=[DECIMAL128_NEGATIVE_ZERO], expected=True, msg="Should return true for Decimal128 -0 array", ), - IsArrayTest( + ArrayTestClass( id="nested_mixed_bson_array", value=[ MinKey(), @@ -160,87 +160,91 @@ # --------------------------------------------------------------------------- # Non-array BSON types → false # --------------------------------------------------------------------------- -BSON_FALSE_TESTS: list[IsArrayTest] = [ - IsArrayTest(id="int64", value=Int64(1), expected=False, msg="Should return false for Int64"), - IsArrayTest( +BSON_FALSE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass(id="int64", value=Int64(1), expected=False, msg="Should return false for Int64"), + ArrayTestClass( id="decimal128", value=Decimal128("1"), expected=False, msg="Should return false for Decimal128", ), - IsArrayTest( + ArrayTestClass( id="objectid", value=ObjectId("000000000000000000000001"), expected=False, msg="Should return false for ObjectId", ), - IsArrayTest( + ArrayTestClass( id="datetime", value=datetime(2024, 1, 1), expected=False, msg="Should return false for datetime", ), - IsArrayTest( + ArrayTestClass( id="binary", value=Binary(b"\x01", 0), expected=False, msg="Should return false for Binary" ), - IsArrayTest( + ArrayTestClass( id="regex", value=Regex("^abc"), expected=False, msg="Should return false for Regex" ), - IsArrayTest( + ArrayTestClass( id="timestamp", value=Timestamp(1, 1), expected=False, msg="Should return false for Timestamp", ), - IsArrayTest(id="minkey", value=MinKey(), expected=False, msg="Should return false for MinKey"), - IsArrayTest(id="maxkey", value=MaxKey(), expected=False, msg="Should return false for MaxKey"), + ArrayTestClass( + id="minkey", value=MinKey(), expected=False, msg="Should return false for MinKey" + ), + ArrayTestClass( + id="maxkey", value=MaxKey(), expected=False, msg="Should return false for MaxKey" + ), ] # --------------------------------------------------------------------------- # Special numeric values → false # --------------------------------------------------------------------------- -SPECIAL_NUMERIC_TESTS: list[IsArrayTest] = [ - IsArrayTest(id="nan", value=FLOAT_NAN, expected=False, msg="Should return false for NaN"), - IsArrayTest( +SPECIAL_NUMERIC_TESTS: list[ArrayTestClass] = [ + ArrayTestClass(id="nan", value=FLOAT_NAN, expected=False, msg="Should return false for NaN"), + ArrayTestClass( id="inf", value=FLOAT_INFINITY, expected=False, msg="Should return false for Infinity" ), - IsArrayTest( + ArrayTestClass( id="neg_inf", value=FLOAT_NEGATIVE_INFINITY, expected=False, msg="Should return false for -Infinity", ), - IsArrayTest( + ArrayTestClass( id="neg_zero", value=DOUBLE_NEGATIVE_ZERO, expected=False, msg="Should return false for negative zero", ), - IsArrayTest( + ArrayTestClass( id="decimal128_nan", value=DECIMAL128_NAN, expected=False, msg="Should return false for Decimal128 NaN", ), - IsArrayTest( + ArrayTestClass( id="decimal128_neg_nan", value=Decimal128("-NaN"), expected=False, msg="Should return false for Decimal128 -NaN", ), - IsArrayTest( + ArrayTestClass( id="decimal128_inf", value=DECIMAL128_INFINITY, expected=False, msg="Should return false for Decimal128 Infinity", ), - IsArrayTest( + ArrayTestClass( id="decimal128_neg_inf", value=DECIMAL128_NEGATIVE_INFINITY, expected=False, msg="Should return false for Decimal128 -Infinity", ), - IsArrayTest( + ArrayTestClass( id="decimal128_neg_zero", value=DECIMAL128_NEGATIVE_ZERO, expected=False, @@ -251,26 +255,26 @@ # --------------------------------------------------------------------------- # Boundary values → false # --------------------------------------------------------------------------- -BOUNDARY_TESTS: list[IsArrayTest] = [ - IsArrayTest( +BOUNDARY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="int32_max", value=INT32_MAX, expected=False, msg="Should return false for INT32_MAX" ), - IsArrayTest( + ArrayTestClass( id="int32_min", value=INT32_MIN, expected=False, msg="Should return false for INT32_MIN" ), - IsArrayTest( + ArrayTestClass( id="int64_max", value=INT64_MAX, expected=False, msg="Should return false for INT64_MAX" ), - IsArrayTest( + ArrayTestClass( id="int64_min", value=INT64_MIN, expected=False, msg="Should return false for INT64_MIN" ), - IsArrayTest( + ArrayTestClass( id="decimal128_max", value=DECIMAL128_MAX, expected=False, msg="Should return false for DECIMAL128_MAX", ), - IsArrayTest( + ArrayTestClass( id="decimal128_min", value=DECIMAL128_MIN, expected=False, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py index d16951ae5..22c56a846 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py @@ -7,8 +7,8 @@ import pytest -from documentdb_tests.compatibility.tests.core.operator.expressions.array.isArray.utils.isArray_common import ( # noqa: E501 - IsArrayTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -20,74 +20,74 @@ # --------------------------------------------------------------------------- # Success: arrays → true # --------------------------------------------------------------------------- -IS_ARRAY_TRUE_TESTS: list[IsArrayTest] = [ - IsArrayTest( +IS_ARRAY_TRUE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="simple_array", value=[1, 2, 3], expected=True, msg="Should return true for simple array", ), - IsArrayTest( + ArrayTestClass( id="empty_array", value=[], expected=True, msg="Should return true for empty array", ), - IsArrayTest( + ArrayTestClass( id="single_element", value=[42], expected=True, msg="Should return true for single-element array", ), - IsArrayTest( + ArrayTestClass( id="nested_array", value=[[1, 2], [3, 4]], expected=True, msg="Should return true for nested array", ), - IsArrayTest( + ArrayTestClass( id="mixed_type_array", value=[1, "two", True, None, {"a": 1}], expected=True, msg="Should return true for mixed-type array", ), - IsArrayTest( + ArrayTestClass( id="array_of_objects", value=[{"a": 1}, {"b": 2}], expected=True, msg="Should return true for array of objects", ), - IsArrayTest( + ArrayTestClass( id="array_of_nulls", value=[None, None], expected=True, msg="Should return true for array of nulls", ), - IsArrayTest( + ArrayTestClass( id="string_array", value=["a", "b", "c"], expected=True, msg="Should return true for string array", ), - IsArrayTest( + ArrayTestClass( id="bool_array", value=[True], expected=True, msg="Should return true for bool array", ), - IsArrayTest( + ArrayTestClass( id="large_array_10k", value=list(range(10000)), expected=True, msg="10K element array returns true", ), - IsArrayTest( + ArrayTestClass( id="deeply_nested_array", value=[[[[[[1]]]]]], expected=True, msg="Deeply nested array returns true", ), - IsArrayTest( + ArrayTestClass( id="large_array_of_arrays", value=[[i] for i in range(10000)], expected=True, @@ -98,25 +98,31 @@ # --------------------------------------------------------------------------- # Success: non-arrays → false # --------------------------------------------------------------------------- -IS_ARRAY_FALSE_TESTS: list[IsArrayTest] = [ - IsArrayTest(id="string", value="hello", expected=False, msg="Should return false for string"), - IsArrayTest(id="int", value=42, expected=False, msg="Should return false for int"), - IsArrayTest(id="double", value=3.14, expected=False, msg="Should return false for double"), - IsArrayTest(id="bool_true", value=True, expected=False, msg="Should return false for true"), - IsArrayTest(id="bool_false", value=False, expected=False, msg="Should return false for false"), - IsArrayTest(id="null", value=None, expected=False, msg="Should return false for null"), - IsArrayTest(id="object", value={"a": 1}, expected=False, msg="Should return false for object"), - IsArrayTest( +IS_ARRAY_FALSE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( + id="string", value="hello", expected=False, msg="Should return false for string" + ), + ArrayTestClass(id="int", value=42, expected=False, msg="Should return false for int"), + ArrayTestClass(id="double", value=3.14, expected=False, msg="Should return false for double"), + ArrayTestClass(id="bool_true", value=True, expected=False, msg="Should return false for true"), + ArrayTestClass( + id="bool_false", value=False, expected=False, msg="Should return false for false" + ), + ArrayTestClass(id="null", value=None, expected=False, msg="Should return false for null"), + ArrayTestClass( + id="object", value={"a": 1}, expected=False, msg="Should return false for object" + ), + ArrayTestClass( id="empty_string", value="", expected=False, msg="Should return false for empty string" ), - IsArrayTest( + ArrayTestClass( id="empty_object", value={}, expected=False, msg="Should return false for empty object" ), - IsArrayTest(id="zero", value=0, expected=False, msg="Should return false for zero"), - IsArrayTest( + ArrayTestClass(id="zero", value=0, expected=False, msg="Should return false for zero"), + ArrayTestClass( id="negative_int", value=-123, expected=False, msg="Should return false for negative int" ), - IsArrayTest( + ArrayTestClass( id="negative_double", value=-1.23, expected=False, @@ -127,23 +133,23 @@ # --------------------------------------------------------------------------- # Array-like edge cases → false # --------------------------------------------------------------------------- -ARRAY_LIKE_TESTS: list[IsArrayTest] = [ - IsArrayTest( +ARRAY_LIKE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="string_brackets", value="[]", expected=False, msg="Should return false for string '[]'" ), - IsArrayTest( + ArrayTestClass( id="string_array_repr", value="[1, 2, 3]", expected=False, msg="Should return false for string '[1, 2, 3]'", ), - IsArrayTest( + ArrayTestClass( id="array_like_object", value={"0": "a", "1": "b"}, expected=False, msg="Should return false for array-like object", ), - IsArrayTest( + ArrayTestClass( id="length_object", value={"length": 3}, expected=False, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/isArray_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/isArray_common.py deleted file mode 100644 index 8d3a010d9..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/utils/isArray_common.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Shared test infrastructure for $isArray expression tests. -""" - -from dataclasses import dataclass -from typing import Any - -from documentdb_tests.framework.test_case import BaseTestCase - - -@dataclass(frozen=True) -class IsArrayTest(BaseTestCase): - """Test case for $isArray operator.""" - - value: Any = None From b48f8f83a0e410bad79a35671acc154d25092f40 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 11:56:12 -0700 Subject: [PATCH 04/10] update based on review guide Signed-off-by: Alina (Xi) Li --- .../test_expression_filter_as_errors.py | 10 +++++-- .../test_expression_filter_bson_types.py | 12 --------- .../test_expression_filter_core_behavior.py | 20 -------------- .../filter/test_expression_filter_errors.py | 18 ++----------- .../test_expression_filter_expressions.py | 14 ---------- ...test_expression_filter_structure_errors.py | 8 ------ .../array/in/test_expression_in_bson_types.py | 12 +++------ .../in/test_expression_in_core_behavior.py | 10 ------- .../array/in/test_expression_in_errors.py | 12 ++------- .../in/test_expression_in_expressions.py | 10 ------- .../in/test_expression_in_nested_arrays.py | 16 +++++------- .../in/test_expression_in_null_missing.py | 6 ----- ...test_expression_indexOfArray_bson_types.py | 26 ++++++++----------- ...t_expression_indexOfArray_core_behavior.py | 20 -------------- .../test_expression_indexOfArray_errors.py | 24 ++--------------- ...est_expression_indexOfArray_expressions.py | 6 ----- ...st_expression_indexOfArray_null_missing.py | 8 ------ .../test_expression_isArray_bson_types.py | 18 +++---------- .../test_expression_isArray_core_behavior.py | 8 ------ .../isArray/test_expression_isArray_errors.py | 2 -- .../test_expression_isArray_expressions.py | 12 --------- 21 files changed, 39 insertions(+), 233 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_as_errors.py index ed33c776d..5a0861764 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_as_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_as_errors.py @@ -4,7 +4,7 @@ Tests invalid 'as' types. """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp @@ -70,7 +70,13 @@ ), ExpressionTestCase( id="type_date", - expression={"$filter": {"input": [1, 2, 3], "as": datetime(2024, 1, 1), "cond": True}}, + expression={ + "$filter": { + "input": [1, 2, 3], + "as": datetime(2024, 1, 1, tzinfo=timezone.utc), + "cond": True, + } + }, error_code=FAILED_TO_PARSE_ERROR, msg="as=datetime should error", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_bson_types.py index 34f520d83..187396bfe 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_bson_types.py @@ -34,9 +34,7 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- # BSON types preserved -# --------------------------------------------------------------------------- BSON_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="int64_values", @@ -126,9 +124,7 @@ ), ] -# --------------------------------------------------------------------------- # Mixed BSON types -# --------------------------------------------------------------------------- MIXED_BSON_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="mixed_bson_types", @@ -156,9 +152,7 @@ ), ] -# --------------------------------------------------------------------------- # Special numeric values as elements -# --------------------------------------------------------------------------- SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="infinity_values", @@ -190,9 +184,7 @@ ), ] -# --------------------------------------------------------------------------- # Decimal128 precision preservation -# --------------------------------------------------------------------------- DECIMAL128_PRECISION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="decimal128_trailing_zeros", @@ -210,9 +202,7 @@ ), ] -# --------------------------------------------------------------------------- # BSON type filtering with $eq condition -# --------------------------------------------------------------------------- BSON_FILTER_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="filter_int64", @@ -310,9 +300,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_BSON_TESTS = ( BSON_TYPE_TESTS + MIXED_BSON_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_core_behavior.py index 1a6acd11e..0c12e1b99 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_core_behavior.py @@ -19,9 +19,7 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import INT32_MAX -# --------------------------------------------------------------------------- # Success: basic filtering -# --------------------------------------------------------------------------- BASIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="gt_filter", @@ -123,9 +121,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: nested arrays (filter does not recurse) -# --------------------------------------------------------------------------- NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_arrays_by_size", @@ -143,9 +139,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: elements with null -# --------------------------------------------------------------------------- NULL_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="filter_out_nulls", @@ -163,9 +157,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: objects as elements -# --------------------------------------------------------------------------- OBJECT_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="filter_objects_by_nested_field", @@ -190,9 +182,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: limit parameter -# --------------------------------------------------------------------------- LIMIT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="limit_1", @@ -284,9 +274,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: type-based filtering -# --------------------------------------------------------------------------- TYPE_FILTER_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="filter_by_type", @@ -304,9 +292,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: cond true or false -# --------------------------------------------------------------------------- COND_FALSY_TRUTHY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="cond_nonzero_truthy", @@ -360,9 +346,7 @@ ] -# --------------------------------------------------------------------------- # Success: type strict equality -# --------------------------------------------------------------------------- TYPE_STRICT_EQUALITY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="false_vs_zero", @@ -394,9 +378,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: numeric equivalence -# --------------------------------------------------------------------------- NUMERIC_EQUIVALENCE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="numeric_equivalence_one", @@ -414,9 +396,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( BASIC_TESTS + NESTED_ARRAY_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_errors.py index 100fb100b..eeb90e8f4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_errors.py @@ -6,7 +6,7 @@ Note: $filter propagates null — null input returns null (tested in core_behavior). """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp @@ -42,9 +42,7 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- # Error: non-array input — standard BSON types -# --------------------------------------------------------------------------- NOT_ARRAY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="string_input", @@ -126,7 +124,7 @@ ExpressionTestCase( id="datetime_input", expression={"$filter": {"input": "$arr", "cond": True}}, - doc={"arr": datetime(2024, 1, 1)}, + doc={"arr": datetime(2024, 1, 1, tzinfo=timezone.utc)}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, msg="Should reject datetime input", ), @@ -167,9 +165,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: special float/Decimal128 values -# --------------------------------------------------------------------------- SPECIAL_NUMERIC_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nan_input", @@ -236,9 +232,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: numeric boundary values -# --------------------------------------------------------------------------- BOUNDARY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="int32_max_input", @@ -284,9 +278,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: invalid limit types -# --------------------------------------------------------------------------- INVALID_LIMIT_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="string_limit", @@ -318,9 +310,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: invalid limit numeric values -# --------------------------------------------------------------------------- INVALID_LIMIT_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="zero_limit", @@ -422,9 +412,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: cond evaluation errors -# --------------------------------------------------------------------------- COND_EVALUATION_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="cond_divide_by_zero", @@ -435,9 +423,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( NOT_ARRAY_ERROR_TESTS + SPECIAL_NUMERIC_ERROR_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_expressions.py index e21ec9be7..a6334924d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_expressions.py @@ -17,9 +17,7 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Field path lookups -# --------------------------------------------------------------------------- FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_field_path", @@ -65,9 +63,7 @@ ), ] -# --------------------------------------------------------------------------- # $let and system variables -# --------------------------------------------------------------------------- LET_AND_VARIABLE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="let_variable", @@ -97,9 +93,7 @@ ), ] -# --------------------------------------------------------------------------- # Null/missing via expression -# --------------------------------------------------------------------------- NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_field", @@ -117,9 +111,7 @@ ), ] -# --------------------------------------------------------------------------- # Nested $filter -# --------------------------------------------------------------------------- NESTED_FILTER_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="filter_then_filter", @@ -136,9 +128,7 @@ ] -# --------------------------------------------------------------------------- # Limit with field reference -# --------------------------------------------------------------------------- LIMIT_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="limit_from_field", @@ -149,9 +139,7 @@ ), ] -# --------------------------------------------------------------------------- # Literal array input (not field path) -# --------------------------------------------------------------------------- LITERAL_INPUT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="literal_array_input", @@ -162,9 +150,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_EXPR_TESTS = ( FIELD_LOOKUP_TESTS + LET_AND_VARIABLE_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_structure_errors.py index 7f588a403..dbf39baa9 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_structure_errors.py @@ -22,9 +22,7 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Error: non-object argument -# --------------------------------------------------------------------------- NON_OBJECT_ARG_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="null_arg", @@ -58,9 +56,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: unknown fields -# --------------------------------------------------------------------------- UNKNOWN_FIELD_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="extra_unknown", @@ -76,9 +72,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: missing required fields -# --------------------------------------------------------------------------- MISSING_REQUIRED_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_input", @@ -100,9 +94,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_STRUCTURE_TESTS = NON_OBJECT_ARG_TESTS + UNKNOWN_FIELD_TESTS + MISSING_REQUIRED_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py index 04850fe61..ebb01d931 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py @@ -4,7 +4,7 @@ Tests searching for various BSON types and cross-type numeric matching. """ -from datetime import datetime +from datetime import datetime, timezone from uuid import UUID import pytest @@ -28,9 +28,7 @@ FLOAT_NEGATIVE_INFINITY, ) -# --------------------------------------------------------------------------- # Success: search for various BSON types -# --------------------------------------------------------------------------- BSON_TYPE_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="bson_int64", @@ -48,8 +46,8 @@ ), ArrayTestClass( id="bson_datetime", - value=datetime(2024, 1, 1), - array=[datetime(2024, 1, 1), 1], + value=datetime(2024, 1, 1, tzinfo=timezone.utc), + array=[datetime(2024, 1, 1, tzinfo=timezone.utc), 1], expected=True, msg="Should find datetime in array", ), @@ -178,9 +176,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: numeric type equivalence -# --------------------------------------------------------------------------- NUMERIC_EQUIVALENCE_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="int_in_doubles", @@ -233,9 +229,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = BSON_TYPE_TESTS + NUMERIC_EQUIVALENCE_TESTS TEST_SUBSET_FOR_LITERAL = [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py index 6ed013129..ad3f591b8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py @@ -16,9 +16,7 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Success: value found in array → True -# --------------------------------------------------------------------------- FOUND_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="found_int", value=2, array=[1, 2, 3], expected=True, msg="Should find int in array" @@ -87,9 +85,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: value not found → False -# --------------------------------------------------------------------------- NOT_FOUND_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="not_found_int", @@ -149,9 +145,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: mixed types in array -# --------------------------------------------------------------------------- MIXED_TYPE_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="mixed_find_string", @@ -183,9 +177,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: large array -# --------------------------------------------------------------------------- _LARGE_ARRAY_SIZE = 20_000 _LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) @@ -220,9 +212,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = FOUND_TESTS + NOT_FOUND_TESTS + MIXED_TYPE_TESTS + LARGE_ARRAY_TESTS TEST_SUBSET_FOR_LITERAL = [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py index 99ee2031d..715e21461 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py @@ -4,7 +4,7 @@ Tests non-array second argument and wrong arity errors. """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp @@ -24,9 +24,7 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import MISSING -# --------------------------------------------------------------------------- # Error: second argument not an array (runs both literal and insert) -# --------------------------------------------------------------------------- NOT_ARRAY_ERROR_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="string_as_array", @@ -94,7 +92,7 @@ ArrayTestClass( id="datetime_as_array", value=1, - array=datetime(2024, 1, 1), + array=datetime(2024, 1, 1, tzinfo=timezone.utc), error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject datetime as array arg", ), @@ -142,9 +140,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: missing as array (literal only, MISSING is a field ref) -# --------------------------------------------------------------------------- LITERAL_ONLY_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="missing_as_array", @@ -155,9 +151,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- TEST_SUBSET_FOR_LITERAL = [ NOT_ARRAY_ERROR_TESTS[0], # string_as_array NOT_ARRAY_ERROR_TESTS[-1], # null_as_array @@ -184,9 +178,7 @@ def test_in_insert(collection, test): ) -# --------------------------------------------------------------------------- # Error: wrong arity -# --------------------------------------------------------------------------- ARITY_ERROR_TESTS = [ pytest.param({"$in": []}, id="zero_args"), pytest.param({"$in": [[1, 2, 3]]}, id="one_arg"), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py index 564847e6e..7aa737a0a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py @@ -15,9 +15,7 @@ from documentdb_tests.framework.error_codes import EXPRESSION_IN_NOT_ARRAY_ERROR -# --------------------------------------------------------------------------- # Nested expressions -# --------------------------------------------------------------------------- @pytest.mark.parametrize( "expression,expected", [ @@ -32,9 +30,7 @@ def test_in_nested_expression(collection, expression, expected): assert_expression_result(result, expected=expected) -# --------------------------------------------------------------------------- # Field path lookups -# --------------------------------------------------------------------------- @pytest.mark.parametrize( "document,value,array_ref,expected", [ @@ -50,9 +46,7 @@ def test_in_field_lookup(collection, document, value, array_ref, expected): assert_expression_result(result, expected=expected) -# --------------------------------------------------------------------------- # Field path: path through array of objects -# --------------------------------------------------------------------------- def test_in_path_through_array_of_objects(collection): """Test $in where field path traverses array of objects.""" result = execute_expression_with_insert( @@ -61,18 +55,14 @@ def test_in_path_through_array_of_objects(collection): assert_expression_result(result, expected=True) -# --------------------------------------------------------------------------- # Non-existent field as array → error (missing resolves to non-array) -# --------------------------------------------------------------------------- def test_in_nonexistent_array_field(collection): """Test $in where array field does not exist (resolves to missing).""" result = execute_expression_with_insert(collection, {"$in": [1, "$nonexistent"]}, {"other": 1}) assert_expression_result(result, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR) -# --------------------------------------------------------------------------- # Non-existent field as value (resolves to missing/null) -# --------------------------------------------------------------------------- @pytest.mark.parametrize( "document,expected", [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py index 168ba1cc2..60e2e0c96 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py @@ -4,7 +4,7 @@ Tests searching for complex elements in nested mixed arrays and deeply nested structures. """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Decimal128, MaxKey, MinKey, ObjectId @@ -19,9 +19,7 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Success: nested mixed arrays as search targets -# --------------------------------------------------------------------------- NESTED_MIXED_ARRAY_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="nested_find_object_in_mixed", @@ -81,8 +79,12 @@ ), ArrayTestClass( id="nested_find_subarray_datetime_objectid", - value=[datetime(2024, 1, 1), ObjectId("000000000000000000000001")], - array=[0, [datetime(2024, 1, 1), ObjectId("000000000000000000000001")], "end"], + value=[datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], + array=[ + 0, + [datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], + "end", + ], expected=True, msg="Should find subarray with datetime and objectid", ), @@ -95,9 +97,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: deeply nested search targets (3-5 levels) -# --------------------------------------------------------------------------- DEEPLY_NESTED_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="nested_3_levels", @@ -143,9 +143,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = NESTED_MIXED_ARRAY_TESTS + DEEPLY_NESTED_TESTS TEST_SUBSET_FOR_LITERAL = [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py index d937111ce..639fc3d50 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py @@ -15,9 +15,7 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import MISSING -# --------------------------------------------------------------------------- # Success: null/missing handling (runs both literal and insert) -# --------------------------------------------------------------------------- NULL_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="null_value_in_array", @@ -35,9 +33,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: missing value handling (literal only, MISSING is a field ref) -# --------------------------------------------------------------------------- LITERAL_ONLY_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="missing_value", @@ -55,9 +51,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- TEST_SUBSET_FOR_LITERAL = [ NULL_TESTS[0], # null_value_in_array NULL_TESTS[1], # null_value_not_in_array diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_bson_types.py index 73d69bb0b..5c19daf72 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_bson_types.py @@ -5,7 +5,7 @@ complex nested mixed arrays. """ -from datetime import datetime +from datetime import datetime, timezone from uuid import UUID import pytest @@ -31,9 +31,7 @@ FLOAT_NEGATIVE_INFINITY, ) -# --------------------------------------------------------------------------- # Success: search for various BSON types -# --------------------------------------------------------------------------- BSON_TYPE_SEARCH_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="search_int64", @@ -51,8 +49,8 @@ ), IndexOfArrayTest( id="search_datetime", - array=[datetime(2024, 1, 1), 1], - search=datetime(2024, 1, 1), + array=[datetime(2024, 1, 1, tzinfo=timezone.utc), 1], + search=datetime(2024, 1, 1, tzinfo=timezone.utc), expected=0, msg="Should find datetime value", ), @@ -174,9 +172,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: numeric type equivalence in search -# --------------------------------------------------------------------------- NUMERIC_EQUIVALENCE_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="int_matches_double", @@ -222,9 +218,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: nested mixed arrays -# --------------------------------------------------------------------------- NESTED_MIXED_ARRAY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="nested_find_object_in_mixed", @@ -284,8 +278,8 @@ ), IndexOfArrayTest( id="nested_find_datetime_in_mixed", - array=["a", datetime(2024, 1, 1), 3, [4]], - search=datetime(2024, 1, 1), + array=["a", datetime(2024, 1, 1, tzinfo=timezone.utc), 3, [4]], + search=datetime(2024, 1, 1, tzinfo=timezone.utc), expected=1, msg="Should find datetime in mixed array", ), @@ -333,8 +327,12 @@ ), IndexOfArrayTest( id="nested_find_subarray_datetime_objectid", - array=[0, [datetime(2024, 1, 1), ObjectId("000000000000000000000001")], "end"], - search=[datetime(2024, 1, 1), ObjectId("000000000000000000000001")], + array=[ + 0, + [datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], + "end", + ], + search=[datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], expected=1, msg="Should find subarray with datetime and objectid", ), @@ -389,9 +387,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = BSON_TYPE_SEARCH_TESTS + NUMERIC_EQUIVALENCE_TESTS + NESTED_MIXED_ARRAY_TESTS TEST_SUBSET_FOR_LITERAL = [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py index b1756a27c..9dbb447c4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py @@ -20,9 +20,7 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Success: basic search — value found -# --------------------------------------------------------------------------- BASIC_FOUND_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="found_first", @@ -110,9 +108,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: value not found → -1 -# --------------------------------------------------------------------------- NOT_FOUND_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="not_found_int", @@ -168,9 +164,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: with start index -# --------------------------------------------------------------------------- START_INDEX_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="start_skips_first", @@ -255,9 +249,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: with start and end index -# --------------------------------------------------------------------------- START_END_INDEX_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="range_found", @@ -342,9 +334,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: first occurrence from start with multiple duplicates -# --------------------------------------------------------------------------- FIRST_FROM_START_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="dup_skip_to_third_occurrence", @@ -364,9 +354,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: detailed range semantics -# --------------------------------------------------------------------------- RANGE_SEMANTICS_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="end_exclusive_includes_before_boundary", @@ -432,9 +420,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: degenerate and single-element edge cases -# --------------------------------------------------------------------------- DEGENERATE_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="single_not_found", @@ -493,9 +479,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: mixed types in array -# --------------------------------------------------------------------------- MIXED_TYPE_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="mixed_find_string", @@ -520,9 +504,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: large array -# --------------------------------------------------------------------------- _LARGE_ARRAY_SIZE = 20_000 _LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) @@ -565,9 +547,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( BASIC_FOUND_TESTS + NOT_FOUND_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py index 36f5e2d1b..9a51759b8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py @@ -5,7 +5,7 @@ boundary values, negative zero, and wrong arity errors. """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp @@ -34,9 +34,7 @@ MISSING, ) -# --------------------------------------------------------------------------- # Success: boundary values for start/end indices -# --------------------------------------------------------------------------- BOUNDARY_INDEX_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="start_int32_max", @@ -91,9 +89,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: negative zero as start index -# --------------------------------------------------------------------------- NEGATIVE_ZERO_START_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="double_neg_zero_start", @@ -122,9 +118,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: first argument not an array (and not null) -# --------------------------------------------------------------------------- NOT_ARRAY_ERROR_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="string_as_array", @@ -191,7 +185,7 @@ ), IndexOfArrayTest( id="datetime_as_array", - array=datetime(2024, 1, 1), + array=datetime(2024, 1, 1, tzinfo=timezone.utc), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="Should reject datetime as array", @@ -233,9 +227,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: start index not integral -# --------------------------------------------------------------------------- START_NOT_INTEGRAL_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="start_fractional_double", @@ -327,9 +319,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: end index not integral -# --------------------------------------------------------------------------- END_NOT_INTEGRAL_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="end_fractional_double", @@ -432,9 +422,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: negative start index -# --------------------------------------------------------------------------- START_NEGATIVE_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="start_neg_one", @@ -478,9 +466,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: negative end index -# --------------------------------------------------------------------------- END_NEGATIVE_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="end_neg_one", @@ -529,9 +515,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( BOUNDARY_INDEX_TESTS + NEGATIVE_ZERO_START_TESTS @@ -588,9 +572,7 @@ def test_indexOfArray_insert(collection, test): ) -# --------------------------------------------------------------------------- # Error: wrong arity -# --------------------------------------------------------------------------- ARITY_ERROR_TESTS = [ pytest.param({"$indexOfArray": []}, id="zero_args"), pytest.param({"$indexOfArray": [[1, 2, 3]]}, id="one_arg"), @@ -605,11 +587,9 @@ def test_indexOfArray_arity_error(collection, expr): assert_expression_result(result, error_code=EXPRESSION_ARITY_ERROR) -# --------------------------------------------------------------------------- # Error: null as literal start/end index # Standalone test because end=None in IndexOfArrayTest means "no end argument", # so null-as-end cannot be expressed via the dataclass. -# --------------------------------------------------------------------------- def test_indexOfArray_null_end(collection): """Test $indexOfArray with null as end index errors.""" result = execute_expression(collection, {"$indexOfArray": [[1, 2, 3], 1, 0, None]}) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py index b264d9543..54b63e199 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py @@ -14,9 +14,7 @@ ) -# --------------------------------------------------------------------------- # Nested expressions -# --------------------------------------------------------------------------- @pytest.mark.parametrize( "expression,expected", [ @@ -65,9 +63,7 @@ def test_indexOfArray_nested_expression(collection, expression, expected): assert_expression_result(result, expected=expected) -# --------------------------------------------------------------------------- # Field path lookups -# --------------------------------------------------------------------------- @pytest.mark.parametrize( "document,array_ref,search,expected", [ @@ -85,9 +81,7 @@ def test_indexOfArray_field_lookup(collection, document, array_ref, search, expe assert_expression_result(result, expected=expected) -# --------------------------------------------------------------------------- # Field path: path through array of objects -# --------------------------------------------------------------------------- def test_indexOfArray_path_through_array_of_objects(collection): """Test $indexOfArray where field path traverses array of objects.""" result = execute_expression_with_insert( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_null_missing.py index af5aa3aa1..96d32a0b0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_null_missing.py @@ -17,9 +17,7 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import MISSING -# --------------------------------------------------------------------------- # Success: null/missing array → null (runs both literal and insert) -# --------------------------------------------------------------------------- NULL_ARRAY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="null_array", @@ -38,9 +36,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: null/missing as search value (runs both literal and insert) -# --------------------------------------------------------------------------- NULL_SEARCH_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="null_value_in_array", @@ -58,9 +54,7 @@ ), ] -# --------------------------------------------------------------------------- # Literal only: MISSING field refs -# --------------------------------------------------------------------------- LITERAL_ONLY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="missing_array", @@ -85,9 +79,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = NULL_ARRAY_TESTS + NULL_SEARCH_TESTS TEST_SUBSET_FOR_LITERAL = [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py index 4070637fd..ad84ecf36 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py @@ -6,7 +6,7 @@ and boundary values. """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp @@ -37,9 +37,7 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- # Arrays containing specific BSON types → true -# --------------------------------------------------------------------------- BSON_ARRAY_TRUE_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="bindata_array", @@ -70,7 +68,7 @@ ), ArrayTestClass( id="datetime_array", - value=[datetime(2024, 1, 1)], + value=[datetime(2024, 1, 1, tzinfo=timezone.utc)], expected=True, msg="Should return true for datetime array", ), @@ -149,7 +147,7 @@ MinKey(), {"a": [Decimal128("1.5")]}, Int64(1), - datetime(2024, 1, 1), + datetime(2024, 1, 1, tzinfo=timezone.utc), Binary(b"\x01", 0), ], expected=True, @@ -157,9 +155,7 @@ ), ] -# --------------------------------------------------------------------------- # Non-array BSON types → false -# --------------------------------------------------------------------------- BSON_FALSE_TESTS: list[ArrayTestClass] = [ ArrayTestClass(id="int64", value=Int64(1), expected=False, msg="Should return false for Int64"), ArrayTestClass( @@ -176,7 +172,7 @@ ), ArrayTestClass( id="datetime", - value=datetime(2024, 1, 1), + value=datetime(2024, 1, 1, tzinfo=timezone.utc), expected=False, msg="Should return false for datetime", ), @@ -200,9 +196,7 @@ ), ] -# --------------------------------------------------------------------------- # Special numeric values → false -# --------------------------------------------------------------------------- SPECIAL_NUMERIC_TESTS: list[ArrayTestClass] = [ ArrayTestClass(id="nan", value=FLOAT_NAN, expected=False, msg="Should return false for NaN"), ArrayTestClass( @@ -252,9 +246,7 @@ ), ] -# --------------------------------------------------------------------------- # Boundary values → false -# --------------------------------------------------------------------------- BOUNDARY_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="int32_max", value=INT32_MAX, expected=False, msg="Should return false for INT32_MAX" @@ -282,9 +274,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_BSON_TESTS = BSON_ARRAY_TRUE_TESTS + BSON_FALSE_TESTS + SPECIAL_NUMERIC_TESTS + BOUNDARY_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py index 22c56a846..7349ca2aa 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py @@ -17,9 +17,7 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Success: arrays → true -# --------------------------------------------------------------------------- IS_ARRAY_TRUE_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="simple_array", @@ -95,9 +93,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: non-arrays → false -# --------------------------------------------------------------------------- IS_ARRAY_FALSE_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="string", value="hello", expected=False, msg="Should return false for string" @@ -130,9 +126,7 @@ ), ] -# --------------------------------------------------------------------------- # Array-like edge cases → false -# --------------------------------------------------------------------------- ARRAY_LIKE_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="string_brackets", value="[]", expected=False, msg="Should return false for string '[]'" @@ -157,9 +151,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- INSERT_TESTS = IS_ARRAY_TRUE_TESTS + IS_ARRAY_FALSE_TESTS + ARRAY_LIKE_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_errors.py index d10723cb9..62e865714 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_errors.py @@ -12,9 +12,7 @@ ) from documentdb_tests.framework.error_codes import EXPRESSION_TYPE_MISMATCH_ERROR -# --------------------------------------------------------------------------- # Arity errors -# --------------------------------------------------------------------------- ARITY_ERROR_TESTS = [ pytest.param({"$isArray": []}, id="zero_args"), pytest.param({"$isArray": [1, 2]}, id="two_args"), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py index c1144c496..ce2472287 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py @@ -27,9 +27,7 @@ ), ] -# --------------------------------------------------------------------------- # Field path lookups -# --------------------------------------------------------------------------- FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_array", @@ -54,9 +52,7 @@ ), ] -# --------------------------------------------------------------------------- # Composite array paths -# --------------------------------------------------------------------------- COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="composite_array_path", @@ -81,9 +77,7 @@ ), ] -# --------------------------------------------------------------------------- # Deep composite array traversal -# --------------------------------------------------------------------------- DEEP_COMPOSITE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="array_at_leaf", @@ -101,9 +95,7 @@ ), ] -# --------------------------------------------------------------------------- # Null and missing handling -# --------------------------------------------------------------------------- NULL_MISSING_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_field", @@ -128,9 +120,7 @@ ), ] -# --------------------------------------------------------------------------- # System variables -# --------------------------------------------------------------------------- SYSTEM_VAR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="root_variable", @@ -155,9 +145,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_EXPR_TESTS = ( FIELD_LOOKUP_TESTS + COMPOSITE_PATH_TESTS From a4c7de358fc594c96749c79798e66672d307a889 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 12:09:01 -0700 Subject: [PATCH 05/10] add missing tests Signed-off-by: Alina (Xi) Li --- .../array/in/test_expression_in_bson_types.py | 36 ++++++++++++++++++- ...t_expression_indexOfArray_core_behavior.py | 26 ++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py index ebb01d931..85abcd6b2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py @@ -23,6 +23,8 @@ DECIMAL128_INFINITY, DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DOUBLE_NEGATIVE_ZERO, FLOAT_INFINITY, FLOAT_NAN, FLOAT_NEGATIVE_INFINITY, @@ -229,8 +231,40 @@ ), ] +# Negative zero equivalence with positive zero +NEGATIVE_ZERO_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( + id="double_neg_zero_matches_zero", + value=DOUBLE_NEGATIVE_ZERO, + array=[0, 1, 2], + expected=True, + msg="$in should treat -0.0 as equivalent to 0", + ), + ArrayTestClass( + id="zero_matches_double_neg_zero_in_array", + value=0, + array=[DOUBLE_NEGATIVE_ZERO, 1, 2], + expected=True, + msg="$in should find 0 in array containing -0.0", + ), + ArrayTestClass( + id="decimal128_neg_zero_matches_zero", + value=DECIMAL128_NEGATIVE_ZERO, + array=[0, 1, 2], + expected=True, + msg="$in should treat Decimal128 -0 as equivalent to 0", + ), + ArrayTestClass( + id="zero_matches_decimal128_neg_zero_in_array", + value=0, + array=[DECIMAL128_NEGATIVE_ZERO, 1, 2], + expected=True, + msg="$in should find 0 in array containing Decimal128 -0", + ), +] + # Aggregate and test -ALL_TESTS = BSON_TYPE_TESTS + NUMERIC_EQUIVALENCE_TESTS +ALL_TESTS = BSON_TYPE_TESTS + NUMERIC_EQUIVALENCE_TESTS + NEGATIVE_ZERO_TESTS TEST_SUBSET_FOR_LITERAL = [ BSON_TYPE_TESTS[0], # bson_int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py index 9dbb447c4..c2c63685b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py @@ -547,6 +547,31 @@ ), ] +# Negative zero treated as equivalent to positive zero in search +NEGATIVE_ZERO_SEARCH_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="search_double_neg_zero_in_zeros", + array=[0, 1, 2], + search=-0.0, + expected=0, + msg="$indexOfArray should find -0.0 at index of 0", + ), + IndexOfArrayTest( + id="search_zero_finds_neg_zero", + array=[-0.0, 1, 2], + search=0, + expected=0, + msg="$indexOfArray should find 0 matching -0.0 in array", + ), + IndexOfArrayTest( + id="search_decimal128_neg_zero", + array=[0, 1, 2], + search=Decimal128("-0"), + expected=0, + msg="$indexOfArray should find Decimal128 -0 at index of 0", + ), +] + # Aggregate and test ALL_TESTS = ( BASIC_FOUND_TESTS @@ -558,6 +583,7 @@ + DEGENERATE_TESTS + MIXED_TYPE_TESTS + LARGE_ARRAY_TESTS + + NEGATIVE_ZERO_SEARCH_TESTS ) TEST_SUBSET_FOR_LITERAL = [ From 014f09e09d6b75a3f65305dcbef683016d0c8387 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 12:17:42 -0700 Subject: [PATCH 06/10] regroup tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved BOUNDARY_INDEX_TESTS (4 success cases) and NEGATIVE_ZERO_START_TESTS (3 success cases) from test_expression_indexOfArray_errors.py → test_expression_indexOfArray_core_behavior.py Signed-off-by: Alina (Xi) Li --- ...t_expression_indexOfArray_core_behavior.py | 70 ++++++++++++++++++ .../test_expression_indexOfArray_errors.py | 72 +------------------ 2 files changed, 73 insertions(+), 69 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py index c2c63685b..21a8ac34c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py @@ -19,6 +19,7 @@ execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DECIMAL128_NEGATIVE_ZERO, INT32_MAX # Success: basic search — value found BASIC_FOUND_TESTS: list[IndexOfArrayTest] = [ @@ -572,6 +573,73 @@ ), ] +# Boundary values for start/end indices +BOUNDARY_INDEX_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="start_int32_max", + array=[1, 2, 3], + search=1, + start=INT32_MAX, + expected=-1, + msg="Should return -1 with INT32_MAX start", + ), + IndexOfArrayTest( + id="end_int32_max", + array=[1, 2, 3], + search=2, + start=0, + end=INT32_MAX, + expected=1, + msg="Should find with INT32_MAX end", + ), + IndexOfArrayTest( + id="start_and_end_int32_max", + array=[1, 2, 3], + search=1, + start=INT32_MAX, + end=INT32_MAX, + expected=-1, + msg="Should return -1 with both INT32_MAX", + ), + IndexOfArrayTest( + id="start_int32_max_minus_1", + array=[1, 2, 3], + search=1, + start=INT32_MAX - 1, + expected=-1, + msg="Should return -1 with INT32_MAX-1 start", + ), +] + +# Negative zero as start/end index treated as 0 +NEGATIVE_ZERO_INDEX_TESTS: list[IndexOfArrayTest] = [ + IndexOfArrayTest( + id="double_neg_zero_start", + array=[10, 20, 30], + search=10, + start=-0.0, + expected=0, + msg="Should treat -0.0 start as 0", + ), + IndexOfArrayTest( + id="decimal128_neg_zero_start", + array=[10, 20, 30], + search=10, + start=DECIMAL128_NEGATIVE_ZERO, + expected=0, + msg="Should treat decimal128 -0 start as 0", + ), + IndexOfArrayTest( + id="double_neg_zero_end", + array=[10, 20, 30], + search=10, + start=0, + end=-0.0, + expected=-1, + msg="Should treat -0.0 end as 0", + ), +] + # Aggregate and test ALL_TESTS = ( BASIC_FOUND_TESTS @@ -584,6 +652,8 @@ + MIXED_TYPE_TESTS + LARGE_ARRAY_TESTS + NEGATIVE_ZERO_SEARCH_TESTS + + BOUNDARY_INDEX_TESTS + + NEGATIVE_ZERO_INDEX_TESTS ) TEST_SUBSET_FOR_LITERAL = [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py index 9a51759b8..d74c3cada 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py @@ -28,22 +28,12 @@ ) from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( - DECIMAL128_NEGATIVE_ZERO, - INT32_MAX, INT64_MAX, MISSING, ) -# Success: boundary values for start/end indices -BOUNDARY_INDEX_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( - id="start_int32_max", - array=[1, 2, 3], - search=1, - start=INT32_MAX, - expected=-1, - msg="Should return -1 with INT32_MAX start", - ), +# Error: INT64_MAX start/end index (not representable as int32) +BOUNDARY_ERROR_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="start_int64_max", array=[1, 2, 3], @@ -52,15 +42,6 @@ error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="Should reject INT64_MAX start", ), - IndexOfArrayTest( - id="end_int32_max", - array=[1, 2, 3], - search=2, - start=0, - end=INT32_MAX, - expected=1, - msg="Should find with INT32_MAX end", - ), IndexOfArrayTest( id="end_int64_max", array=[1, 2, 3], @@ -70,52 +51,6 @@ error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="Should reject INT64_MAX end", ), - IndexOfArrayTest( - id="start_and_end_int32_max", - array=[1, 2, 3], - search=1, - start=INT32_MAX, - end=INT32_MAX, - expected=-1, - msg="Should return -1 with both INT32_MAX", - ), - IndexOfArrayTest( - id="start_int32_max_minus_1", - array=[1, 2, 3], - search=1, - start=INT32_MAX - 1, - expected=-1, - msg="Should return -1 with INT32_MAX-1 start", - ), -] - -# Success: negative zero as start index -NEGATIVE_ZERO_START_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( - id="double_neg_zero_start", - array=[10, 20, 30], - search=10, - start=-0.0, - expected=0, - msg="Should treat -0.0 start as 0", - ), - IndexOfArrayTest( - id="decimal128_neg_zero_start", - array=[10, 20, 30], - search=10, - start=DECIMAL128_NEGATIVE_ZERO, - expected=0, - msg="Should treat decimal128 -0 start as 0", - ), - IndexOfArrayTest( - id="double_neg_zero_end", - array=[10, 20, 30], - search=10, - start=0, - end=-0.0, - expected=-1, - msg="Should treat -0.0 end as 0", - ), ] # Error: first argument not an array (and not null) @@ -517,8 +452,7 @@ # Aggregate and test ALL_TESTS = ( - BOUNDARY_INDEX_TESTS - + NEGATIVE_ZERO_START_TESTS + BOUNDARY_ERROR_TESTS + NOT_ARRAY_ERROR_TESTS + START_NOT_INTEGRAL_TESTS + END_NOT_INTEGRAL_TESTS From ae32627e995e7360bd9a31ed2a948b4208f62c93 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 12:20:03 -0700 Subject: [PATCH 07/10] rename tests and remove dups Signed-off-by: Alina (Xi) Li --- .../array/in/test_expression_in_expressions.py | 11 +---------- .../test_expression_indexOfArray_expressions.py | 9 --------- .../isArray/test_expression_isArray_expressions.py | 4 ++-- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py index 7aa737a0a..bdf92b5e6 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py @@ -46,15 +46,6 @@ def test_in_field_lookup(collection, document, value, array_ref, expected): assert_expression_result(result, expected=expected) -# Field path: path through array of objects -def test_in_path_through_array_of_objects(collection): - """Test $in where field path traverses array of objects.""" - result = execute_expression_with_insert( - collection, {"$in": [20, "$a.b"]}, {"a": [{"b": 10}, {"b": 20}]} - ) - assert_expression_result(result, expected=True) - - # Non-existent field as array → error (missing resolves to non-array) def test_in_nonexistent_array_field(collection): """Test $in where array field does not exist (resolves to missing).""" @@ -69,7 +60,7 @@ def test_in_nonexistent_array_field(collection): ({"arr": [1, None, 3]}, False), ({"arr": [1, 2, 3]}, False), ], - ids=["null_in_array", "null_not_in_array"], + ids=["array_contains_null", "array_without_null"], ) def test_in_nonexistent_value_field(collection, document, expected): """Test $in where value field does not exist (missing vs null).""" diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py index 54b63e199..d367d62cd 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py @@ -81,15 +81,6 @@ def test_indexOfArray_field_lookup(collection, document, array_ref, search, expe assert_expression_result(result, expected=expected) -# Field path: path through array of objects -def test_indexOfArray_path_through_array_of_objects(collection): - """Test $indexOfArray where field path traverses array of objects.""" - result = execute_expression_with_insert( - collection, {"$indexOfArray": ["$a.b", 20]}, {"a": [{"b": 10}, {"b": 20}]} - ) - assert_expression_result(result, expected=1) - - def test_indexOfArray_composite_array_as_array(collection): """Test $indexOfArray with composite array from $x.y as the array argument.""" result = execute_expression_with_insert( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py index ce2472287..ab5e1680f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py @@ -30,14 +30,14 @@ # Field path lookups FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_array", + id="nested_field_array", expression={"$isArray": "$a.b"}, doc={"a": {"b": [1, 2]}}, expected=True, msg="Nested array field", ), ExpressionTestCase( - id="deeply_nested_array", + id="deeply_nested_field_array", expression={"$isArray": "$a.b.c"}, doc={"a": {"b": {"c": [1]}}}, expected=True, From a9989ebd475aff6a3cc22c64264d6327cb464832 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 15:37:47 -0700 Subject: [PATCH 08/10] in-line values Signed-off-by: Alina (Xi) Li --- .../in/test_expression_in_core_behavior.py | 14 +++++----- ...t_expression_indexOfArray_core_behavior.py | 26 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py index ad3f591b8..2abfbbce0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py @@ -178,35 +178,33 @@ ] # Success: large array -_LARGE_ARRAY_SIZE = 20_000 -_LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) LARGE_ARRAY_TESTS: list[ArrayTestClass] = [ ArrayTestClass( id="large_array_found_first", value=0, - array=_LARGE_ARRAY, + array=list(range(20_000)), expected=True, msg="Should find first element in large array", ), ArrayTestClass( id="large_array_found_last", - value=_LARGE_ARRAY_SIZE - 1, - array=_LARGE_ARRAY, + value=19_999, + array=list(range(20_000)), expected=True, msg="Should find last element in large array", ), ArrayTestClass( id="large_array_found_middle", - value=_LARGE_ARRAY_SIZE // 2, - array=_LARGE_ARRAY, + value=10_000, + array=list(range(20_000)), expected=True, msg="Should find middle element in large array", ), ArrayTestClass( id="large_array_not_found", value=-1, - array=_LARGE_ARRAY, + array=list(range(20_000)), expected=False, msg="Should not find absent value in large array", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py index 21a8ac34c..856eb3e1e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py @@ -506,44 +506,42 @@ ] # Success: large array -_LARGE_ARRAY_SIZE = 20_000 -_LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) LARGE_ARRAY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( id="large_array_first", - array=_LARGE_ARRAY, + array=list(range(20_000)), search=0, expected=0, msg="Should find first in large array", ), IndexOfArrayTest( id="large_array_last", - array=_LARGE_ARRAY, - search=_LARGE_ARRAY_SIZE - 1, - expected=_LARGE_ARRAY_SIZE - 1, + array=list(range(20_000)), + search=19_999, + expected=19_999, msg="Should find last in large array", ), IndexOfArrayTest( id="large_array_middle", - array=_LARGE_ARRAY, - search=_LARGE_ARRAY_SIZE // 2, - expected=_LARGE_ARRAY_SIZE // 2, + array=list(range(20_000)), + search=10_000, + expected=10_000, msg="Should find middle in large array", ), IndexOfArrayTest( id="large_array_not_found", - array=_LARGE_ARRAY, + array=list(range(20_000)), search=-1, expected=-1, msg="Should return -1 for absent value in large array", ), IndexOfArrayTest( id="large_array_with_start", - array=_LARGE_ARRAY, - search=_LARGE_ARRAY_SIZE - 1, - start=_LARGE_ARRAY_SIZE - 2, - expected=_LARGE_ARRAY_SIZE - 1, + array=list(range(20_000)), + search=19_999, + start=19_998, + expected=19_999, msg="Should find with start in large array", ), ] From 9f00a9666495eae92b6ffa7057be3ba637fee5eb Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 15:51:11 -0700 Subject: [PATCH 09/10] revert smoke test changes Signed-off-by: Alina (Xi) Li --- .../expressions/array/filter/test_smoke_expression_filter.py | 2 +- .../operator/expressions/array/in/test_smoke_expression_in.py | 2 +- .../array/indexOfArray/test_smoke_expression_indexOfArray.py | 2 +- .../expressions/array/isArray/test_smoke_expression_isArray.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_expression_filter.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_expression_filter.py index add4d436f..49048c03b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_expression_filter.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_expression_filter.py @@ -40,4 +40,4 @@ def test_smoke_expression_filter(collection): ) expected = [{"_id": 1, "filtered": [4, 5]}, {"_id": 2, "filtered": [10, 15, 20, 25]}] - assertSuccess(result, expected, "Should support $filter expression") + assertSuccess(result, expected, msg="Should support $filter expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_expression_in.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_expression_in.py index 0220f4c01..46e97f208 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_expression_in.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_expression_in.py @@ -28,4 +28,4 @@ def test_smoke_expression_in(collection): ) expected = [{"_id": 1, "found": True}, {"_id": 2, "found": False}] - assertSuccess(result, expected, "Should support $in expression") + assertSuccess(result, expected, msg="Should support $in expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_expression_indexOfArray.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_expression_indexOfArray.py index 1959985d7..d92b4fb31 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_expression_indexOfArray.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_expression_indexOfArray.py @@ -31,4 +31,4 @@ def test_smoke_expression_indexOfArray(collection): ) expected = [{"_id": 1, "index": 1}, {"_id": 2, "index": 1}] - assertSuccess(result, expected, "Should support $indexOfArray expression") + assertSuccess(result, expected, msg="Should support $indexOfArray expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_expression_isArray.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_expression_isArray.py index 54608b372..c16ab7d55 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_expression_isArray.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_expression_isArray.py @@ -26,4 +26,4 @@ def test_smoke_expression_isArray(collection): ) expected = [{"_id": 1, "isArray": True}, {"_id": 2, "isArray": False}] - assertSuccess(result, expected, "Should support $isArray expression") + assertSuccess(result, expected, msg="Should support $isArray expression") From 822831c06583b4275f743d3ca4cd8047fdcbd9ef Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 15:53:28 -0700 Subject: [PATCH 10/10] Replace CombinationTest with CombinationTest Signed-off-by: Alina (Xi) Li --- ...array_arrayElemAt_indexOfArray_in_slice.py | 174 ++++++++---------- 1 file changed, 78 insertions(+), 96 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py index a542e6461..8d742302e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py @@ -5,100 +5,86 @@ and with other operators like $concatArrays, $reverseArray, $filter, $map, $size, etc. """ -from dataclasses import dataclass -from typing import Any - import pytest from bson import Decimal128, MaxKey, MinKey, Regex +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, execute_expression, ) from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_case import BaseTestCase - - -@dataclass(frozen=True) -class CombinationTest(BaseTestCase): - """Test case for combination expression tests.""" - expr: Any = None - - -# --------------------------------------------------------------------------- -# $arrayElemAt combinations -# --------------------------------------------------------------------------- -ARRAY_ELEM_AT_COMBINATION_TESTS: list[CombinationTest] = [ - CombinationTest( +ARRAY_ELEM_AT_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="arrayElemAt_index_from_indexOfArray", - expr={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 30]}]}, + expression={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 30]}]}, expected=30, msg="Should use $indexOfArray result as index", ), - CombinationTest( + ExpressionTestCase( id="arrayElemAt_last_element_via_size", - expr={"$arrayElemAt": [[10, 20, 30], {"$subtract": [{"$size": [[10, 20, 30]]}, 1]}]}, + expression={"$arrayElemAt": [[10, 20, 30], {"$subtract": [{"$size": [[10, 20, 30]]}, 1]}]}, expected=30, msg="Should access last element via $size - 1", ), - CombinationTest( + ExpressionTestCase( id="arrayElemAt_elem_from_slice", - expr={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], -2]}, 0]}, + expression={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], -2]}, 0]}, expected=30, msg="Should access element from $slice result", ), - CombinationTest( + ExpressionTestCase( id="arrayElemAt_elem_from_slice_3arg", - expr={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], 1, 2]}, 1]}, + expression={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], 1, 2]}, 1]}, expected=30, msg="Should access element from $slice 3-arg result", ), - CombinationTest( + ExpressionTestCase( id="arrayElemAt_elem_from_reverseArray", - expr={"$arrayElemAt": [{"$reverseArray": [[10, 20, 30]]}, 0]}, + expression={"$arrayElemAt": [{"$reverseArray": [[10, 20, 30]]}, 0]}, expected=30, msg="Should access element from $reverseArray result", ), - CombinationTest( + ExpressionTestCase( id="arrayElemAt_elem_from_concatArrays", - expr={"$arrayElemAt": [{"$concatArrays": [[10, 20], [30, 40]]}, 2]}, + expression={"$arrayElemAt": [{"$concatArrays": [[10, 20], [30, 40]]}, 2]}, expected=30, msg="Should access element from $concatArrays result", ), - CombinationTest( + ExpressionTestCase( id="arrayElemAt_computed_index", - expr={"$arrayElemAt": [[10, 20, 30], {"$subtract": [3, 1]}]}, + expression={"$arrayElemAt": [[10, 20, 30], {"$subtract": [3, 1]}]}, expected=30, msg="Should use computed index from $subtract", ), ] -# --------------------------------------------------------------------------- # $in combinations -# --------------------------------------------------------------------------- -IN_COMBINATION_TESTS: list[CombinationTest] = [ - CombinationTest( +IN_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="in_value_from_add", - expr={"$in": [{"$add": [1, 1]}, [1, 2, 3]]}, + expression={"$in": [{"$add": [1, 1]}, [1, 2, 3]]}, expected=True, msg="Should find value computed by $add", ), - CombinationTest( + ExpressionTestCase( id="in_array_from_concatArrays", - expr={"$in": [3, {"$concatArrays": [[1, 2], [3, 4]]}]}, + expression={"$in": [3, {"$concatArrays": [[1, 2], [3, 4]]}]}, expected=True, msg="Should search in $concatArrays result", ), - CombinationTest( + ExpressionTestCase( id="in_value_from_arrayElemAt", - expr={"$in": [{"$arrayElemAt": [[10, 20, 30], 1]}, [5, 20, 35]]}, + expression={"$in": [{"$arrayElemAt": [[10, 20, 30], 1]}, [5, 20, 35]]}, expected=True, msg="Should find value from $arrayElemAt", ), - CombinationTest( + ExpressionTestCase( id="in_array_from_filter", - expr={ + expression={ "$in": [ 4, { @@ -113,9 +99,9 @@ class CombinationTest(BaseTestCase): expected=True, msg="Should search in $filter result", ), - CombinationTest( + ExpressionTestCase( id="in_array_from_map", - expr={ + expression={ "$in": [ 20, { @@ -130,33 +116,33 @@ class CombinationTest(BaseTestCase): expected=True, msg="Should search in $map result", ), - CombinationTest( + ExpressionTestCase( id="in_array_from_reverseArray", - expr={"$in": [1, {"$reverseArray": [[1, 2, 3]]}]}, + expression={"$in": [1, {"$reverseArray": [[1, 2, 3]]}]}, expected=True, msg="Should search in $reverseArray result", ), - CombinationTest( + ExpressionTestCase( id="in_cond_with_inner_in", - expr={"$in": [5, {"$cond": [{"$in": ["a", ["a", "b"]]}, [5, 6], [7, 8]]}]}, + expression={"$in": [5, {"$cond": [{"$in": ["a", ["a", "b"]]}, [5, 6], [7, 8]]}]}, expected=True, msg="Should search in $cond-selected array", ), - CombinationTest( + ExpressionTestCase( id="in_inside_cond", - expr={"$cond": [{"$in": [2, [1, 2, 3]]}, "found", "not_found"]}, + expression={"$cond": [{"$in": [2, [1, 2, 3]]}, "found", "not_found"]}, expected="found", msg="Should use $in result in $cond", ), - CombinationTest( + ExpressionTestCase( id="in_value_from_indexOfArray", - expr={"$in": [{"$indexOfArray": [[10, 20, 30], 20]}, [0, 1, 2]]}, + expression={"$in": [{"$indexOfArray": [[10, 20, 30], 20]}, [0, 1, 2]]}, expected=True, msg="Should find $indexOfArray result in array", ), - CombinationTest( + ExpressionTestCase( id="in_nested_decimal128", - expr={ + expression={ "$in": [ {"$arrayElemAt": [[Decimal128("1.1"), Decimal128("2.2")], 1]}, [Decimal128("2.2"), Decimal128("3.3")], @@ -167,31 +153,29 @@ class CombinationTest(BaseTestCase): ), ] -# --------------------------------------------------------------------------- # $indexOfArray combinations -# --------------------------------------------------------------------------- -INDEX_OF_ARRAY_COMBINATION_TESTS: list[CombinationTest] = [ - CombinationTest( +INDEX_OF_ARRAY_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="indexOfArray_result_as_arrayElemAt_index", - expr={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 20]}]}, + expression={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 20]}]}, expected=20, msg="Should use $indexOfArray result as $arrayElemAt index", ), - CombinationTest( + ExpressionTestCase( id="indexOfArray_search_from_add", - expr={"$indexOfArray": [[1, 2, 3], {"$add": [1, 1]}]}, + expression={"$indexOfArray": [[1, 2, 3], {"$add": [1, 1]}]}, expected=1, msg="Should search for value computed by $add", ), - CombinationTest( + ExpressionTestCase( id="indexOfArray_array_from_concatArrays", - expr={"$indexOfArray": [{"$concatArrays": [[1, 2], [3, 4]]}, 3]}, + expression={"$indexOfArray": [{"$concatArrays": [[1, 2], [3, 4]]}, 3]}, expected=2, msg="Should search in $concatArrays result", ), - CombinationTest( + ExpressionTestCase( id="indexOfArray_array_from_filter", - expr={ + expression={ "$indexOfArray": [ {"$filter": {"input": [1, 2, 3, 4, 5], "cond": {"$gt": ["$$this", 2]}}}, 4, @@ -200,21 +184,23 @@ class CombinationTest(BaseTestCase): expected=1, msg="Should search in $filter result", ), - CombinationTest( + ExpressionTestCase( id="indexOfArray_result_in_cond", - expr={"$cond": [{"$gte": [{"$indexOfArray": [[1, 2, 3], 2]}, 0]}, "found", "not_found"]}, + expression={ + "$cond": [{"$gte": [{"$indexOfArray": [[1, 2, 3], 2]}, 0]}, "found", "not_found"] + }, expected="found", msg="Should use $indexOfArray result in $cond", ), - CombinationTest( + ExpressionTestCase( id="indexOfArray_start_from_subtract", - expr={"$indexOfArray": [[1, 2, 1, 2], 1, {"$subtract": [3, 1]}]}, + expression={"$indexOfArray": [[1, 2, 1, 2], 1, {"$subtract": [3, 1]}]}, expected=2, msg="Should use $subtract result as start index", ), - CombinationTest( + ExpressionTestCase( id="indexOfArray_via_arrayElemAt", - expr={ + expression={ "$indexOfArray": [ ["a", "b", "c", "d"], { @@ -228,9 +214,9 @@ class CombinationTest(BaseTestCase): expected=1, msg="Should search for value from nested $arrayElemAt/$indexOfArray", ), - CombinationTest( + ExpressionTestCase( id="indexOfArray_subarray_mixed_bson", - expr={ + expression={ "$indexOfArray": [ [[MinKey(), MaxKey()], [1, 2], "x"], { @@ -244,9 +230,9 @@ class CombinationTest(BaseTestCase): expected=1, msg="Should find mixed BSON subarray via nested operators", ), - CombinationTest( + ExpressionTestCase( id="indexOfArray_triple_nested_decimal128", - expr={ + expression={ "$indexOfArray": [ [Decimal128("1.1"), Decimal128("2.2"), Decimal128("3.3")], { @@ -267,25 +253,23 @@ class CombinationTest(BaseTestCase): ), ] -# --------------------------------------------------------------------------- # $slice combinations -# --------------------------------------------------------------------------- -SLICE_COMBINATION_TESTS: list[CombinationTest] = [ - CombinationTest( +SLICE_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="slice_array_from_concatArrays", - expr={"$slice": [{"$concatArrays": [[1, 2], [3, 4, 5]]}, 3]}, + expression={"$slice": [{"$concatArrays": [[1, 2], [3, 4, 5]]}, 3]}, expected=[1, 2, 3], msg="Should slice $concatArrays result", ), - CombinationTest( + ExpressionTestCase( id="slice_n_from_subtract", - expr={"$slice": [[1, 2, 3, 4, 5], {"$subtract": [5, 2]}]}, + expression={"$slice": [[1, 2, 3, 4, 5], {"$subtract": [5, 2]}]}, expected=[1, 2, 3], msg="Should use $subtract result as n", ), - CombinationTest( + ExpressionTestCase( id="slice_array_from_filter", - expr={ + expression={ "$slice": [ { "$filter": { @@ -300,9 +284,9 @@ class CombinationTest(BaseTestCase): expected=[3, 4], msg="Should slice $filter result", ), - CombinationTest( + ExpressionTestCase( id="slice_position_from_indexOfArray", - expr={ + expression={ "$slice": [ [10, 20, 30, 40, 50], {"$indexOfArray": [[10, 20, 30, 40, 50], 30]}, @@ -312,9 +296,9 @@ class CombinationTest(BaseTestCase): expected=[30, 40], msg="Should use $indexOfArray result as position", ), - CombinationTest( + ExpressionTestCase( id="slice_array_from_map", - expr={ + expression={ "$slice": [ { "$map": { @@ -329,23 +313,23 @@ class CombinationTest(BaseTestCase): expected=[10, 20], msg="Should slice $map result", ), - CombinationTest( + ExpressionTestCase( id="slice_array_from_reverseArray", - expr={"$slice": [{"$reverseArray": [[1, 2, 3, 4, 5]]}, 3]}, + expression={"$slice": [{"$reverseArray": [[1, 2, 3, 4, 5]]}, 3]}, expected=[5, 4, 3], msg="Should slice $reverseArray result", ), - CombinationTest( + ExpressionTestCase( id="slice_n_from_size", - expr={"$slice": [[10, 20, 30, 40], {"$subtract": [{"$size": [[10, 20, 30, 40]]}, 1]}]}, + expression={ + "$slice": [[10, 20, 30, 40], {"$subtract": [{"$size": [[10, 20, 30, 40]]}, 1]}] + }, expected=[10, 20, 30], msg="Should use $size-based computation as n", ), ] -# --------------------------------------------------------------------------- # Aggregate all combination tests -# --------------------------------------------------------------------------- ALL_COMBINATION_TESTS = ( ARRAY_ELEM_AT_COMBINATION_TESTS + IN_COMBINATION_TESTS @@ -357,13 +341,11 @@ class CombinationTest(BaseTestCase): @pytest.mark.parametrize("test", pytest_params(ALL_COMBINATION_TESTS)) def test_combination_expression(collection, test): """Test array operators composed with other operators.""" - result = execute_expression(collection, test.expr) + result = execute_expression(collection, test.expression) assert_expression_result(result, expected=test.expected, msg=test.msg) -# --------------------------------------------------------------------------- # Standalone tests for behavior that doesn't fit the dataclass pattern -# --------------------------------------------------------------------------- def test_arrayElemAt_oob_is_missing_not_null(collection): """Test out-of-bounds result is truly MISSING (field absent), not null.""" result = execute_expression(collection, {"$type": {"$arrayElemAt": [[1, 2, 3], 10]}})