From 8e1c6f08fd6167ba4ce250365dd8d23e3506373b Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 11:24:16 -0700 Subject: [PATCH 01/21] 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 37548e938b448c9bda2164bfc852f1ad266535af Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 11:40:23 -0700 Subject: [PATCH 02/21] 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 529732d1aaf8ec94ffb1eda005bd4cd6aca893c9 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 11:46:19 -0700 Subject: [PATCH 03/21] 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 2550543646dfc2ac7bb8712f5f1a7b25ec28e164 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 11:56:12 -0700 Subject: [PATCH 04/21] 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 3e8e3003d6e94c38098e384f092dc86f1d5094c6 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 12:09:01 -0700 Subject: [PATCH 05/21] 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 060c1a064e7375f5e417b35c2d3582f1347fb531 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 12:17:42 -0700 Subject: [PATCH 06/21] 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 c8881ec7b098a52bb6dbe04e964e2135fd5e1023 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 12:20:03 -0700 Subject: [PATCH 07/21] 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 05230fd039166e1c122a17704e9da4dfb1b58185 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 15:37:47 -0700 Subject: [PATCH 08/21] 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 b7287f360d32282cf8986b425ee0d17f44b3b565 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 15:51:11 -0700 Subject: [PATCH 09/21] 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 552d9e140b32319dc5fab5b4acc22b3f86e1eeb3 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 15:53:28 -0700 Subject: [PATCH 10/21] 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]}}) From 0b83cbba76efa04262fe4453f4342009b3fd9aa3 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 7 Jul 2026 13:13:48 -0700 Subject: [PATCH 11/21] replace ArrayTestClass with ExpressionTestCase Signed-off-by: Alina (Xi) Li --- ..._as_errors.py => test_filter_as_errors.py} | 0 ...son_types.py => test_filter_bson_types.py} | 0 ...havior.py => test_filter_core_behavior.py} | 0 ...filter_errors.py => test_filter_errors.py} | 0 ...ressions.py => test_filter_expressions.py} | 0 ...ors.py => test_filter_structure_errors.py} | 0 ...ression_filter.py => test_smoke_filter.py} | 0 .../in/test_expression_in_core_behavior.py | 240 ----------- .../in/test_expression_in_nested_arrays.py | 173 -------- ...in_bson_types.py => test_in_bson_types.py} | 229 ++++++----- .../array/in/test_in_core_behavior.py | 270 +++++++++++++ ...ression_in_errors.py => test_in_errors.py} | 123 +++--- ..._expressions.py => test_in_expressions.py} | 0 .../array/in/test_in_nested_arrays.py | 167 ++++++++ ...ull_missing.py => test_in_null_missing.py} | 46 +-- ...moke_expression_in.py => test_smoke_in.py} | 0 ...pes.py => test_indexOfArray_bson_types.py} | 0 ....py => test_indexOfArray_core_behavior.py} | 0 ..._errors.py => test_indexOfArray_errors.py} | 0 ...ns.py => test_indexOfArray_expressions.py} | 0 ...g.py => test_indexOfArray_null_missing.py} | 0 ...xOfArray.py => test_smoke_indexOfArray.py} | 0 .../test_expression_isArray_bson_types.py | 301 -------------- .../test_expression_isArray_core_behavior.py | 189 --------- .../array/isArray/test_isArray_bson_types.py | 372 ++++++++++++++++++ .../isArray/test_isArray_core_behavior.py | 329 ++++++++++++++++ ...Array_errors.py => test_isArray_errors.py} | 0 ...essions.py => test_isArray_expressions.py} | 0 ...ssion_isArray.py => test_smoke_isArray.py} | 0 .../expressions/array/utils/__init__.py | 0 .../array/utils/array_test_case.py | 28 -- 31 files changed, 1321 insertions(+), 1146 deletions(-) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/{test_expression_filter_as_errors.py => test_filter_as_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/{test_expression_filter_bson_types.py => test_filter_bson_types.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/{test_expression_filter_core_behavior.py => test_filter_core_behavior.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/{test_expression_filter_errors.py => test_filter_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/{test_expression_filter_expressions.py => test_filter_expressions.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/{test_expression_filter_structure_errors.py => test_filter_structure_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/{test_smoke_expression_filter.py => test_smoke_filter.py} (100%) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py rename documentdb_tests/compatibility/tests/core/operator/expressions/array/in/{test_expression_in_bson_types.py => test_in_bson_types.py} (52%) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py rename documentdb_tests/compatibility/tests/core/operator/expressions/array/in/{test_expression_in_errors.py => test_in_errors.py} (65%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/in/{test_expression_in_expressions.py => test_in_expressions.py} (100%) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py rename documentdb_tests/compatibility/tests/core/operator/expressions/array/in/{test_expression_in_null_missing.py => test_in_null_missing.py} (61%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/in/{test_smoke_expression_in.py => test_smoke_in.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/{test_expression_indexOfArray_bson_types.py => test_indexOfArray_bson_types.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/{test_expression_indexOfArray_core_behavior.py => test_indexOfArray_core_behavior.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/{test_expression_indexOfArray_errors.py => test_indexOfArray_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/{test_expression_indexOfArray_expressions.py => test_indexOfArray_expressions.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/{test_expression_indexOfArray_null_missing.py => test_indexOfArray_null_missing.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/{test_smoke_expression_indexOfArray.py => test_smoke_indexOfArray.py} (100%) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py delete 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_isArray_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py rename documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/{test_expression_isArray_errors.py => test_isArray_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/{test_expression_isArray_expressions.py => test_isArray_expressions.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/{test_smoke_expression_isArray.py => test_smoke_isArray.py} (100%) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py 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_filter_as_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_as_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_as_errors.py 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_filter_bson_types.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_bson_types.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_bson_types.py 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_filter_core_behavior.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_core_behavior.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_core_behavior.py 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_filter_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py 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_filter_expressions.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_expressions.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_expressions.py 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_filter_structure_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_expression_filter_structure_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_structure_errors.py 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_filter.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_expression_filter.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_filter.py 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 deleted file mode 100644 index 2abfbbce0..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_core_behavior.py +++ /dev/null @@ -1,240 +0,0 @@ -""" -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.array_test_case import ( # noqa: E501 - ArrayTestClass, -) -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[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" - ), - ArrayTestClass( - id="found_last", value=3, array=[1, 2, 3], expected=True, msg="Should find last element" - ), - ArrayTestClass( - id="found_string", - value="b", - array=["a", "b", "c"], - expected=True, - msg="Should find string in array", - ), - ArrayTestClass( - id="found_bool_true", - value=True, - array=[True, False], - expected=True, - msg="Should find true in array", - ), - ArrayTestClass( - id="found_bool_false", - value=False, - array=[True, False], - expected=True, - msg="Should find false in array", - ), - ArrayTestClass( - id="found_null", - value=None, - array=[None, 1, 2], - expected=True, - msg="Should find null in array", - ), - ArrayTestClass( - id="found_nested_array", - value=[3, 4], - array=[[1, 2], [3, 4]], - expected=True, - msg="Should find nested array", - ), - ArrayTestClass( - id="found_object", - value={"a": 1}, - array=[{"a": 1}, {"b": 2}], - expected=True, - msg="Should find object in array", - ), - ArrayTestClass( - id="found_single_element", - value=42, - array=[42], - expected=True, - msg="Should find value in single-element array", - ), - ArrayTestClass( - 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[ArrayTestClass] = [ - ArrayTestClass( - id="not_found_int", - value=4, - array=[1, 2, 3], - expected=False, - msg="Should not find absent int", - ), - ArrayTestClass( - id="not_found_string", - value="z", - array=["a", "b"], - expected=False, - msg="Should not find absent string", - ), - ArrayTestClass( - id="not_found_empty_array", - value=1, - array=[], - expected=False, - msg="Should not find value in empty array", - ), - ArrayTestClass( - id="not_found_type_mismatch", - value="1", - array=[1, 2, 3], - expected=False, - msg="Should not find string '1' in int array", - ), - ArrayTestClass( - id="not_found_bool_vs_int", - value=True, - array=[1, 0], - expected=False, - msg="Should not find bool in int array", - ), - ArrayTestClass( - id="not_found_null", - value=None, - array=[1, 2, 3], - expected=False, - msg="Should not find null in non-null array", - ), - ArrayTestClass( - id="not_found_partial_array", - value=[1], - array=[[1, 2], [3, 4]], - expected=False, - msg="Should not find partial array match", - ), - ArrayTestClass( - 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[ArrayTestClass] = [ - ArrayTestClass( - id="mixed_find_string", - value="2", - array=[1, "2", True, None, [1]], - expected=True, - msg="Should find string in mixed-type array", - ), - ArrayTestClass( - id="mixed_find_null", - value=None, - array=[1, "2", True, None, [1]], - expected=True, - msg="Should find null in mixed-type array", - ), - ArrayTestClass( - id="mixed_find_array", - value=[1], - array=[1, "2", True, None, [1]], - expected=True, - msg="Should find array in mixed-type array", - ), - ArrayTestClass( - 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_TESTS: list[ArrayTestClass] = [ - ArrayTestClass( - id="large_array_found_first", - value=0, - array=list(range(20_000)), - expected=True, - msg="Should find first element in large array", - ), - ArrayTestClass( - id="large_array_found_last", - 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=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=list(range(20_000)), - 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_nested_arrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py deleted file mode 100644 index 60e2e0c96..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_nested_arrays.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -Nested array search tests for $in expression. - -Tests searching for complex elements in nested mixed arrays and deeply nested structures. -""" - -from datetime import datetime, timezone - -import pytest -from bson import Binary, Decimal128, MaxKey, MinKey, ObjectId - -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, - 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[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", - ), - 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", - ), - 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", - ), - 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", - ), - ArrayTestClass( - id="nested_find_empty_object", - value={}, - array=[1, {}, [2], "a"], - expected=True, - msg="Should find empty object in array", - ), - ArrayTestClass( - id="nested_find_empty_array", - value=[], - array=[1, {}, [], "a"], - expected=True, - msg="Should find empty array in array", - ), - 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", - ), - 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", - ), - ArrayTestClass( - id="nested_find_subarray_datetime_objectid", - 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", - ), - ArrayTestClass( - 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[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", - ), - 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", - ), - 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", - ), - ArrayTestClass( - id="nested_inner_not_outer", - value=[2, 3], - array=[[1, [2, 3]], [2, 3], 4], - expected=True, - msg="Should find inner array match", - ), - ArrayTestClass( - id="nested_5_levels", - value=[[[[99]]]], - array=[[[[[99]]]], "other"], - expected=True, - msg="Should find 5-level nested array", - ), - ArrayTestClass( - 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_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py similarity index 52% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_bson_types.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py index 85abcd6b2..1a6cdb9a3 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_in_bson_types.py @@ -10,12 +10,11 @@ import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp -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.expression_test_case import ( # noqa: E501 + ExpressionTestCase, ) 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 @@ -31,233 +30,242 @@ ) # Success: search for various BSON types -BSON_TYPE_TESTS: list[ArrayTestClass] = [ - ArrayTestClass( +BSON_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="bson_int64", - value=Int64(99), - array=[Int64(99), 1], + doc={"val": Int64(99), "arr": [Int64(99), 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find Int64 in array", ), - ArrayTestClass( + ExpressionTestCase( id="bson_decimal128", - value=Decimal128("1.5"), - array=[Decimal128("1.5"), 2], + doc={"val": Decimal128("1.5"), "arr": [Decimal128("1.5"), 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find Decimal128 in array", ), - ArrayTestClass( + ExpressionTestCase( id="bson_datetime", - value=datetime(2024, 1, 1, tzinfo=timezone.utc), - array=[datetime(2024, 1, 1, tzinfo=timezone.utc), 1], + doc={ + "val": datetime(2024, 1, 1, tzinfo=timezone.utc), + "arr": [datetime(2024, 1, 1, tzinfo=timezone.utc), 1], + }, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find datetime in array", ), - ArrayTestClass( + ExpressionTestCase( id="bson_objectid", - value=ObjectId("000000000000000000000001"), - array=[ObjectId("000000000000000000000001"), 1], + doc={ + "val": ObjectId("000000000000000000000001"), + "arr": [ObjectId("000000000000000000000001"), 1], + }, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find ObjectId in array", ), - ArrayTestClass( + ExpressionTestCase( id="bson_binary", - value=Binary(b"\x01\x02", 0), - array=[Binary(b"\x01\x02", 0), 1], + doc={"val": Binary(b"\x01\x02", 0), "arr": [Binary(b"\x01\x02", 0), 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find Binary in array", ), - ArrayTestClass( + ExpressionTestCase( id="bson_regex", - value=Regex("^abc", "i"), - array=[Regex("^abc", "i"), 1], + doc={"val": Regex("^abc", "i"), "arr": [Regex("^abc", "i"), 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find Regex in array", ), - ArrayTestClass( + ExpressionTestCase( id="bson_timestamp", - value=Timestamp(1, 1), - array=[Timestamp(1, 1), 1], + doc={"val": Timestamp(1, 1), "arr": [Timestamp(1, 1), 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find Timestamp in array", ), - ArrayTestClass( + ExpressionTestCase( id="bson_minkey", - value=MinKey(), - array=[MinKey(), 1], + doc={"val": MinKey(), "arr": [MinKey(), 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find MinKey in array", ), - ArrayTestClass( + ExpressionTestCase( id="bson_maxkey", - value=MaxKey(), - array=[1, MaxKey()], + doc={"val": MaxKey(), "arr": [1, MaxKey()]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find MaxKey in array", ), - ArrayTestClass( + ExpressionTestCase( id="bson_uuid", - value=Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), - array=[Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), 1], + doc={ + "val": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), + "arr": [Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), 1], + }, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find UUID binary in array", ), # Special float values - ArrayTestClass( + ExpressionTestCase( id="float_infinity_in_array", - value=FLOAT_INFINITY, - array=[FLOAT_INFINITY, 1], + doc={"val": FLOAT_INFINITY, "arr": [FLOAT_INFINITY, 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find Infinity in array", ), - ArrayTestClass( + ExpressionTestCase( id="float_neg_infinity_in_array", - value=FLOAT_NEGATIVE_INFINITY, - array=[FLOAT_NEGATIVE_INFINITY, 1], + doc={"val": FLOAT_NEGATIVE_INFINITY, "arr": [FLOAT_NEGATIVE_INFINITY, 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find -Infinity in array", ), - ArrayTestClass( + ExpressionTestCase( id="float_infinity_not_in_array", - value=FLOAT_INFINITY, - array=[1, 2, 3], + doc={"val": FLOAT_INFINITY, "arr": [1, 2, 3]}, + expression={"$in": ["$val", "$arr"]}, expected=False, msg="Should not find Infinity in non-Infinity array", ), # Special Decimal128 values - ArrayTestClass( + ExpressionTestCase( id="decimal128_infinity_in_array", - value=DECIMAL128_INFINITY, - array=[DECIMAL128_INFINITY, 1], + doc={"val": DECIMAL128_INFINITY, "arr": [DECIMAL128_INFINITY, 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find Decimal128 Infinity in array", ), - ArrayTestClass( + ExpressionTestCase( id="decimal128_neg_infinity_in_array", - value=DECIMAL128_NEGATIVE_INFINITY, - array=[DECIMAL128_NEGATIVE_INFINITY, 1], + doc={"val": DECIMAL128_NEGATIVE_INFINITY, "arr": [DECIMAL128_NEGATIVE_INFINITY, 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find Decimal128 -Infinity in array", ), # NaN equality: NaN == NaN in BSON comparison (unlike IEEE 754) - ArrayTestClass( + ExpressionTestCase( id="float_nan_found", - value=FLOAT_NAN, - array=[FLOAT_NAN, 1], + doc={"val": FLOAT_NAN, "arr": [FLOAT_NAN, 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find NaN in array (BSON equality)", ), - ArrayTestClass( + ExpressionTestCase( id="decimal128_nan_found", - value=DECIMAL128_NAN, - array=[DECIMAL128_NAN, 1], + doc={"val": DECIMAL128_NAN, "arr": [DECIMAL128_NAN, 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find Decimal128 NaN in array (BSON equality)", ), - ArrayTestClass( + ExpressionTestCase( id="float_nan_matches_decimal128_nan", - value=FLOAT_NAN, - array=[DECIMAL128_NAN, 1], + doc={"val": FLOAT_NAN, "arr": [DECIMAL128_NAN, 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="float NaN should match Decimal128 NaN cross-type", ), - ArrayTestClass( + ExpressionTestCase( id="decimal128_nan_matches_float_nan", - value=DECIMAL128_NAN, - array=[FLOAT_NAN, 1], + doc={"val": DECIMAL128_NAN, "arr": [FLOAT_NAN, 1]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Decimal128 NaN should match float NaN cross-type", ), # Aggregation $in does NOT pattern-match regex against strings (unlike query $in) - ArrayTestClass( + ExpressionTestCase( id="regex_no_pattern_match", - value=Regex("^a"), - array=["abc", "def"], + doc={"val": Regex("^a"), "arr": ["abc", "def"]}, + expression={"$in": ["$val", "$arr"]}, expected=False, msg="Regex should not pattern-match strings in aggregation $in", ), ] # Success: numeric type equivalence -NUMERIC_EQUIVALENCE_TESTS: list[ArrayTestClass] = [ - ArrayTestClass( +NUMERIC_EQUIVALENCE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="int_in_doubles", - value=1, - array=[1.0, 2.0], + doc={"val": 1, "arr": [1.0, 2.0]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find int in doubles via numeric equivalence", ), - ArrayTestClass( + ExpressionTestCase( id="int_in_longs", - value=1, - array=[Int64(1), 2], + doc={"val": 1, "arr": [Int64(1), 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find int in longs via numeric equivalence", ), - ArrayTestClass( + ExpressionTestCase( id="int_in_decimal128s", - value=1, - array=[Decimal128("1"), 2], + doc={"val": 1, "arr": [Decimal128("1"), 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find int in decimal128s via numeric equivalence", ), - ArrayTestClass( + ExpressionTestCase( id="double_in_ints", - value=1.0, - array=[1, 2], + doc={"val": 1.0, "arr": [1, 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find double in ints via numeric equivalence", ), - ArrayTestClass( + ExpressionTestCase( id="long_in_ints", - value=Int64(1), - array=[1, 2], + doc={"val": Int64(1), "arr": [1, 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find long in ints via numeric equivalence", ), - ArrayTestClass( + ExpressionTestCase( id="decimal128_in_ints", - value=Decimal128("1"), - array=[1, 2], + doc={"val": Decimal128("1"), "arr": [1, 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find decimal128 in ints via numeric equivalence", ), - ArrayTestClass( + ExpressionTestCase( id="decimal128_in_doubles", - value=Decimal128("1.5"), - array=[1.5, 2.5], + doc={"val": Decimal128("1.5"), "arr": [1.5, 2.5]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find decimal128 in doubles via numeric equivalence", ), ] # Negative zero equivalence with positive zero -NEGATIVE_ZERO_TESTS: list[ArrayTestClass] = [ - ArrayTestClass( +NEGATIVE_ZERO_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="double_neg_zero_matches_zero", - value=DOUBLE_NEGATIVE_ZERO, - array=[0, 1, 2], + doc={"val": DOUBLE_NEGATIVE_ZERO, "arr": [0, 1, 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="$in should treat -0.0 as equivalent to 0", ), - ArrayTestClass( + ExpressionTestCase( id="zero_matches_double_neg_zero_in_array", - value=0, - array=[DOUBLE_NEGATIVE_ZERO, 1, 2], + doc={"val": 0, "arr": [DOUBLE_NEGATIVE_ZERO, 1, 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="$in should find 0 in array containing -0.0", ), - ArrayTestClass( + ExpressionTestCase( id="decimal128_neg_zero_matches_zero", - value=DECIMAL128_NEGATIVE_ZERO, - array=[0, 1, 2], + doc={"val": DECIMAL128_NEGATIVE_ZERO, "arr": [0, 1, 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="$in should treat Decimal128 -0 as equivalent to 0", ), - ArrayTestClass( + ExpressionTestCase( id="zero_matches_decimal128_neg_zero_in_array", - value=0, - array=[DECIMAL128_NEGATIVE_ZERO, 1, 2], + doc={"val": 0, "arr": [DECIMAL128_NEGATIVE_ZERO, 1, 2]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="$in should find 0 in array containing Decimal128 -0", ), @@ -266,28 +274,11 @@ # Aggregate and test ALL_TESTS = BSON_TYPE_TESTS + NUMERIC_EQUIVALENCE_TESTS + NEGATIVE_ZERO_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} - ) +def test_in_bson_insert(collection, test): + """Test $in BSON type matching 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/in/test_in_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py new file mode 100644 index 000000000..ace925d4d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py @@ -0,0 +1,270 @@ +""" +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.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +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[ExpressionTestCase] = [ + ExpressionTestCase( + id="found_int", + doc={"val": 2, "arr": [1, 2, 3]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find int in array", + ), + ExpressionTestCase( + id="found_first", + doc={"val": 1, "arr": [1, 2, 3]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find first element", + ), + ExpressionTestCase( + id="found_last", + doc={"val": 3, "arr": [1, 2, 3]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find last element", + ), + ExpressionTestCase( + id="found_string", + doc={"val": "b", "arr": ["a", "b", "c"]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find string in array", + ), + ExpressionTestCase( + id="found_bool_true", + doc={"val": True, "arr": [True, False]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find true in array", + ), + ExpressionTestCase( + id="found_bool_false", + doc={"val": False, "arr": [True, False]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find false in array", + ), + ExpressionTestCase( + id="found_null", + doc={"val": None, "arr": [None, 1, 2]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find null in array", + ), + ExpressionTestCase( + id="found_nested_array", + doc={"val": [3, 4], "arr": [[1, 2], [3, 4]]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find nested array", + ), + ExpressionTestCase( + id="found_object", + doc={"val": {"a": 1}, "arr": [{"a": 1}, {"b": 2}]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find object in array", + ), + ExpressionTestCase( + id="found_single_element", + doc={"val": 42, "arr": [42]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find value in single-element array", + ), + ExpressionTestCase( + id="found_duplicate", + doc={"val": 5, "arr": [5, 5, 5]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find value in array of duplicates", + ), +] + +# Success: value not found → False +NOT_FOUND_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="not_found_int", + doc={"val": 4, "arr": [1, 2, 3]}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find absent int", + ), + ExpressionTestCase( + id="not_found_string", + doc={"val": "z", "arr": ["a", "b"]}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find absent string", + ), + ExpressionTestCase( + id="not_found_empty_array", + doc={"val": 1, "arr": []}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find value in empty array", + ), + ExpressionTestCase( + id="not_found_type_mismatch", + doc={"val": "1", "arr": [1, 2, 3]}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find string '1' in int array", + ), + ExpressionTestCase( + id="not_found_bool_vs_int", + doc={"val": True, "arr": [1, 0]}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find bool in int array", + ), + ExpressionTestCase( + id="not_found_null", + doc={"val": None, "arr": [1, 2, 3]}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find null in non-null array", + ), + ExpressionTestCase( + id="not_found_partial_array", + doc={"val": [1], "arr": [[1, 2], [3, 4]]}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find partial array match", + ), + ExpressionTestCase( + id="not_found_partial_object", + doc={"val": {"a": 1}, "arr": [{"a": 1, "b": 2}]}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find partial object match", + ), +] + +# Success: mixed types in array +MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="mixed_find_string", + doc={"val": "2", "arr": [1, "2", True, None, [1]]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find string in mixed-type array", + ), + ExpressionTestCase( + id="mixed_find_null", + doc={"val": None, "arr": [1, "2", True, None, [1]]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find null in mixed-type array", + ), + ExpressionTestCase( + id="mixed_find_array", + doc={"val": [1], "arr": [1, "2", True, None, [1]]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find array in mixed-type array", + ), + ExpressionTestCase( + id="mixed_not_found", + doc={"val": "x", "arr": [1, "2", True, None, [1]]}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find absent value in mixed-type array", + ), +] + +# Success: large array + +LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="large_array_found_first", + doc={"val": 0, "arr": list(range(20_000))}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find first element in large array", + ), + ExpressionTestCase( + id="large_array_found_last", + doc={"val": 19_999, "arr": list(range(20_000))}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find last element in large array", + ), + ExpressionTestCase( + id="large_array_found_middle", + doc={"val": 10_000, "arr": list(range(20_000))}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find middle element in large array", + ), + ExpressionTestCase( + id="large_array_not_found", + doc={"val": -1, "arr": list(range(20_000))}, + expression={"$in": ["$val", "$arr"]}, + 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 + + +@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, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +# Property [Literal Evaluation]: $in evaluates correctly with inline literal values. +TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_found_int", + doc=None, + expression={"$in": [2, {"$literal": [1, 2, 3]}]}, + expected=True, + msg="$in should find int in literal array", + ), + ExpressionTestCase( + "literal_not_found_int", + doc=None, + expression={"$in": [4, {"$literal": [1, 2, 3]}]}, + expected=False, + msg="$in should not find absent int in literal array", + ), + ExpressionTestCase( + "literal_large_array_found", + doc=None, + expression={"$in": [0, {"$literal": list(range(20_000))}]}, + expected=True, + msg="$in should find value in large literal array", + ), +] + + +@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, test.expression) + 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_in_errors.py similarity index 65% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_errors.py index 715e21461..90a1acb26 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_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.array_test_case import ( # noqa: E501 - ArrayTestClass, +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, @@ -25,127 +25,127 @@ 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( +NOT_ARRAY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="string_as_array", - value=1, - array="hello", + doc={"val": 1, "arr": "hello"}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject string as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="int_as_array", - value=1, - array=42, + doc={"val": 1, "arr": 42}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject int as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="double_as_array", - value=1, - array=3.14, + doc={"val": 1, "arr": 3.14}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject double as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="bool_true_as_array", - value=1, - array=True, + doc={"val": 1, "arr": True}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject bool true as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="bool_false_as_array", - value=1, - array=False, + doc={"val": 1, "arr": False}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject bool false as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="object_as_array", - value=1, - array={"a": 1}, + doc={"val": 1, "arr": {"a": 1}}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject object as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="decimal128_as_array", - value=1, - array=Decimal128("1"), + doc={"val": 1, "arr": Decimal128("1")}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject decimal128 as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="int64_as_array", - value=1, - array=Int64(1), + doc={"val": 1, "arr": Int64(1)}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject int64 as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="binary_as_array", - value=1, - array=Binary(b"x", 0), + doc={"val": 1, "arr": Binary(b"x", 0)}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject binary as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="datetime_as_array", - value=1, - array=datetime(2024, 1, 1, tzinfo=timezone.utc), + doc={"val": 1, "arr": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject datetime as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="objectid_as_array", - value=1, - array=ObjectId(), + doc={"val": 1, "arr": ObjectId()}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject objectid as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="regex_as_array", - value=1, - array=Regex("x"), + doc={"val": 1, "arr": Regex("x")}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject regex as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="maxkey_as_array", - value=1, - array=MaxKey(), + doc={"val": 1, "arr": MaxKey()}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject maxkey as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="minkey_as_array", - value=1, - array=MinKey(), + doc={"val": 1, "arr": MinKey()}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject minkey as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="timestamp_as_array", - value=1, - array=Timestamp(0, 0), + doc={"val": 1, "arr": Timestamp(0, 0)}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject timestamp as array arg", ), - ArrayTestClass( + ExpressionTestCase( id="null_as_array", - value=1, - array=None, + doc={"val": 1, "arr": None}, + expression={"$in": ["$val", "$arr"]}, 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[ArrayTestClass] = [ - ArrayTestClass( +LITERAL_ONLY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="missing_as_array", - value=1, - array=MISSING, + doc={"val": 1, "arr": MISSING}, + expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, msg="Should reject missing as array arg", ), @@ -158,21 +158,10 @@ ] + 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} - ) + 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/in/test_expression_in_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_expressions.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_expressions.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_expressions.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py new file mode 100644 index 000000000..85f28b4eb --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py @@ -0,0 +1,167 @@ +""" +Nested array search tests for $in expression. + +Tests searching for complex elements in nested mixed arrays and deeply nested structures. +""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Decimal128, MaxKey, MinKey, ObjectId + +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 + +# Success: nested mixed arrays as search targets +NESTED_MIXED_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_find_object_in_mixed", + doc={"val": {"a": 1}, "arr": [1, "two", {"a": 1}, [3, 4], True]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find object in nested mixed array", + ), + ExpressionTestCase( + id="nested_find_array_in_mixed", + doc={"val": [3, 4], "arr": [1, "two", {"a": 1}, [3, 4], True]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find array in nested mixed array", + ), + ExpressionTestCase( + id="nested_find_deep_object", + doc={"val": {"a": {"b": 3}}, "arr": [[1, 2], {"a": {"b": 3}}, "x"]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find deep object in array", + ), + ExpressionTestCase( + id="nested_find_array_with_mixed_types", + doc={"val": [None, "a", 2], "arr": [1, [None, "a", 2], "b"]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find mixed-type subarray", + ), + ExpressionTestCase( + id="nested_find_empty_object", + doc={"val": {}, "arr": [1, {}, [2], "a"]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find empty object in array", + ), + ExpressionTestCase( + id="nested_find_empty_array", + doc={"val": [], "arr": [1, {}, [], "a"]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find empty array in array", + ), + ExpressionTestCase( + id="nested_find_subarray_binary_decimal128", + doc={ + "val": [Binary(b"\x01\x02", 0), Decimal128("3.14")], + "arr": [1, [Binary(b"\x01\x02", 0), Decimal128("3.14")], "x", [3]], + }, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find subarray with binary and decimal128", + ), + ExpressionTestCase( + id="nested_find_subarray_object_array", + doc={"val": [{"k": 1}, [2, 3]], "arr": ["a", [{"k": 1}, [2, 3]], None, 4]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find subarray with object and array", + ), + ExpressionTestCase( + id="nested_find_subarray_datetime_objectid", + doc={ + "val": [ + datetime(2024, 1, 1, tzinfo=timezone.utc), + ObjectId("000000000000000000000001"), + ], + "arr": [ + 0, + [datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], + "end", + ], + }, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find subarray with datetime and objectid", + ), + ExpressionTestCase( + id="nested_find_subarray_minkey_maxkey", + doc={"val": [MinKey(), MaxKey()], "arr": [[MinKey(), MaxKey()], 1, "a"]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find subarray with minkey and maxkey", + ), +] + +# Success: deeply nested search targets (3-5 levels) +DEEPLY_NESTED_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_3_levels", + doc={"val": [[2, 3], [4, 5]], "arr": [1, [[2, 3], [4, 5]], "end"]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find 3-level nested array", + ), + ExpressionTestCase( + id="nested_4_levels", + doc={"val": [[[1, 2], 3], 4], "arr": ["a", [[[1, 2], 3], 4], None]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find 4-level nested array", + ), + ExpressionTestCase( + id="nested_deep_mixed_bson", + doc={ + "val": [[MinKey(), {"a": [Decimal128("1.5")]}], True], + "arr": [0, [[MinKey(), {"a": [Decimal128("1.5")]}], True], "x"], + }, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find deeply nested mixed BSON", + ), + ExpressionTestCase( + id="nested_inner_not_outer", + doc={"val": [2, 3], "arr": [[1, [2, 3]], [2, 3], 4]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find inner array match", + ), + ExpressionTestCase( + id="nested_5_levels", + doc={"val": [[[[99]]]], "arr": [[[[[99]]]], "other"]}, + expression={"$in": ["$val", "$arr"]}, + expected=True, + msg="Should find 5-level nested array", + ), + ExpressionTestCase( + id="nested_deep_not_found", + doc={"val": [2, 3], "arr": [[1, [2, 3]], [4, 5]]}, + expression={"$in": ["$val", "$arr"]}, + expected=False, + msg="Should not find array at wrong nesting level", + ), +] + +# Aggregate and test +ALL_TESTS = NESTED_MIXED_ARRAY_TESTS + DEEPLY_NESTED_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_in_nested_insert(collection, test): + """Test $in nested array matching 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/in/test_expression_in_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py similarity index 61% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_expression_in_null_missing.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py index 639fc3d50..292fb5b26 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_in_null_missing.py @@ -4,48 +4,47 @@ import pytest -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.expression_test_case import ( # noqa: E501 + ExpressionTestCase, ) 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[ArrayTestClass] = [ - ArrayTestClass( +NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="null_value_in_array", - value=None, - array=[1, None, 3], + doc={"val": None, "arr": [1, None, 3]}, + expression={"$in": ["$val", "$arr"]}, expected=True, msg="Should find null value in array containing null", ), - ArrayTestClass( + ExpressionTestCase( id="null_value_not_in_array", - value=None, - array=[1, 2, 3], + doc={"val": None, "arr": [1, 2, 3]}, + expression={"$in": ["$val", "$arr"]}, 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[ArrayTestClass] = [ - ArrayTestClass( +LITERAL_ONLY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="missing_value", - value=MISSING, - array=[1, 2, 3], + doc={"val": MISSING, "arr": [1, 2, 3]}, + expression={"$in": ["$val", "$arr"]}, expected=False, msg="Should not find missing value in array", ), - ArrayTestClass( + ExpressionTestCase( id="missing_value_null_in_array", - value=MISSING, - array=[1, None, 3], + doc={"val": MISSING, "arr": [1, None, 3]}, + expression={"$in": ["$val", "$arr"]}, expected=False, msg="Should not find missing value even with null in array", ), @@ -58,21 +57,10 @@ ] + 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} - ) + 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/in/test_smoke_expression_in.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_in.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_expression_in.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_in.py 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_indexOfArray_bson_types.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_bson_types.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py 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_indexOfArray_core_behavior.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_core_behavior.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py 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_indexOfArray_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py 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_indexOfArray_expressions.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_expressions.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_expressions.py 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_indexOfArray_null_missing.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_expression_indexOfArray_null_missing.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py 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_indexOfArray.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_expression_indexOfArray.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_indexOfArray.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 deleted file mode 100644 index ad84ecf36..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_bson_types.py +++ /dev/null @@ -1,301 +0,0 @@ -""" -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, timezone - -import pytest -from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp - -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, - 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[ArrayTestClass] = [ - ArrayTestClass( - id="bindata_array", - value=[Binary(b"\x00", 0)], - expected=True, - msg="Should return true for BinData array", - ), - ArrayTestClass( - id="timestamp_array", - value=[Timestamp(0, 0)], - expected=True, - msg="Should return true for Timestamp array", - ), - ArrayTestClass( - id="int64_array", value=[Int64(1)], expected=True, msg="Should return true for Int64 array" - ), - ArrayTestClass( - id="decimal128_array", - value=[Decimal128("1")], - expected=True, - msg="Should return true for Decimal128 array", - ), - ArrayTestClass( - id="objectid_array", - value=[ObjectId()], - expected=True, - msg="Should return true for ObjectId array", - ), - ArrayTestClass( - id="datetime_array", - value=[datetime(2024, 1, 1, tzinfo=timezone.utc)], - expected=True, - msg="Should return true for datetime array", - ), - ArrayTestClass( - id="minkey_array", - value=[MinKey()], - expected=True, - msg="Should return true for MinKey array", - ), - ArrayTestClass( - id="maxkey_array", - value=[MaxKey()], - expected=True, - msg="Should return true for MaxKey array", - ), - ArrayTestClass( - id="regex_array", - value=[Regex(".*")], - expected=True, - msg="Should return true for Regex array", - ), - ArrayTestClass( - id="nan_array", value=[float("nan")], expected=True, msg="Should return true for NaN array" - ), - ArrayTestClass( - id="inf_array", - value=[float("inf")], - expected=True, - msg="Should return true for Infinity array", - ), - ArrayTestClass( - id="decimal128_nan_array", - value=[Decimal128("NaN")], - expected=True, - msg="Should return true for Decimal128 NaN array", - ), - ArrayTestClass( - id="decimal128_inf_array", - value=[Decimal128("Infinity")], - expected=True, - msg="Should return true for Decimal128 Infinity array", - ), - ArrayTestClass( - id="decimal128_neg_nan_array", - value=[Decimal128("-NaN")], - expected=True, - msg="Should return true for Decimal128 -NaN array", - ), - ArrayTestClass( - id="decimal128_neg_inf_array", - value=[DECIMAL128_NEGATIVE_INFINITY], - expected=True, - msg="Should return true for Decimal128 -Infinity array", - ), - ArrayTestClass( - id="neg_inf_array", - value=[FLOAT_NEGATIVE_INFINITY], - expected=True, - msg="Should return true for -Infinity array", - ), - ArrayTestClass( - id="neg_zero_array", - value=[DOUBLE_NEGATIVE_ZERO], - expected=True, - msg="Should return true for negative zero array", - ), - ArrayTestClass( - id="decimal128_neg_zero_array", - value=[DECIMAL128_NEGATIVE_ZERO], - expected=True, - msg="Should return true for Decimal128 -0 array", - ), - ArrayTestClass( - id="nested_mixed_bson_array", - value=[ - MinKey(), - {"a": [Decimal128("1.5")]}, - Int64(1), - datetime(2024, 1, 1, tzinfo=timezone.utc), - Binary(b"\x01", 0), - ], - expected=True, - msg="Should return true for nested mixed BSON array", - ), -] - -# 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( - id="decimal128", - value=Decimal128("1"), - expected=False, - msg="Should return false for Decimal128", - ), - ArrayTestClass( - id="objectid", - value=ObjectId("000000000000000000000001"), - expected=False, - msg="Should return false for ObjectId", - ), - ArrayTestClass( - id="datetime", - value=datetime(2024, 1, 1, tzinfo=timezone.utc), - expected=False, - msg="Should return false for datetime", - ), - ArrayTestClass( - id="binary", value=Binary(b"\x01", 0), expected=False, msg="Should return false for Binary" - ), - ArrayTestClass( - id="regex", value=Regex("^abc"), expected=False, msg="Should return false for Regex" - ), - ArrayTestClass( - id="timestamp", - value=Timestamp(1, 1), - expected=False, - msg="Should return false for Timestamp", - ), - 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[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" - ), - ArrayTestClass( - id="neg_inf", - value=FLOAT_NEGATIVE_INFINITY, - expected=False, - msg="Should return false for -Infinity", - ), - ArrayTestClass( - id="neg_zero", - value=DOUBLE_NEGATIVE_ZERO, - expected=False, - msg="Should return false for negative zero", - ), - ArrayTestClass( - id="decimal128_nan", - value=DECIMAL128_NAN, - expected=False, - msg="Should return false for Decimal128 NaN", - ), - ArrayTestClass( - id="decimal128_neg_nan", - value=Decimal128("-NaN"), - expected=False, - msg="Should return false for Decimal128 -NaN", - ), - ArrayTestClass( - id="decimal128_inf", - value=DECIMAL128_INFINITY, - expected=False, - msg="Should return false for Decimal128 Infinity", - ), - ArrayTestClass( - id="decimal128_neg_inf", - value=DECIMAL128_NEGATIVE_INFINITY, - expected=False, - msg="Should return false for Decimal128 -Infinity", - ), - ArrayTestClass( - id="decimal128_neg_zero", - value=DECIMAL128_NEGATIVE_ZERO, - expected=False, - msg="Should return false for Decimal128 -0", - ), -] - -# Boundary values → false -BOUNDARY_TESTS: list[ArrayTestClass] = [ - ArrayTestClass( - id="int32_max", value=INT32_MAX, expected=False, msg="Should return false for INT32_MAX" - ), - ArrayTestClass( - id="int32_min", value=INT32_MIN, expected=False, msg="Should return false for INT32_MIN" - ), - ArrayTestClass( - id="int64_max", value=INT64_MAX, expected=False, msg="Should return false for INT64_MAX" - ), - ArrayTestClass( - id="int64_min", value=INT64_MIN, expected=False, msg="Should return false for INT64_MIN" - ), - ArrayTestClass( - id="decimal128_max", - value=DECIMAL128_MAX, - expected=False, - msg="Should return false for DECIMAL128_MAX", - ), - ArrayTestClass( - 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 deleted file mode 100644 index 7349ca2aa..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_core_behavior.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -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.utils.array_test_case import ( # noqa: E501 - ArrayTestClass, -) -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[ArrayTestClass] = [ - ArrayTestClass( - id="simple_array", - value=[1, 2, 3], - expected=True, - msg="Should return true for simple array", - ), - ArrayTestClass( - id="empty_array", - value=[], - expected=True, - msg="Should return true for empty array", - ), - ArrayTestClass( - id="single_element", - value=[42], - expected=True, - msg="Should return true for single-element array", - ), - ArrayTestClass( - id="nested_array", - value=[[1, 2], [3, 4]], - expected=True, - msg="Should return true for nested array", - ), - ArrayTestClass( - id="mixed_type_array", - value=[1, "two", True, None, {"a": 1}], - expected=True, - msg="Should return true for mixed-type array", - ), - ArrayTestClass( - id="array_of_objects", - value=[{"a": 1}, {"b": 2}], - expected=True, - msg="Should return true for array of objects", - ), - ArrayTestClass( - id="array_of_nulls", - value=[None, None], - expected=True, - msg="Should return true for array of nulls", - ), - ArrayTestClass( - id="string_array", - value=["a", "b", "c"], - expected=True, - msg="Should return true for string array", - ), - ArrayTestClass( - id="bool_array", - value=[True], - expected=True, - msg="Should return true for bool array", - ), - ArrayTestClass( - id="large_array_10k", - value=list(range(10000)), - expected=True, - msg="10K element array returns true", - ), - ArrayTestClass( - id="deeply_nested_array", - value=[[[[[[1]]]]]], - expected=True, - msg="Deeply nested array returns true", - ), - ArrayTestClass( - 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[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" - ), - ArrayTestClass( - id="empty_object", value={}, expected=False, msg="Should return false for empty object" - ), - 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" - ), - ArrayTestClass( - id="negative_double", - value=-1.23, - expected=False, - msg="Should return false for negative double", - ), -] - -# Array-like edge cases → false -ARRAY_LIKE_TESTS: list[ArrayTestClass] = [ - ArrayTestClass( - id="string_brackets", value="[]", expected=False, msg="Should return false for string '[]'" - ), - ArrayTestClass( - id="string_array_repr", - value="[1, 2, 3]", - expected=False, - msg="Should return false for string '[1, 2, 3]'", - ), - ArrayTestClass( - id="array_like_object", - value={"0": "a", "1": "b"}, - expected=False, - msg="Should return false for array-like object", - ), - ArrayTestClass( - 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_isArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py new file mode 100644 index 000000000..2dfeb5979 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py @@ -0,0 +1,372 @@ +""" +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, timezone + +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_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[ExpressionTestCase] = [ + ExpressionTestCase( + id="bindata_array", + doc={"val": [Binary(b"\x00", 0)]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for BinData array", + ), + ExpressionTestCase( + id="timestamp_array", + doc={"val": [Timestamp(0, 0)]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Timestamp array", + ), + ExpressionTestCase( + id="int64_array", + doc={"val": [Int64(1)]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Int64 array", + ), + ExpressionTestCase( + id="decimal128_array", + doc={"val": [Decimal128("1")]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Decimal128 array", + ), + ExpressionTestCase( + id="objectid_array", + doc={"val": [ObjectId()]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for ObjectId array", + ), + ExpressionTestCase( + id="datetime_array", + doc={"val": [datetime(2024, 1, 1, tzinfo=timezone.utc)]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for datetime array", + ), + ExpressionTestCase( + id="minkey_array", + doc={"val": [MinKey()]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for MinKey array", + ), + ExpressionTestCase( + id="maxkey_array", + doc={"val": [MaxKey()]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for MaxKey array", + ), + ExpressionTestCase( + id="regex_array", + doc={"val": [Regex(".*")]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Regex array", + ), + ExpressionTestCase( + id="nan_array", + doc={"val": [float("nan")]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for NaN array", + ), + ExpressionTestCase( + id="inf_array", + doc={"val": [float("inf")]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Infinity array", + ), + ExpressionTestCase( + id="decimal128_nan_array", + doc={"val": [Decimal128("NaN")]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Decimal128 NaN array", + ), + ExpressionTestCase( + id="decimal128_inf_array", + doc={"val": [Decimal128("Infinity")]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Decimal128 Infinity array", + ), + ExpressionTestCase( + id="decimal128_neg_nan_array", + doc={"val": [Decimal128("-NaN")]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Decimal128 -NaN array", + ), + ExpressionTestCase( + id="decimal128_neg_inf_array", + doc={"val": [DECIMAL128_NEGATIVE_INFINITY]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Decimal128 -Infinity array", + ), + ExpressionTestCase( + id="neg_inf_array", + doc={"val": [FLOAT_NEGATIVE_INFINITY]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for -Infinity array", + ), + ExpressionTestCase( + id="neg_zero_array", + doc={"val": [DOUBLE_NEGATIVE_ZERO]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for negative zero array", + ), + ExpressionTestCase( + id="decimal128_neg_zero_array", + doc={"val": [DECIMAL128_NEGATIVE_ZERO]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for Decimal128 -0 array", + ), + ExpressionTestCase( + id="nested_mixed_bson_array", + doc={ + "val": [ + MinKey(), + {"a": [Decimal128("1.5")]}, + Int64(1), + datetime(2024, 1, 1, tzinfo=timezone.utc), + Binary(b"\x01", 0), + ] + }, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for nested mixed BSON array", + ), +] + +# Non-array BSON types → false +BSON_FALSE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="int64", + doc={"val": Int64(1)}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Int64", + ), + ExpressionTestCase( + id="decimal128", + doc={"val": Decimal128("1")}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Decimal128", + ), + ExpressionTestCase( + id="objectid", + doc={"val": ObjectId("000000000000000000000001")}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for ObjectId", + ), + ExpressionTestCase( + id="datetime", + doc={"val": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for datetime", + ), + ExpressionTestCase( + id="binary", + doc={"val": Binary(b"\x01", 0)}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Binary", + ), + ExpressionTestCase( + id="regex", + doc={"val": Regex("^abc")}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Regex", + ), + ExpressionTestCase( + id="timestamp", + doc={"val": Timestamp(1, 1)}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Timestamp", + ), + ExpressionTestCase( + id="minkey", + doc={"val": MinKey()}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for MinKey", + ), + ExpressionTestCase( + id="maxkey", + doc={"val": MaxKey()}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for MaxKey", + ), +] + +# Special numeric values → false +SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nan", + doc={"val": FLOAT_NAN}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for NaN", + ), + ExpressionTestCase( + id="inf", + doc={"val": FLOAT_INFINITY}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Infinity", + ), + ExpressionTestCase( + id="neg_inf", + doc={"val": FLOAT_NEGATIVE_INFINITY}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for -Infinity", + ), + ExpressionTestCase( + id="neg_zero", + doc={"val": DOUBLE_NEGATIVE_ZERO}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for negative zero", + ), + ExpressionTestCase( + id="decimal128_nan", + doc={"val": DECIMAL128_NAN}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Decimal128 NaN", + ), + ExpressionTestCase( + id="decimal128_neg_nan", + doc={"val": Decimal128("-NaN")}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Decimal128 -NaN", + ), + ExpressionTestCase( + id="decimal128_inf", + doc={"val": DECIMAL128_INFINITY}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Decimal128 Infinity", + ), + ExpressionTestCase( + id="decimal128_neg_inf", + doc={"val": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Decimal128 -Infinity", + ), + ExpressionTestCase( + id="decimal128_neg_zero", + doc={"val": DECIMAL128_NEGATIVE_ZERO}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for Decimal128 -0", + ), +] + +# Boundary values → false +BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="int32_max", + doc={"val": INT32_MAX}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for INT32_MAX", + ), + ExpressionTestCase( + id="int32_min", + doc={"val": INT32_MIN}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for INT32_MIN", + ), + ExpressionTestCase( + id="int64_max", + doc={"val": INT64_MAX}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for INT64_MAX", + ), + ExpressionTestCase( + id="int64_min", + doc={"val": INT64_MIN}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for INT64_MIN", + ), + ExpressionTestCase( + id="decimal128_max", + doc={"val": DECIMAL128_MAX}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for DECIMAL128_MAX", + ), + ExpressionTestCase( + id="decimal128_min", + doc={"val": DECIMAL128_MIN}, + expression={"$isArray": "$val"}, + 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, 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/isArray/test_isArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py new file mode 100644 index 000000000..fa0634292 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py @@ -0,0 +1,329 @@ +""" +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.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +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[ExpressionTestCase] = [ + ExpressionTestCase( + id="simple_array", + doc={"val": [1, 2, 3]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for simple array", + ), + ExpressionTestCase( + id="empty_array", + doc={"val": []}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for empty array", + ), + ExpressionTestCase( + id="single_element", + doc={"val": [42]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for single-element array", + ), + ExpressionTestCase( + id="nested_array", + doc={"val": [[1, 2], [3, 4]]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for nested array", + ), + ExpressionTestCase( + id="mixed_type_array", + doc={"val": [1, "two", True, None, {"a": 1}]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for mixed-type array", + ), + ExpressionTestCase( + id="array_of_objects", + doc={"val": [{"a": 1}, {"b": 2}]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for array of objects", + ), + ExpressionTestCase( + id="array_of_nulls", + doc={"val": [None, None]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for array of nulls", + ), + ExpressionTestCase( + id="string_array", + doc={"val": ["a", "b", "c"]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for string array", + ), + ExpressionTestCase( + id="bool_array", + doc={"val": [True]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Should return true for bool array", + ), + ExpressionTestCase( + id="large_array_10k", + doc={"val": list(range(10000))}, + expression={"$isArray": "$val"}, + expected=True, + msg="10K element array returns true", + ), + ExpressionTestCase( + id="deeply_nested_array", + doc={"val": [[[[[[1]]]]]]}, + expression={"$isArray": "$val"}, + expected=True, + msg="Deeply nested array returns true", + ), + ExpressionTestCase( + id="large_array_of_arrays", + doc={"val": [[i] for i in range(10000)]}, + expression={"$isArray": "$val"}, + expected=True, + msg="10K nested arrays returns true", + ), +] + +# Success: non-arrays → false +IS_ARRAY_FALSE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="string", + doc={"val": "hello"}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for string", + ), + ExpressionTestCase( + id="int", + doc={"val": 42}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for int", + ), + ExpressionTestCase( + id="double", + doc={"val": 3.14}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for double", + ), + ExpressionTestCase( + id="bool_true", + doc={"val": True}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for true", + ), + ExpressionTestCase( + id="bool_false", + doc={"val": False}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for false", + ), + ExpressionTestCase( + id="null", + doc={"val": None}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for null", + ), + ExpressionTestCase( + id="object", + doc={"val": {"a": 1}}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for object", + ), + ExpressionTestCase( + id="empty_string", + doc={"val": ""}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for empty string", + ), + ExpressionTestCase( + id="empty_object", + doc={"val": {}}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for empty object", + ), + ExpressionTestCase( + id="zero", + doc={"val": 0}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for zero", + ), + ExpressionTestCase( + id="negative_int", + doc={"val": -123}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for negative int", + ), + ExpressionTestCase( + id="negative_double", + doc={"val": -1.23}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for negative double", + ), +] + +# Array-like edge cases → false +ARRAY_LIKE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="string_brackets", + doc={"val": "[]"}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for string '[]'", + ), + ExpressionTestCase( + id="string_array_repr", + doc={"val": "[1, 2, 3]"}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for string '[1, 2, 3]'", + ), + ExpressionTestCase( + id="array_like_object", + doc={"val": {"0": "a", "1": "b"}}, + expression={"$isArray": "$val"}, + expected=False, + msg="Should return false for array-like object", + ), + ExpressionTestCase( + id="length_object", + doc={"val": {"length": 3}}, + expression={"$isArray": "$val"}, + 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, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +# Property [Literal Evaluation]: $isArray evaluates correctly with inline literal values. +TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_simple_array", + doc=None, + expression={"$isArray": [{"$literal": [1, 2, 3]}]}, + expected=True, + msg="$isArray should return true for literal simple array", + ), + ExpressionTestCase( + "literal_empty_array", + doc=None, + expression={"$isArray": [{"$literal": []}]}, + expected=True, + msg="$isArray should return true for literal empty array", + ), + ExpressionTestCase( + "literal_nested_array", + doc=None, + expression={"$isArray": [{"$literal": [[1], [2]]}]}, + expected=True, + msg="$isArray should return true for literal nested array", + ), + ExpressionTestCase( + "literal_array_of_objects", + doc=None, + expression={"$isArray": [{"$literal": [{"a": 1}, {"b": 2}]}]}, + expected=True, + msg="$isArray should return true for literal array of objects", + ), + ExpressionTestCase( + "literal_array_of_nulls", + doc=None, + expression={"$isArray": [{"$literal": [None, None]}]}, + expected=True, + msg="$isArray should return true for literal array of nulls", + ), + ExpressionTestCase( + "literal_string", + doc=None, + expression={"$isArray": ["hello"]}, + expected=False, + msg="$isArray should return false for literal string", + ), + ExpressionTestCase( + "literal_int", + doc=None, + expression={"$isArray": [42]}, + expected=False, + msg="$isArray should return false for literal int", + ), + ExpressionTestCase( + "literal_bool_true", + doc=None, + expression={"$isArray": [True]}, + expected=False, + msg="$isArray should return false for literal true", + ), + ExpressionTestCase( + "literal_bool_false", + doc=None, + expression={"$isArray": [False]}, + expected=False, + msg="$isArray should return false for literal false", + ), + ExpressionTestCase( + "literal_null", + doc=None, + expression={"$isArray": [None]}, + expected=False, + msg="$isArray should return false for literal null", + ), + ExpressionTestCase( + "literal_object", + doc=None, + expression={"$isArray": [{"$literal": {"a": 1}}]}, + expected=False, + msg="$isArray should return false for literal object", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_isArray_literal(collection, test): + """Test $isArray with literal values.""" + result = execute_expression(collection, test.expression) + 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_isArray_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_errors.py 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_isArray_expressions.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_expression_isArray_expressions.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_expressions.py 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_isArray.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_expression_isArray.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_isArray.py 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 deleted file mode 100644 index e69de29bb..000000000 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 deleted file mode 100644 index 7c26bd337..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -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 From 7d9a86085f2ba22764822485b71527779636d240 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 7 Jul 2026 13:27:20 -0700 Subject: [PATCH 12/21] add back removed tests Signed-off-by: Alina (Xi) Li --- .../array/in/test_in_nested_arrays.py | 38 ++++++++++++++++ .../array/isArray/test_isArray_bson_types.py | 43 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py index 85f28b4eb..06ac47ed0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py @@ -14,6 +14,7 @@ ) 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 @@ -165,3 +166,40 @@ def test_in_nested_insert(collection, test): assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) + + +# Property [Literal Evaluation]: $in nested array matching works with inline literal values. +TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_nested_find_object_in_mixed", + doc=None, + expression={"$in": [{"a": 1}, {"$literal": [1, "two", {"a": 1}, [3, 4], True]}]}, + expected=True, + msg="$in should find object in literal nested mixed array", + ), + ExpressionTestCase( + "literal_nested_3_levels", + doc=None, + expression={ + "$in": [{"$literal": [[2, 3], [4, 5]]}, {"$literal": [1, [[2, 3], [4, 5]], "end"]}] + }, + expected=True, + msg="$in should find 3-level nested array in literal", + ), + ExpressionTestCase( + "literal_nested_deep_not_found", + doc=None, + expression={"$in": [{"$literal": [2, 3]}, {"$literal": [[1, [2, 3]], [4, 5]]}]}, + expected=False, + msg="$in should not find array at wrong nesting level in literal", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_in_nested_literal(collection, test): + """Test $in nested arrays with literal values.""" + result = execute_expression(collection, test.expression) + 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_isArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py index 2dfeb5979..748bb73be 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py @@ -16,6 +16,7 @@ ) 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 @@ -370,3 +371,45 @@ def test_isArray_bson_insert(collection, test): """Test $isArray 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) + + +# Property [Literal Evaluation]: $isArray BSON type detection works with inline literal values. +TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_bindata_array", + doc=None, + expression={"$isArray": [{"$literal": [Binary(b"\x00", 0)]}]}, + expected=True, + msg="$isArray should return true for literal BinData array", + ), + ExpressionTestCase( + "literal_nested_mixed_bson_array", + doc=None, + expression={"$isArray": [{"$literal": [MinKey(), {"a": [Decimal128("1.5")]}, Int64(1)]}]}, + expected=True, + msg="$isArray should return true for literal nested mixed BSON array", + ), + ExpressionTestCase( + "literal_int64", + doc=None, + expression={"$isArray": [Int64(1)]}, + expected=False, + msg="$isArray should return false for literal Int64", + ), + ExpressionTestCase( + "literal_nan", + doc=None, + expression={"$isArray": [FLOAT_NAN]}, + expected=False, + msg="$isArray should return false for literal 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.""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) From 5ca0514a74a036803041a45324b51c9e33d80fb3 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 7 Jul 2026 13:31:05 -0700 Subject: [PATCH 13/21] merge same functions together Signed-off-by: Alina (Xi) Li --- .../array/in/test_in_core_behavior.py | 27 +++++++-------- .../array/in/test_in_nested_arrays.py | 25 +++++--------- .../array/isArray/test_isArray_bson_types.py | 33 +++++++++---------- .../isArray/test_isArray_core_behavior.py | 27 +++++++-------- 4 files changed, 47 insertions(+), 65 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py index ace925d4d..007927bb6 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py @@ -223,18 +223,6 @@ ] # Aggregate and test -ALL_TESTS = FOUND_TESTS + NOT_FOUND_TESTS + MIXED_TYPE_TESTS + LARGE_ARRAY_TESTS - - -@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, test.expression, test.doc) - assert_expression_result( - result, expected=test.expected, error_code=test.error_code, msg=test.msg - ) - - # Property [Literal Evaluation]: $in evaluates correctly with inline literal values. TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -260,11 +248,18 @@ def test_in_insert(collection, test): ), ] +ALL_TESTS = ( + FOUND_TESTS + NOT_FOUND_TESTS + MIXED_TYPE_TESTS + LARGE_ARRAY_TESTS + TEST_SUBSET_FOR_LITERAL +) + -@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, test.expression) +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_in_insert(collection, test): + """Test $in with values from inserted documents.""" + if test.doc is None: + result = execute_expression(collection, test.expression) + else: + 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/in/test_in_nested_arrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py index 06ac47ed0..c302631d8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py @@ -156,18 +156,6 @@ ] # Aggregate and test -ALL_TESTS = NESTED_MIXED_ARRAY_TESTS + DEEPLY_NESTED_TESTS - - -@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) -def test_in_nested_insert(collection, test): - """Test $in nested array matching 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 - ) - - # Property [Literal Evaluation]: $in nested array matching works with inline literal values. TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -195,11 +183,16 @@ def test_in_nested_insert(collection, test): ), ] +ALL_TESTS = NESTED_MIXED_ARRAY_TESTS + DEEPLY_NESTED_TESTS + TEST_SUBSET_FOR_LITERAL + -@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) -def test_in_nested_literal(collection, test): - """Test $in nested arrays with literal values.""" - result = execute_expression(collection, test.expression) +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_in_nested_insert(collection, test): + """Test $in nested array matching with values from inserted documents.""" + if test.doc is None: + result = execute_expression(collection, test.expression) + else: + 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_isArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py index 748bb73be..b1d430550 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py @@ -363,16 +363,6 @@ ] # 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, test.expression, test.doc) - assert_expression_result(result, expected=test.expected, msg=test.msg) - - # Property [Literal Evaluation]: $isArray BSON type detection works with inline literal values. TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -405,11 +395,20 @@ def test_isArray_bson_insert(collection, test): ), ] +ALL_BSON_TESTS = ( + BSON_ARRAY_TRUE_TESTS + + BSON_FALSE_TESTS + + SPECIAL_NUMERIC_TESTS + + BOUNDARY_TESTS + + TEST_SUBSET_FOR_LITERAL +) + -@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) -def test_isArray_bson_literal(collection, test): - """Test $isArray BSON types with literal values.""" - result = execute_expression(collection, test.expression) - assert_expression_result( - result, expected=test.expected, error_code=test.error_code, msg=test.msg - ) +@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.""" + if test.doc is None: + result = execute_expression(collection, test.expression) + else: + 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/isArray/test_isArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py index fa0634292..ba05d3a46 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py @@ -226,18 +226,6 @@ ] # 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, test.expression, test.doc) - assert_expression_result( - result, expected=test.expected, error_code=test.error_code, msg=test.msg - ) - - # Property [Literal Evaluation]: $isArray evaluates correctly with inline literal values. TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -319,11 +307,18 @@ def test_isArray_insert(collection, test): ), ] +INSERT_TESTS = ( + IS_ARRAY_TRUE_TESTS + IS_ARRAY_FALSE_TESTS + ARRAY_LIKE_TESTS + TEST_SUBSET_FOR_LITERAL +) + -@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) -def test_isArray_literal(collection, test): - """Test $isArray with literal values.""" - result = execute_expression(collection, test.expression) +@pytest.mark.parametrize("test", pytest_params(INSERT_TESTS)) +def test_isArray_insert(collection, test): + """Test $isArray with values from inserted documents.""" + if test.doc is None: + result = execute_expression(collection, test.expression) + else: + 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 1606e79a251210500b164c5964d4372d1f311132 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 15:19:42 -0700 Subject: [PATCH 14/21] convert ARITY_ERROR_TESTS to use ExpressionTestCase Signed-off-by: Alina (Xi) Li --- .../expressions/array/in/test_in_errors.py | 126 +++++++++--------- 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_errors.py index 90a1acb26..279549c19 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_errors.py @@ -24,159 +24,165 @@ 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) +# Property [Not Array]: $in rejects non-array second argument across all BSON types. NOT_ARRAY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string_as_array", + "string_as_array", doc={"val": 1, "arr": "hello"}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject string as array arg", + msg="$in should reject string as array arg", ), ExpressionTestCase( - id="int_as_array", + "int_as_array", doc={"val": 1, "arr": 42}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject int as array arg", + msg="$in should reject int as array arg", ), ExpressionTestCase( - id="double_as_array", + "double_as_array", doc={"val": 1, "arr": 3.14}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject double as array arg", + msg="$in should reject double as array arg", ), ExpressionTestCase( - id="bool_true_as_array", + "bool_true_as_array", doc={"val": 1, "arr": True}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject bool true as array arg", + msg="$in should reject bool true as array arg", ), ExpressionTestCase( - id="bool_false_as_array", + "bool_false_as_array", doc={"val": 1, "arr": False}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject bool false as array arg", + msg="$in should reject bool false as array arg", ), ExpressionTestCase( - id="object_as_array", + "object_as_array", doc={"val": 1, "arr": {"a": 1}}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject object as array arg", + msg="$in should reject object as array arg", ), ExpressionTestCase( - id="decimal128_as_array", + "decimal128_as_array", doc={"val": 1, "arr": Decimal128("1")}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject decimal128 as array arg", + msg="$in should reject decimal128 as array arg", ), ExpressionTestCase( - id="int64_as_array", + "int64_as_array", doc={"val": 1, "arr": Int64(1)}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject int64 as array arg", + msg="$in should reject int64 as array arg", ), ExpressionTestCase( - id="binary_as_array", + "binary_as_array", doc={"val": 1, "arr": Binary(b"x", 0)}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject binary as array arg", + msg="$in should reject binary as array arg", ), ExpressionTestCase( - id="datetime_as_array", + "datetime_as_array", doc={"val": 1, "arr": datetime(2024, 1, 1, tzinfo=timezone.utc)}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject datetime as array arg", + msg="$in should reject datetime as array arg", ), ExpressionTestCase( - id="objectid_as_array", + "objectid_as_array", doc={"val": 1, "arr": ObjectId()}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject objectid as array arg", + msg="$in should reject objectid as array arg", ), ExpressionTestCase( - id="regex_as_array", + "regex_as_array", doc={"val": 1, "arr": Regex("x")}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject regex as array arg", + msg="$in should reject regex as array arg", ), ExpressionTestCase( - id="maxkey_as_array", + "maxkey_as_array", doc={"val": 1, "arr": MaxKey()}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject maxkey as array arg", + msg="$in should reject maxkey as array arg", ), ExpressionTestCase( - id="minkey_as_array", + "minkey_as_array", doc={"val": 1, "arr": MinKey()}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject minkey as array arg", + msg="$in should reject minkey as array arg", ), ExpressionTestCase( - id="timestamp_as_array", + "timestamp_as_array", doc={"val": 1, "arr": Timestamp(0, 0)}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject timestamp as array arg", + msg="$in should reject timestamp as array arg", ), ExpressionTestCase( - id="null_as_array", + "null_as_array", doc={"val": 1, "arr": None}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject null as array arg", + msg="$in should reject null as array arg", ), ] -# Error: missing as array (literal only, MISSING is a field ref) +# Property [Missing Field]: $in rejects a missing field as the second argument. LITERAL_ONLY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="missing_as_array", + "missing_as_array", doc={"val": 1, "arr": MISSING}, expression={"$in": ["$val", "$arr"]}, error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, - msg="Should reject missing as array arg", + msg="$in 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 +# Property [Arity]: $in requires exactly two arguments. +ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_args", + expression={"$in": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$in should reject zero arguments", + ), + ExpressionTestCase( + "one_arg", + expression={"$in": [[1, 2, 3]]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$in should reject one argument", + ), + ExpressionTestCase( + "three_args", + expression={"$in": [1, [1, 2], 3]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$in should reject three arguments", + ), +] +ALL_ERROR_TESTS = NOT_ARRAY_ERROR_TESTS + LITERAL_ONLY_TESTS + ARITY_ERROR_TESTS -@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, test.expression, test.doc) + +@pytest.mark.parametrize("test", pytest_params(ALL_ERROR_TESTS)) +def test_in_error(collection, test): + """Test $in error cases.""" + if test.doc is None: + result = execute_expression(collection, test.expression) + else: + 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 ) - - -# 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) From 330a579dfd56ab528d357e477c5f3a451490d381 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 15:31:15 -0700 Subject: [PATCH 15/21] apply style guide changes Signed-off-by: Alina (Xi) Li --- .../array/filter/test_filter_as_errors.py | 64 ++-- .../array/filter/test_filter_bson_types.py | 116 +++---- .../array/filter/test_filter_core_behavior.py | 184 +++++------ .../array/filter/test_filter_errors.py | 204 ++++++------- .../array/filter/test_filter_expressions.py | 50 +-- .../filter/test_filter_structure_errors.py | 40 +-- .../array/filter/test_smoke_filter.py | 2 +- .../array/in/test_in_bson_types.py | 116 +++---- .../array/in/test_in_core_behavior.py | 108 +++---- .../array/in/test_in_nested_arrays.py | 64 ++-- .../array/in/test_in_null_missing.py | 16 +- .../expressions/array/in/test_smoke_in.py | 2 +- .../test_indexOfArray_bson_types.py | 192 ++++++------ .../test_indexOfArray_core_behavior.py | 288 +++++++++--------- .../indexOfArray/test_indexOfArray_errors.py | 250 ++++++++------- .../test_indexOfArray_null_missing.py | 28 +- .../indexOfArray/test_smoke_indexOfArray.py | 4 +- .../array/isArray/test_isArray_bson_types.py | 172 +++++------ .../isArray/test_isArray_core_behavior.py | 112 +++---- .../array/isArray/test_isArray_errors.py | 43 ++- .../array/isArray/test_isArray_expressions.py | 54 ++-- .../array/isArray/test_smoke_isArray.py | 2 +- ...array_arrayElemAt_indexOfArray_in_slice.py | 132 ++++---- .../test_expressions_combination_filter.py | 12 +- .../test_expressions_combination_isArray.py | 8 +- 25 files changed, 1157 insertions(+), 1106 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_as_errors.py index 5a0861764..b1d2089ea 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_as_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_as_errors.py @@ -21,55 +21,55 @@ INVALID_AS_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="type_int", + "type_int", expression={"$filter": {"input": [1, 2, 3], "as": 1, "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=int should error", + msg="$filter should reject int as variable name", ), ExpressionTestCase( - id="type_long", + "type_long", expression={"$filter": {"input": [1, 2, 3], "as": Int64(1), "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=Int64 should error", + msg="$filter should reject Int64 as variable name", ), ExpressionTestCase( - id="type_object", + "type_object", expression={"$filter": {"input": [1, 2, 3], "as": {"a": 1}, "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=object should error", + msg="$filter should reject object as variable name", ), ExpressionTestCase( - id="type_array", + "type_array", expression={"$filter": {"input": [1, 2, 3], "as": [1], "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=array should error", + msg="$filter should reject array as variable name", ), ExpressionTestCase( - id="type_minkey", + "type_minkey", expression={"$filter": {"input": [1, 2, 3], "as": MinKey(), "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=MinKey should error", + msg="$filter should reject MinKey as variable name", ), ExpressionTestCase( - id="type_maxkey", + "type_maxkey", expression={"$filter": {"input": [1, 2, 3], "as": MaxKey(), "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=MaxKey should error", + msg="$filter should reject MaxKey as variable name", ), ExpressionTestCase( - id="type_bindata", + "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", + msg="$filter should reject Binary as variable name", ), ExpressionTestCase( - id="type_objectid", + "type_objectid", expression={"$filter": {"input": [1, 2, 3], "as": ObjectId(), "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=ObjectId should error", + msg="$filter should reject ObjectId as variable name", ), ExpressionTestCase( - id="type_date", + "type_date", expression={ "$filter": { "input": [1, 2, 3], @@ -78,49 +78,49 @@ } }, error_code=FAILED_TO_PARSE_ERROR, - msg="as=datetime should error", + msg="$filter should reject datetime as variable name", ), ExpressionTestCase( - id="type_timestamp", + "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", + msg="$filter should reject Timestamp as variable name", ), ExpressionTestCase( - id="type_regex", + "type_regex", expression={"$filter": {"input": [1, 2, 3], "as": Regex("pattern"), "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=Regex should error", + msg="$filter should reject Regex as variable name", ), ExpressionTestCase( - id="type_bool_true", + "type_bool_true", expression={"$filter": {"input": [1, 2, 3], "as": True, "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=True should error", + msg="$filter should reject True as variable name", ), ExpressionTestCase( - id="type_null", + "type_null", expression={"$filter": {"input": [1, 2, 3], "as": None, "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=None should error", + msg="$filter should reject None as variable name", ), ExpressionTestCase( - id="type_empty_string", + "type_empty_string", expression={"$filter": {"input": [1, 2, 3], "as": "", "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as='' should error", + msg="$filter should reject '' as variable name", ), ExpressionTestCase( - id="type_nan", + "type_nan", expression={"$filter": {"input": [1, 2, 3], "as": float("nan"), "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=NaN should error", + msg="$filter should reject NaN as variable name", ), ExpressionTestCase( - id="type_infinity", + "type_infinity", expression={"$filter": {"input": [1, 2, 3], "as": float("inf"), "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=inf should error", + msg="$filter should reject inf as variable name", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_bson_types.py index 187396bfe..52e43899d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_bson_types.py @@ -37,21 +37,21 @@ # BSON types preserved BSON_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int64_values", + "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", + msg="$filter should preserve Int64 values", ), ExpressionTestCase( - id="decimal128_values", + "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", + msg="$filter should preserve Decimal128 values", ), ExpressionTestCase( - id="datetime_values", + "datetime_values", expression={"$filter": {"input": "$arr", "cond": True}}, doc={ "arr": [ @@ -63,52 +63,52 @@ datetime(2024, 1, 1, tzinfo=timezone.utc), datetime(2024, 6, 1, tzinfo=timezone.utc), ], - msg="Should preserve datetime values", + msg="$filter should preserve datetime values", ), ExpressionTestCase( - id="objectid_values", + "objectid_values", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": [ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]}, expected=[ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")], - msg="Should preserve ObjectId values", + msg="$filter should preserve ObjectId values", ), ExpressionTestCase( - id="binary_values", + "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", + msg="$filter should preserve Binary values", ), ExpressionTestCase( - id="binary_subtype_preservation", + "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", + msg="$filter should preserve Binary subtype", ), ExpressionTestCase( - id="regex_values", + "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", + msg="$filter should preserve Regex values", ), ExpressionTestCase( - id="timestamp_values", + "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", + msg="$filter should preserve Timestamp values", ), ExpressionTestCase( - id="minkey_maxkey", + "minkey_maxkey", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": [MinKey(), MaxKey()]}, expected=[MinKey(), MaxKey()], - msg="Should preserve MinKey/MaxKey values", + msg="$filter should preserve MinKey/MaxKey values", ), ExpressionTestCase( - id="uuid_values", + "uuid_values", expression={"$filter": {"input": "$arr", "cond": True}}, doc={ "arr": [ @@ -120,21 +120,21 @@ Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), Binary.from_uuid(UUID("fedcba98-7654-3210-0123-456789abcdef")), ], - msg="Should preserve UUID binary values", + msg="$filter should preserve UUID binary values", ), ] # Mixed BSON types MIXED_BSON_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="mixed_bson_types", + "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", + msg="$filter should preserve mixed BSON types", ), ExpressionTestCase( - id="mixed_dates_and_ids", + "mixed_dates_and_ids", expression={"$filter": {"input": "$arr", "cond": True}}, doc={ "arr": [ @@ -148,78 +148,78 @@ ObjectId("000000000000000000000001"), Timestamp(1, 0), ], - msg="Should preserve dates, ObjectIds, timestamps", + msg="$filter should preserve dates, ObjectIds, timestamps", ), ] # Special numeric values as elements SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="infinity_values", + "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", + msg="$filter should preserve infinity values", ), ExpressionTestCase( - id="decimal128_infinity", + "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", + msg="$filter should preserve Decimal128 infinity values", ), ExpressionTestCase( - id="boundary_values", + "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", + msg="$filter should preserve numeric boundary values", ), ExpressionTestCase( - id="negative_zero", + "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", + msg="$filter should preserve negative zero values", ), ] # Decimal128 precision preservation DECIMAL128_PRECISION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="decimal128_trailing_zeros", + "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", + msg="$filter decimal128 trailing zeros preserved", ), ExpressionTestCase( - id="decimal128_nan", + "decimal128_nan", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": [DECIMAL128_NAN, Decimal128("1")]}, expected=[DECIMAL128_NAN, Decimal128("1")], - msg="Decimal128 NaN preserved", + msg="$filter decimal128 NaN preserved", ), ] # BSON type filtering with $eq condition BSON_FILTER_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="filter_int64", + "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", + msg="$filter should filter and preserve Int64", ), ExpressionTestCase( - id="filter_decimal128", + "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", + msg="$filter should filter and preserve Decimal128", ), ExpressionTestCase( - id="filter_datetime", + "filter_datetime", expression={ "$filter": { "input": "$arr", @@ -233,10 +233,10 @@ ] }, expected=[datetime(2024, 6, 1, tzinfo=timezone.utc)], - msg="Should filter and preserve datetime", + msg="$filter should filter and preserve datetime", ), ExpressionTestCase( - id="filter_objectid", + "filter_objectid", expression={ "$filter": { "input": "$arr", @@ -245,58 +245,58 @@ }, doc={"arr": [ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]}, expected=[ObjectId("000000000000000000000001")], - msg="Should filter and preserve ObjectId", + msg="$filter should filter and preserve ObjectId", ), ExpressionTestCase( - id="filter_binary_subtype", + "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", + msg="$filter should filter and preserve Binary subtype", ), ExpressionTestCase( - id="filter_timestamp", + "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", + msg="$filter should filter and preserve Timestamp", ), ExpressionTestCase( - id="filter_regex", + "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", + msg="$filter should filter and preserve Regex", ), ExpressionTestCase( - id="filter_minkey", + "filter_minkey", expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", MinKey()]}}}, doc={"arr": [MinKey(), MaxKey()]}, expected=[MinKey()], - msg="Should filter and preserve MinKey", + msg="$filter should filter and preserve MinKey", ), ExpressionTestCase( - id="filter_decimal128_nan_not_gte", + "filter_decimal128_nan_not_gte", expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 1]}}}, doc={"arr": [Decimal128("NaN")]}, expected=[], - msg="Decimal128 NaN not >= 1", + msg="$filter decimal128 NaN not >= 1", ), ExpressionTestCase( - id="filter_decimal128_neg_inf_not_gte", + "filter_decimal128_neg_inf_not_gte", expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 1]}}}, doc={"arr": [Decimal128("-Infinity")]}, expected=[], - msg="Decimal128 -Infinity not >= 1", + msg="$filter decimal128 -Infinity not >= 1", ), ExpressionTestCase( - id="filter_decimal128_inf_gte", + "filter_decimal128_inf_gte", expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 1]}}}, doc={"arr": [Decimal128("Infinity")]}, expected=[Decimal128("Infinity")], - msg="Decimal128 Infinity >= 1", + msg="$filter decimal128 Infinity >= 1", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_core_behavior.py index 0c12e1b99..2d15db2b7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_core_behavior.py @@ -22,326 +22,326 @@ # Success: basic filtering BASIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="gt_filter", + "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", + msg="$filter should keep elements greater than 3", ), ExpressionTestCase( - id="gte_filter", + "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", + msg="$filter should keep elements >= 3", ), ExpressionTestCase( - id="lt_filter", + "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", + msg="$filter should keep elements less than 3", ), ExpressionTestCase( - id="eq_filter", + "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", + msg="$filter should keep elements equal to 2", ), ExpressionTestCase( - id="ne_filter", + "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", + msg="$filter should keep elements not equal to 2", ), ExpressionTestCase( - id="none_match", + "none_match", expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 100]}}}, doc={"arr": [1, 2, 3]}, expected=[], - msg="Should return empty when none match", + msg="$filter should return empty when none match", ), ExpressionTestCase( - id="string_filter", + "string_filter", expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", "abcd"]}}}, doc={"arr": ["abcd", "efgh", "abcd", "zyz"]}, expected=["abcd", "abcd"], - msg="Should filter strings abcd", + msg="$filter should filter strings abcd", ), ExpressionTestCase( - id="bool_cond_true", + "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", + msg="$filter literal true cond should keep all elements", ), ExpressionTestCase( - id="bool_cond_false", + "bool_cond_false", expression={"$filter": {"input": "$arr", "cond": False}}, doc={"arr": [1, 2, 3]}, expected=[], - msg="Literal false cond should keep no elements", + msg="$filter literal false cond should keep no elements", ), ExpressionTestCase( - id="empty_array", + "empty_array", expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 0]}}}, doc={"arr": []}, expected=[], - msg="Should return empty array for empty input", + msg="$filter should return empty array for empty input", ), ExpressionTestCase( - id="null_input", + "null_input", expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 0]}}}, doc={"arr": None}, expected=None, - msg="Should return null when input is null", + msg="$filter should return null when input is null", ), ExpressionTestCase( - id="custom_as_var", + "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", + msg="$filter should use custom 'as' variable name", ), ExpressionTestCase( - id="single_element_match", + "single_element_match", expression={"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 0]}}}, doc={"arr": [42]}, expected=[42], - msg="Should keep single matching element", + msg="$filter should keep single matching element", ), ExpressionTestCase( - id="large_array_1000", + "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", + msg="$filter should filter large array", ), ] # Success: nested arrays (filter does not recurse) NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_arrays_by_size", + "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", + msg="$filter should filter subarrays by size", ), ExpressionTestCase( - id="nested_arrays_identity", + "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", + msg="$filter should preserve nested arrays when all match", ), ] # Success: elements with null NULL_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="filter_out_nulls", + "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", + msg="$filter should filter out null elements", ), ExpressionTestCase( - id="keep_only_nulls", + "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", + msg="$filter should keep only null elements", ), ] # Success: objects as elements OBJECT_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="filter_objects_by_nested_field", + "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", + msg="$filter should filter objects by nested field", ), ExpressionTestCase( - id="duplicate_objects", + "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", + msg="$filter should return all matching duplicate objects", ), ExpressionTestCase( - id="single_object_no_match", + "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", + msg="$filter should return empty array when single object does not match", ), ] # Success: limit parameter LIMIT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="limit_1", + "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", + msg="$filter should return at most 1 matching element", ), ExpressionTestCase( - id="limit_2", + "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", + msg="$filter should return at most 2 matching elements", ), ExpressionTestCase( - id="limit_exceeds_matches", + "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", + msg="$filter limit > matches should return all matches", ), ExpressionTestCase( - id="limit_on_empty", + "limit_on_empty", expression={"$filter": {"input": "$arr", "cond": True, "limit": 5}}, doc={"arr": []}, expected=[], - msg="Limit on empty array returns empty", + msg="$filter limit on empty array returns empty", ), ExpressionTestCase( - id="limit_with_none_matching", + "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", + msg="$filter limit with no matches returns empty", ), ExpressionTestCase( - id="limit_long", + "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", + msg="$filter long limit should work", ), ExpressionTestCase( - id="limit_double_whole", + "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", + msg="$filter whole-number double limit should work", ), ExpressionTestCase( - id="limit_decimal128", + "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", + msg="$filter decimal128 limit should work", ), ExpressionTestCase( - id="limit_with_duplicates", + "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", + msg="$filter limit 2 with duplicates returns first 2 matches", ), ExpressionTestCase( - id="limit_int32_max", + "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", + msg="$filter iNT32_MAX limit should return all matches", ), ExpressionTestCase( - id="limit_null", + "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", + msg="$filter null limit should return all matches", ), ExpressionTestCase( - id="limit_missing_field_ref", + "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", + msg="$filter missing field reference for limit should return all matches", ), ] # Success: type-based filtering TYPE_FILTER_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="filter_by_type", + "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", + msg="$filter should filter by BSON type", ), ExpressionTestCase( - id="filter_strings_only", + "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", + msg="$filter should keep only string elements", ), ] # Success: cond true or false COND_FALSY_TRUTHY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="cond_nonzero_truthy", + "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", + msg="$filter non-zero values are truthy", ), ExpressionTestCase( - id="cond_null_falsy", + "cond_null_falsy", expression={"$filter": {"input": "$arr", "cond": "$$this"}}, doc={"arr": [1, None, 2, None]}, expected=[1, 2], - msg="Null is falsy", + msg="$filter null is falsy", ), ExpressionTestCase( - id="cond_zero_falsy", + "cond_zero_falsy", expression={"$filter": {"input": "$arr", "cond": 0}}, doc={"arr": [1, 2]}, expected=[], - msg="0 is falsy", + msg="$filter 0 is falsy", ), ExpressionTestCase( - id="cond_empty_string_truthy", + "cond_empty_string_truthy", expression={"$filter": {"input": "$arr", "cond": ""}}, doc={"arr": [1, 2]}, expected=[1, 2], - msg="Empty string is truthy in MongoDB", + msg="$filter empty string is truthy in MongoDB", ), ExpressionTestCase( - id="cond_object_truthy", + "cond_object_truthy", expression={"$filter": {"input": "$arr", "cond": {"x": 10}}}, doc={"arr": [1, 2]}, expected=[1, 2], - msg="Object is truthy", + msg="$filter object is truthy", ), ExpressionTestCase( - id="cond_empty_object_truthy", + "cond_empty_object_truthy", expression={"$filter": {"input": "$arr", "cond": {}}}, doc={"arr": [1, 2]}, expected=[1, 2], - msg="Empty object is truthy", + msg="$filter empty object is truthy", ), ExpressionTestCase( - id="cond_empty_array_truthy", + "cond_empty_array_truthy", expression={"$filter": {"input": "$arr", "cond": []}}, doc={"arr": [1, 2]}, expected=[1, 2], - msg="Empty array is truthy", + msg="$filter empty array is truthy", ), ] @@ -349,28 +349,28 @@ # Success: type strict equality TYPE_STRICT_EQUALITY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="false_vs_zero", + "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", + "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", + "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", + "null_vs_zero_false_empty_string", expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", None]}}}, doc={"arr": [None, 0, False, ""]}, expected=[None], @@ -381,18 +381,18 @@ # Success: numeric equivalence NUMERIC_EQUIVALENCE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="numeric_equivalence_one", + "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", + msg="$filter all numeric representations of 1 should match", ), ExpressionTestCase( - id="numeric_equivalence_zero", + "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", + msg="$filter all numeric representations of 0 should match", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py index eeb90e8f4..178b67d4f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py @@ -45,381 +45,381 @@ # Error: non-array input — standard BSON types NOT_ARRAY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string_input", + "string_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": "hello"}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject string input", + msg="$filter should reject string input", ), ExpressionTestCase( - id="int_input", + "int_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": 42}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject int input", + msg="$filter should reject int input", ), ExpressionTestCase( - id="negative_int_input", + "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", + msg="$filter should reject negative int input", ), ExpressionTestCase( - id="bool_input", + "bool_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": True}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject bool input", + msg="$filter should reject bool input", ), ExpressionTestCase( - id="bool_false_input", + "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", + msg="$filter should reject bool false input", ), ExpressionTestCase( - id="object_input", + "object_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": {"a": 1}}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject object input", + msg="$filter should reject object input", ), ExpressionTestCase( - id="double_input", + "double_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": 3.14}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject double input", + msg="$filter should reject double input", ), ExpressionTestCase( - id="negative_double_input", + "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", + msg="$filter should reject negative double input", ), ExpressionTestCase( - id="decimal128_input", + "decimal128_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": Decimal128("1")}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject decimal128 input", + msg="$filter should reject decimal128 input", ), ExpressionTestCase( - id="int64_input", + "int64_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": Int64(1)}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject int64 input", + msg="$filter should reject int64 input", ), ExpressionTestCase( - id="objectid_input", + "objectid_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": ObjectId()}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject objectid input", + msg="$filter should reject objectid input", ), ExpressionTestCase( - id="datetime_input", + "datetime_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": datetime(2024, 1, 1, tzinfo=timezone.utc)}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject datetime input", + msg="$filter should reject datetime input", ), ExpressionTestCase( - id="binary_input", + "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", + msg="$filter should reject binary input", ), ExpressionTestCase( - id="regex_input", + "regex_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": Regex("x")}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject regex input", + msg="$filter should reject regex input", ), ExpressionTestCase( - id="maxkey_input", + "maxkey_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": MaxKey()}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject maxkey input", + msg="$filter should reject maxkey input", ), ExpressionTestCase( - id="minkey_input", + "minkey_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": MinKey()}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject minkey input", + msg="$filter should reject minkey input", ), ExpressionTestCase( - id="timestamp_input", + "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", + msg="$filter should reject timestamp input", ), ] # Error: special float/Decimal128 values SPECIAL_NUMERIC_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nan_input", + "nan_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": FLOAT_NAN}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject NaN input", + msg="$filter should reject NaN input", ), ExpressionTestCase( - id="inf_input", + "inf_input", expression={"$filter": {"input": "$arr", "cond": True}}, doc={"arr": FLOAT_INFINITY}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, - msg="Should reject Infinity input", + msg="$filter should reject Infinity input", ), ExpressionTestCase( - id="neg_inf_input", + "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", + msg="$filter should reject -Infinity input", ), ExpressionTestCase( - id="neg_zero_input", + "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", + msg="$filter should reject negative zero input", ), ExpressionTestCase( - id="decimal128_nan_input", + "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", + msg="$filter should reject Decimal128 NaN input", ), ExpressionTestCase( - id="decimal128_neg_nan_input", + "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", + msg="$filter should reject Decimal128 -NaN input", ), ExpressionTestCase( - id="decimal128_inf_input", + "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", + msg="$filter should reject Decimal128 Infinity input", ), ExpressionTestCase( - id="decimal128_neg_inf_input", + "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", + msg="$filter should reject Decimal128 -Infinity input", ), ExpressionTestCase( - id="decimal128_neg_zero_input", + "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", + msg="$filter should reject Decimal128 -0 input", ), ] # Error: numeric boundary values BOUNDARY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int32_max_input", + "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", + msg="$filter should reject INT32_MAX input", ), ExpressionTestCase( - id="int32_min_input", + "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", + msg="$filter should reject INT32_MIN input", ), ExpressionTestCase( - id="int64_max_input", + "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", + msg="$filter should reject INT64_MAX input", ), ExpressionTestCase( - id="int64_min_input", + "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", + msg="$filter should reject INT64_MIN input", ), ExpressionTestCase( - id="decimal128_max_input", + "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", + msg="$filter should reject DECIMAL128_MAX input", ), ExpressionTestCase( - id="decimal128_min_input", + "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", + msg="$filter should reject DECIMAL128_MIN input", ), ] # Error: invalid limit types INVALID_LIMIT_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string_limit", + "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", + msg="$filter string limit should error", ), ExpressionTestCase( - id="bool_limit", + "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", + msg="$filter bool limit should error", ), ExpressionTestCase( - id="object_limit", + "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", + msg="$filter object limit should error", ), ExpressionTestCase( - id="array_limit", + "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", + msg="$filter array limit should error", ), ] # Error: invalid limit numeric values INVALID_LIMIT_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="zero_limit", + "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", + msg="$filter limit 0 should error", ), ExpressionTestCase( - id="neg_int_limit", + "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", + msg="$filter negative int should error", ), ExpressionTestCase( - id="neg_long_limit", + "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", + msg="$filter negative long should error", ), ExpressionTestCase( - id="neg_double_limit", + "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", + msg="$filter negative double should error", ), ExpressionTestCase( - id="neg_decimal128_limit", + "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", + msg="$filter negative decimal128 should error", ), ExpressionTestCase( - id="fractional_1_5_limit", + "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", + msg="$filter fractional 1.5 should error", ), ExpressionTestCase( - id="fractional_dec_0_5_limit", + "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", + msg="$filter fractional decimal128 0.5 should error", ), ExpressionTestCase( - id="nan_limit", + "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", + msg="$filter naN should error", ), ExpressionTestCase( - id="inf_limit", + "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", + msg="$filter infinity should error", ), ExpressionTestCase( - id="neg_inf_limit", + "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", + msg="$filter -Infinity should error", ), ExpressionTestCase( - id="decimal128_nan_limit", + "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", + msg="$filter decimal128 NaN should error", ), ExpressionTestCase( - id="decimal128_inf_limit", + "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", + msg="$filter decimal128 Infinity should error", ), ExpressionTestCase( - id="neg_zero_double_limit", + "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", + msg="$filter -0.0 limit should error", ), ExpressionTestCase( - id="neg_zero_decimal128_limit", + "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", + msg="$filter decimal128 -0 limit should error", ), ] # Error: cond evaluation errors COND_EVALUATION_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="cond_divide_by_zero", + "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", + msg="$filter division by zero in cond should error", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_expressions.py index a6334924d..207630821 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_expressions.py @@ -20,53 +20,53 @@ # Field path lookups FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_field_path", + "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", + msg="$filter should resolve nested field path", ), ExpressionTestCase( - id="deeply_nested_field", + "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", + msg="$filter should resolve deeply nested field path", ), ExpressionTestCase( - id="composite_array_path", + "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", + msg="$filter composite array path should resolve to array", ), ExpressionTestCase( - id="index_path_on_object_key", + "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", + msg="$filter object key '0' resolves correctly", ), ExpressionTestCase( - id="object_key_zero", + "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", + "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", + msg="$filter should access outer document field in cond", ), ] # $let and system variables LET_AND_VARIABLE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="let_variable", + "let_variable", expression={ "$let": { "vars": {"arr": "$values"}, @@ -75,17 +75,17 @@ }, doc={"values": [1, 2, 3, 4]}, expected=[3, 4], - msg="Should work with $let variables", + msg="$filter should work with $let variables", ), ExpressionTestCase( - id="root_variable", + "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", + msg="$filter should work with $$ROOT", ), ExpressionTestCase( - id="current_variable", + "current_variable", expression={"$filter": {"input": "$$CURRENT.values", "cond": {"$gt": ["$$this", 2]}}}, doc={"_id": 2, "values": [1, 2, 3, 4]}, expected=[3, 4], @@ -96,14 +96,14 @@ # Null/missing via expression NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="missing_field", + "missing_field", expression={"$filter": {"input": "$nonexistent", "cond": True}}, doc={"other": 1}, expected=None, - msg="Missing field should return null", + msg="$filter missing field should return null", ), ExpressionTestCase( - id="remove_variable", + "remove_variable", expression={"$filter": {"input": "$$REMOVE", "cond": True}}, doc={"x": 1}, expected=None, @@ -114,7 +114,7 @@ # Nested $filter NESTED_FILTER_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="filter_then_filter", + "filter_then_filter", expression={ "$filter": { "input": {"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 1]}}}, @@ -123,7 +123,7 @@ }, doc={"arr": [1, 2, 3, 4, 5, 6]}, expected=[2, 3, 4], - msg="Nested $filter should chain conditions", + msg="$filter nested $filter should chain conditions", ), ] @@ -131,22 +131,22 @@ # Limit with field reference LIMIT_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="limit_from_field", + "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", + msg="$filter limit from field reference", ), ] # Literal array input (not field path) LITERAL_INPUT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="literal_array_input", + "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", + msg="$filter should filter literal array input", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_structure_errors.py index dbf39baa9..8c680dd05 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_structure_errors.py @@ -25,72 +25,72 @@ # Error: non-object argument NON_OBJECT_ARG_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="null_arg", + "null_arg", expression={"$filter": None}, error_code=FILTER_NON_OBJECT_ARG_ERROR, - msg="Null arg should error", + msg="$filter null arg should error", ), ExpressionTestCase( - id="int_arg", + "int_arg", expression={"$filter": 1}, error_code=FILTER_NON_OBJECT_ARG_ERROR, - msg="Int arg should error", + msg="$filter int arg should error", ), ExpressionTestCase( - id="string_arg", + "string_arg", expression={"$filter": "string"}, error_code=FILTER_NON_OBJECT_ARG_ERROR, - msg="String arg should error", + msg="$filter string arg should error", ), ExpressionTestCase( - id="array_arg", + "array_arg", expression={"$filter": []}, error_code=FILTER_NON_OBJECT_ARG_ERROR, - msg="Array arg should error", + msg="$filter array arg should error", ), ExpressionTestCase( - id="bool_arg", + "bool_arg", expression={"$filter": True}, error_code=FILTER_NON_OBJECT_ARG_ERROR, - msg="Bool arg should error", + msg="$filter bool arg should error", ), ] # Error: unknown fields UNKNOWN_FIELD_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="extra_unknown", + "extra_unknown", expression={"$filter": {"input": [1], "cond": True, "unknown": 1}}, error_code=FILTER_UNKNOWN_FIELD_ERROR, - msg="Extra unknown field should error", + msg="$filter extra unknown field should error", ), ExpressionTestCase( - id="only_unknown", + "only_unknown", expression={"$filter": {"dummy": 124}}, error_code=FILTER_UNKNOWN_FIELD_ERROR, - msg="Only unknown field should error", + msg="$filter only unknown field should error", ), ] # Error: missing required fields MISSING_REQUIRED_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="missing_input", + "missing_input", expression={"$filter": {"as": "x", "cond": True}}, error_code=FILTER_MISSING_INPUT_ERROR, - msg="Missing input should error", + msg="$filter missing input should error", ), ExpressionTestCase( - id="missing_cond", + "missing_cond", expression={"$filter": {"input": [1, 2, 3]}}, error_code=FILTER_MISSING_COND_ERROR, - msg="Missing cond should error", + msg="$filter missing cond should error", ), ExpressionTestCase( - id="empty_object", + "empty_object", expression={"$filter": {}}, error_code=FILTER_MISSING_INPUT_ERROR, - msg="Empty object should error", + msg="$filter empty object should error", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_filter.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_filter.py index 49048c03b..828336022 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_filter.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_smoke_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, ignore_doc_order=True, msg="Should support $filter expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py index 1a6cdb9a3..61b0af003 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py @@ -32,238 +32,238 @@ # Success: search for various BSON types BSON_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="bson_int64", + "bson_int64", doc={"val": Int64(99), "arr": [Int64(99), 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find Int64 in array", + msg="$in should find Int64 in array", ), ExpressionTestCase( - id="bson_decimal128", + "bson_decimal128", doc={"val": Decimal128("1.5"), "arr": [Decimal128("1.5"), 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find Decimal128 in array", + msg="$in should find Decimal128 in array", ), ExpressionTestCase( - id="bson_datetime", + "bson_datetime", doc={ "val": datetime(2024, 1, 1, tzinfo=timezone.utc), "arr": [datetime(2024, 1, 1, tzinfo=timezone.utc), 1], }, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find datetime in array", + msg="$in should find datetime in array", ), ExpressionTestCase( - id="bson_objectid", + "bson_objectid", doc={ "val": ObjectId("000000000000000000000001"), "arr": [ObjectId("000000000000000000000001"), 1], }, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find ObjectId in array", + msg="$in should find ObjectId in array", ), ExpressionTestCase( - id="bson_binary", + "bson_binary", doc={"val": Binary(b"\x01\x02", 0), "arr": [Binary(b"\x01\x02", 0), 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find Binary in array", + msg="$in should find Binary in array", ), ExpressionTestCase( - id="bson_regex", + "bson_regex", doc={"val": Regex("^abc", "i"), "arr": [Regex("^abc", "i"), 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find Regex in array", + msg="$in should find Regex in array", ), ExpressionTestCase( - id="bson_timestamp", + "bson_timestamp", doc={"val": Timestamp(1, 1), "arr": [Timestamp(1, 1), 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find Timestamp in array", + msg="$in should find Timestamp in array", ), ExpressionTestCase( - id="bson_minkey", + "bson_minkey", doc={"val": MinKey(), "arr": [MinKey(), 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find MinKey in array", + msg="$in should find MinKey in array", ), ExpressionTestCase( - id="bson_maxkey", + "bson_maxkey", doc={"val": MaxKey(), "arr": [1, MaxKey()]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find MaxKey in array", + msg="$in should find MaxKey in array", ), ExpressionTestCase( - id="bson_uuid", + "bson_uuid", doc={ "val": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), "arr": [Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), 1], }, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find UUID binary in array", + msg="$in should find UUID binary in array", ), # Special float values ExpressionTestCase( - id="float_infinity_in_array", + "float_infinity_in_array", doc={"val": FLOAT_INFINITY, "arr": [FLOAT_INFINITY, 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find Infinity in array", + msg="$in should find Infinity in array", ), ExpressionTestCase( - id="float_neg_infinity_in_array", + "float_neg_infinity_in_array", doc={"val": FLOAT_NEGATIVE_INFINITY, "arr": [FLOAT_NEGATIVE_INFINITY, 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find -Infinity in array", + msg="$in should find -Infinity in array", ), ExpressionTestCase( - id="float_infinity_not_in_array", + "float_infinity_not_in_array", doc={"val": FLOAT_INFINITY, "arr": [1, 2, 3]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find Infinity in non-Infinity array", + msg="$in should not find Infinity in non-Infinity array", ), # Special Decimal128 values ExpressionTestCase( - id="decimal128_infinity_in_array", + "decimal128_infinity_in_array", doc={"val": DECIMAL128_INFINITY, "arr": [DECIMAL128_INFINITY, 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find Decimal128 Infinity in array", + msg="$in should find Decimal128 Infinity in array", ), ExpressionTestCase( - id="decimal128_neg_infinity_in_array", + "decimal128_neg_infinity_in_array", doc={"val": DECIMAL128_NEGATIVE_INFINITY, "arr": [DECIMAL128_NEGATIVE_INFINITY, 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find Decimal128 -Infinity in array", + msg="$in should find Decimal128 -Infinity in array", ), # NaN equality: NaN == NaN in BSON comparison (unlike IEEE 754) ExpressionTestCase( - id="float_nan_found", + "float_nan_found", doc={"val": FLOAT_NAN, "arr": [FLOAT_NAN, 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find NaN in array (BSON equality)", + msg="$in should find NaN in array (BSON equality)", ), ExpressionTestCase( - id="decimal128_nan_found", + "decimal128_nan_found", doc={"val": DECIMAL128_NAN, "arr": [DECIMAL128_NAN, 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find Decimal128 NaN in array (BSON equality)", + msg="$in should find Decimal128 NaN in array (BSON equality)", ), ExpressionTestCase( - id="float_nan_matches_decimal128_nan", + "float_nan_matches_decimal128_nan", doc={"val": FLOAT_NAN, "arr": [DECIMAL128_NAN, 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="float NaN should match Decimal128 NaN cross-type", + msg="$in float NaN should match Decimal128 NaN cross-type", ), ExpressionTestCase( - id="decimal128_nan_matches_float_nan", + "decimal128_nan_matches_float_nan", doc={"val": DECIMAL128_NAN, "arr": [FLOAT_NAN, 1]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Decimal128 NaN should match float NaN cross-type", + msg="$in decimal128 NaN should match float NaN cross-type", ), # Aggregation $in does NOT pattern-match regex against strings (unlike query $in) ExpressionTestCase( - id="regex_no_pattern_match", + "regex_no_pattern_match", doc={"val": Regex("^a"), "arr": ["abc", "def"]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Regex should not pattern-match strings in aggregation $in", + msg="$in regex should not pattern-match strings in aggregation $in", ), ] # Success: numeric type equivalence NUMERIC_EQUIVALENCE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int_in_doubles", + "int_in_doubles", doc={"val": 1, "arr": [1.0, 2.0]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find int in doubles via numeric equivalence", + msg="$in should find int in doubles via numeric equivalence", ), ExpressionTestCase( - id="int_in_longs", + "int_in_longs", doc={"val": 1, "arr": [Int64(1), 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find int in longs via numeric equivalence", + msg="$in should find int in longs via numeric equivalence", ), ExpressionTestCase( - id="int_in_decimal128s", + "int_in_decimal128s", doc={"val": 1, "arr": [Decimal128("1"), 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find int in decimal128s via numeric equivalence", + msg="$in should find int in decimal128s via numeric equivalence", ), ExpressionTestCase( - id="double_in_ints", + "double_in_ints", doc={"val": 1.0, "arr": [1, 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find double in ints via numeric equivalence", + msg="$in should find double in ints via numeric equivalence", ), ExpressionTestCase( - id="long_in_ints", + "long_in_ints", doc={"val": Int64(1), "arr": [1, 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find long in ints via numeric equivalence", + msg="$in should find long in ints via numeric equivalence", ), ExpressionTestCase( - id="decimal128_in_ints", + "decimal128_in_ints", doc={"val": Decimal128("1"), "arr": [1, 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find decimal128 in ints via numeric equivalence", + msg="$in should find decimal128 in ints via numeric equivalence", ), ExpressionTestCase( - id="decimal128_in_doubles", + "decimal128_in_doubles", doc={"val": Decimal128("1.5"), "arr": [1.5, 2.5]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find decimal128 in doubles via numeric equivalence", + msg="$in should find decimal128 in doubles via numeric equivalence", ), ] # Negative zero equivalence with positive zero NEGATIVE_ZERO_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="double_neg_zero_matches_zero", + "double_neg_zero_matches_zero", doc={"val": DOUBLE_NEGATIVE_ZERO, "arr": [0, 1, 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, msg="$in should treat -0.0 as equivalent to 0", ), ExpressionTestCase( - id="zero_matches_double_neg_zero_in_array", + "zero_matches_double_neg_zero_in_array", doc={"val": 0, "arr": [DOUBLE_NEGATIVE_ZERO, 1, 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, msg="$in should find 0 in array containing -0.0", ), ExpressionTestCase( - id="decimal128_neg_zero_matches_zero", + "decimal128_neg_zero_matches_zero", doc={"val": DECIMAL128_NEGATIVE_ZERO, "arr": [0, 1, 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, msg="$in should treat Decimal128 -0 as equivalent to 0", ), ExpressionTestCase( - id="zero_matches_decimal128_neg_zero_in_array", + "zero_matches_decimal128_neg_zero_in_array", doc={"val": 0, "arr": [DECIMAL128_NEGATIVE_ZERO, 1, 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py index 007927bb6..f49c12f60 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_core_behavior.py @@ -19,173 +19,173 @@ # Success: value found in array → True FOUND_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="found_int", + "found_int", doc={"val": 2, "arr": [1, 2, 3]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find int in array", + msg="$in should find int in array", ), ExpressionTestCase( - id="found_first", + "found_first", doc={"val": 1, "arr": [1, 2, 3]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find first element", + msg="$in should find first element", ), ExpressionTestCase( - id="found_last", + "found_last", doc={"val": 3, "arr": [1, 2, 3]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find last element", + msg="$in should find last element", ), ExpressionTestCase( - id="found_string", + "found_string", doc={"val": "b", "arr": ["a", "b", "c"]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find string in array", + msg="$in should find string in array", ), ExpressionTestCase( - id="found_bool_true", + "found_bool_true", doc={"val": True, "arr": [True, False]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find true in array", + msg="$in should find true in array", ), ExpressionTestCase( - id="found_bool_false", + "found_bool_false", doc={"val": False, "arr": [True, False]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find false in array", + msg="$in should find false in array", ), ExpressionTestCase( - id="found_null", + "found_null", doc={"val": None, "arr": [None, 1, 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find null in array", + msg="$in should find null in array", ), ExpressionTestCase( - id="found_nested_array", + "found_nested_array", doc={"val": [3, 4], "arr": [[1, 2], [3, 4]]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find nested array", + msg="$in should find nested array", ), ExpressionTestCase( - id="found_object", + "found_object", doc={"val": {"a": 1}, "arr": [{"a": 1}, {"b": 2}]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find object in array", + msg="$in should find object in array", ), ExpressionTestCase( - id="found_single_element", + "found_single_element", doc={"val": 42, "arr": [42]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find value in single-element array", + msg="$in should find value in single-element array", ), ExpressionTestCase( - id="found_duplicate", + "found_duplicate", doc={"val": 5, "arr": [5, 5, 5]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find value in array of duplicates", + msg="$in should find value in array of duplicates", ), ] # Success: value not found → False NOT_FOUND_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="not_found_int", + "not_found_int", doc={"val": 4, "arr": [1, 2, 3]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find absent int", + msg="$in should not find absent int", ), ExpressionTestCase( - id="not_found_string", + "not_found_string", doc={"val": "z", "arr": ["a", "b"]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find absent string", + msg="$in should not find absent string", ), ExpressionTestCase( - id="not_found_empty_array", + "not_found_empty_array", doc={"val": 1, "arr": []}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find value in empty array", + msg="$in should not find value in empty array", ), ExpressionTestCase( - id="not_found_type_mismatch", + "not_found_type_mismatch", doc={"val": "1", "arr": [1, 2, 3]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find string '1' in int array", + msg="$in should not find string '1' in int array", ), ExpressionTestCase( - id="not_found_bool_vs_int", + "not_found_bool_vs_int", doc={"val": True, "arr": [1, 0]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find bool in int array", + msg="$in should not find bool in int array", ), ExpressionTestCase( - id="not_found_null", + "not_found_null", doc={"val": None, "arr": [1, 2, 3]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find null in non-null array", + msg="$in should not find null in non-null array", ), ExpressionTestCase( - id="not_found_partial_array", + "not_found_partial_array", doc={"val": [1], "arr": [[1, 2], [3, 4]]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find partial array match", + msg="$in should not find partial array match", ), ExpressionTestCase( - id="not_found_partial_object", + "not_found_partial_object", doc={"val": {"a": 1}, "arr": [{"a": 1, "b": 2}]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find partial object match", + msg="$in should not find partial object match", ), ] # Success: mixed types in array MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="mixed_find_string", + "mixed_find_string", doc={"val": "2", "arr": [1, "2", True, None, [1]]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find string in mixed-type array", + msg="$in should find string in mixed-type array", ), ExpressionTestCase( - id="mixed_find_null", + "mixed_find_null", doc={"val": None, "arr": [1, "2", True, None, [1]]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find null in mixed-type array", + msg="$in should find null in mixed-type array", ), ExpressionTestCase( - id="mixed_find_array", + "mixed_find_array", doc={"val": [1], "arr": [1, "2", True, None, [1]]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find array in mixed-type array", + msg="$in should find array in mixed-type array", ), ExpressionTestCase( - id="mixed_not_found", + "mixed_not_found", doc={"val": "x", "arr": [1, "2", True, None, [1]]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find absent value in mixed-type array", + msg="$in should not find absent value in mixed-type array", ), ] @@ -193,32 +193,32 @@ LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="large_array_found_first", + "large_array_found_first", doc={"val": 0, "arr": list(range(20_000))}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find first element in large array", + msg="$in should find first element in large array", ), ExpressionTestCase( - id="large_array_found_last", + "large_array_found_last", doc={"val": 19_999, "arr": list(range(20_000))}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find last element in large array", + msg="$in should find last element in large array", ), ExpressionTestCase( - id="large_array_found_middle", + "large_array_found_middle", doc={"val": 10_000, "arr": list(range(20_000))}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find middle element in large array", + msg="$in should find middle element in large array", ), ExpressionTestCase( - id="large_array_not_found", + "large_array_not_found", doc={"val": -1, "arr": list(range(20_000))}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find absent value in large array", + msg="$in should not find absent value in large array", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py index c302631d8..fe6d864d3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py @@ -22,66 +22,66 @@ # Success: nested mixed arrays as search targets NESTED_MIXED_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_find_object_in_mixed", + "nested_find_object_in_mixed", doc={"val": {"a": 1}, "arr": [1, "two", {"a": 1}, [3, 4], True]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find object in nested mixed array", + msg="$in should find object in nested mixed array", ), ExpressionTestCase( - id="nested_find_array_in_mixed", + "nested_find_array_in_mixed", doc={"val": [3, 4], "arr": [1, "two", {"a": 1}, [3, 4], True]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find array in nested mixed array", + msg="$in should find array in nested mixed array", ), ExpressionTestCase( - id="nested_find_deep_object", + "nested_find_deep_object", doc={"val": {"a": {"b": 3}}, "arr": [[1, 2], {"a": {"b": 3}}, "x"]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find deep object in array", + msg="$in should find deep object in array", ), ExpressionTestCase( - id="nested_find_array_with_mixed_types", + "nested_find_array_with_mixed_types", doc={"val": [None, "a", 2], "arr": [1, [None, "a", 2], "b"]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find mixed-type subarray", + msg="$in should find mixed-type subarray", ), ExpressionTestCase( - id="nested_find_empty_object", + "nested_find_empty_object", doc={"val": {}, "arr": [1, {}, [2], "a"]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find empty object in array", + msg="$in should find empty object in array", ), ExpressionTestCase( - id="nested_find_empty_array", + "nested_find_empty_array", doc={"val": [], "arr": [1, {}, [], "a"]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find empty array in array", + msg="$in should find empty array in array", ), ExpressionTestCase( - id="nested_find_subarray_binary_decimal128", + "nested_find_subarray_binary_decimal128", doc={ "val": [Binary(b"\x01\x02", 0), Decimal128("3.14")], "arr": [1, [Binary(b"\x01\x02", 0), Decimal128("3.14")], "x", [3]], }, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find subarray with binary and decimal128", + msg="$in should find subarray with binary and decimal128", ), ExpressionTestCase( - id="nested_find_subarray_object_array", + "nested_find_subarray_object_array", doc={"val": [{"k": 1}, [2, 3]], "arr": ["a", [{"k": 1}, [2, 3]], None, 4]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find subarray with object and array", + msg="$in should find subarray with object and array", ), ExpressionTestCase( - id="nested_find_subarray_datetime_objectid", + "nested_find_subarray_datetime_objectid", doc={ "val": [ datetime(2024, 1, 1, tzinfo=timezone.utc), @@ -95,63 +95,63 @@ }, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find subarray with datetime and objectid", + msg="$in should find subarray with datetime and objectid", ), ExpressionTestCase( - id="nested_find_subarray_minkey_maxkey", + "nested_find_subarray_minkey_maxkey", doc={"val": [MinKey(), MaxKey()], "arr": [[MinKey(), MaxKey()], 1, "a"]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find subarray with minkey and maxkey", + msg="$in should find subarray with minkey and maxkey", ), ] # Success: deeply nested search targets (3-5 levels) DEEPLY_NESTED_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_3_levels", + "nested_3_levels", doc={"val": [[2, 3], [4, 5]], "arr": [1, [[2, 3], [4, 5]], "end"]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find 3-level nested array", + msg="$in should find 3-level nested array", ), ExpressionTestCase( - id="nested_4_levels", + "nested_4_levels", doc={"val": [[[1, 2], 3], 4], "arr": ["a", [[[1, 2], 3], 4], None]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find 4-level nested array", + msg="$in should find 4-level nested array", ), ExpressionTestCase( - id="nested_deep_mixed_bson", + "nested_deep_mixed_bson", doc={ "val": [[MinKey(), {"a": [Decimal128("1.5")]}], True], "arr": [0, [[MinKey(), {"a": [Decimal128("1.5")]}], True], "x"], }, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find deeply nested mixed BSON", + msg="$in should find deeply nested mixed BSON", ), ExpressionTestCase( - id="nested_inner_not_outer", + "nested_inner_not_outer", doc={"val": [2, 3], "arr": [[1, [2, 3]], [2, 3], 4]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find inner array match", + msg="$in should find inner array match", ), ExpressionTestCase( - id="nested_5_levels", + "nested_5_levels", doc={"val": [[[[99]]]], "arr": [[[[[99]]]], "other"]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find 5-level nested array", + msg="$in should find 5-level nested array", ), ExpressionTestCase( - id="nested_deep_not_found", + "nested_deep_not_found", doc={"val": [2, 3], "arr": [[1, [2, 3]], [4, 5]]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find array at wrong nesting level", + msg="$in should not find array at wrong nesting level", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py index 292fb5b26..0a53ef7c9 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py @@ -17,36 +17,36 @@ # Success: null/missing handling (runs both literal and insert) NULL_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="null_value_in_array", + "null_value_in_array", doc={"val": None, "arr": [1, None, 3]}, expression={"$in": ["$val", "$arr"]}, expected=True, - msg="Should find null value in array containing null", + msg="$in should find null value in array containing null", ), ExpressionTestCase( - id="null_value_not_in_array", + "null_value_not_in_array", doc={"val": None, "arr": [1, 2, 3]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find null in array without null", + msg="$in should not find null in array without null", ), ] # Success: missing value handling (literal only, MISSING is a field ref) LITERAL_ONLY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="missing_value", + "missing_value", doc={"val": MISSING, "arr": [1, 2, 3]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find missing value in array", + msg="$in should not find missing value in array", ), ExpressionTestCase( - id="missing_value_null_in_array", + "missing_value_null_in_array", doc={"val": MISSING, "arr": [1, None, 3]}, expression={"$in": ["$val", "$arr"]}, expected=False, - msg="Should not find missing value even with null in array", + msg="$in should not find missing value even with null in array", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_in.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_in.py index 46e97f208..4de6a75e8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_in.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_smoke_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, ignore_doc_order=True, msg="Should support $in expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py index 5c19daf72..7b9b6849c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py @@ -34,299 +34,299 @@ # Success: search for various BSON types BSON_TYPE_SEARCH_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="search_int64", + "search_int64", array=[Int64(99), 1], search=Int64(99), expected=0, - msg="Should find Int64 value", + msg="$indexOfArray should find Int64 value", ), IndexOfArrayTest( - id="search_decimal128", + "search_decimal128", array=[Decimal128("1.5"), 2], search=Decimal128("1.5"), expected=0, - msg="Should find Decimal128 value", + msg="$indexOfArray should find Decimal128 value", ), IndexOfArrayTest( - id="search_datetime", + "search_datetime", array=[datetime(2024, 1, 1, tzinfo=timezone.utc), 1], search=datetime(2024, 1, 1, tzinfo=timezone.utc), expected=0, - msg="Should find datetime value", + msg="$indexOfArray should find datetime value", ), IndexOfArrayTest( - id="search_objectid", + "search_objectid", array=[ObjectId("000000000000000000000001"), 1], search=ObjectId("000000000000000000000001"), expected=0, - msg="Should find ObjectId value", + msg="$indexOfArray should find ObjectId value", ), IndexOfArrayTest( - id="search_binary", + "search_binary", array=[Binary(b"\x01\x02", 0), 1], search=Binary(b"\x01\x02", 0), expected=0, - msg="Should find Binary value", + msg="$indexOfArray should find Binary value", ), IndexOfArrayTest( - id="search_regex", + "search_regex", array=[Regex("^abc", "i"), 1], search=Regex("^abc", "i"), expected=0, - msg="Should find Regex value", + msg="$indexOfArray should find Regex value", ), IndexOfArrayTest( - id="search_timestamp", + "search_timestamp", array=[Timestamp(1, 1), 1], search=Timestamp(1, 1), expected=0, - msg="Should find Timestamp value", + msg="$indexOfArray should find Timestamp value", ), IndexOfArrayTest( - id="search_minkey", + "search_minkey", array=[MinKey(), 1], search=MinKey(), expected=0, - msg="Should find MinKey value", + msg="$indexOfArray should find MinKey value", ), IndexOfArrayTest( - id="search_maxkey", + "search_maxkey", array=[1, MaxKey()], search=MaxKey(), expected=1, - msg="Should find MaxKey value", + msg="$indexOfArray should find MaxKey value", ), IndexOfArrayTest( - id="search_uuid", + "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", + msg="$indexOfArray should find UUID binary value", ), # Special float values IndexOfArrayTest( - id="search_infinity", + "search_infinity", array=[FLOAT_INFINITY, 1], search=FLOAT_INFINITY, expected=0, - msg="Should find Infinity", + msg="$indexOfArray should find Infinity", ), IndexOfArrayTest( - id="search_neg_infinity", + "search_neg_infinity", array=[FLOAT_NEGATIVE_INFINITY, 1], search=FLOAT_NEGATIVE_INFINITY, expected=0, - msg="Should find -Infinity", + msg="$indexOfArray should find -Infinity", ), IndexOfArrayTest( - id="search_infinity_not_found", + "search_infinity_not_found", array=[1, 2, 3], search=FLOAT_INFINITY, expected=-1, - msg="Should not find Infinity in regular array", + msg="$indexOfArray should not find Infinity in regular array", ), # Special Decimal128 values IndexOfArrayTest( - id="search_decimal128_infinity", + "search_decimal128_infinity", array=[DECIMAL128_INFINITY, 1], search=DECIMAL128_INFINITY, expected=0, - msg="Should find Decimal128 Infinity", + msg="$indexOfArray should find Decimal128 Infinity", ), IndexOfArrayTest( - id="search_decimal128_neg_infinity", + "search_decimal128_neg_infinity", array=[DECIMAL128_NEGATIVE_INFINITY, 1], search=DECIMAL128_NEGATIVE_INFINITY, expected=0, - msg="Should find Decimal128 -Infinity", + msg="$indexOfArray should find Decimal128 -Infinity", ), # NaN equality: NaN == NaN in BSON comparison (unlike IEEE 754) IndexOfArrayTest( - id="search_nan_found", + "search_nan_found", array=[FLOAT_NAN, 1], search=FLOAT_NAN, expected=0, - msg="Should find NaN (BSON equality)", + msg="$indexOfArray should find NaN (BSON equality)", ), IndexOfArrayTest( - id="search_decimal128_nan_found", + "search_decimal128_nan_found", array=[DECIMAL128_NAN, 1], search=DECIMAL128_NAN, expected=0, - msg="Should find Decimal128 NaN (BSON equality)", + msg="$indexOfArray should find Decimal128 NaN (BSON equality)", ), # Cross-type NaN matching IndexOfArrayTest( - id="search_decimal128_nan_matches_float_nan", + "search_decimal128_nan_matches_float_nan", array=[FLOAT_NAN, 1], search=DECIMAL128_NAN, expected=0, - msg="Decimal128 NaN should match float NaN cross-type", + msg="$indexOfArray decimal128 NaN should match float NaN cross-type", ), IndexOfArrayTest( - id="search_decimal128_neg_nan_matches_nan", + "search_decimal128_neg_nan_matches_nan", array=[DECIMAL128_NAN, 1], search=Decimal128("-NaN"), expected=0, - msg="Decimal128 -NaN should match Decimal128 NaN", + msg="$indexOfArray decimal128 -NaN should match Decimal128 NaN", ), ] # Success: numeric type equivalence in search NUMERIC_EQUIVALENCE_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="int_matches_double", + "int_matches_double", array=[1.0, 2.0], search=1, expected=0, - msg="Should match int to double", + msg="$indexOfArray should match int to double", ), IndexOfArrayTest( - id="int_matches_long", + "int_matches_long", array=[Int64(1), 2], search=1, expected=0, - msg="Should match int to long", + msg="$indexOfArray should match int to long", ), IndexOfArrayTest( - id="int_matches_decimal128", + "int_matches_decimal128", array=[Decimal128("1"), 2], search=1, expected=0, - msg="Should match int to decimal128", + msg="$indexOfArray should match int to decimal128", ), IndexOfArrayTest( - id="double_matches_int", + "double_matches_int", array=[1, 2], search=1.0, expected=0, - msg="Should match double to int", + msg="$indexOfArray should match double to int", ), IndexOfArrayTest( - id="long_matches_int", + "long_matches_int", array=[1, 2], search=Int64(1), expected=0, - msg="Should match long to int", + msg="$indexOfArray should match long to int", ), IndexOfArrayTest( - id="decimal128_matches_int", + "decimal128_matches_int", array=[1, 2], search=Decimal128("1"), expected=0, - msg="Should match decimal128 to int", + msg="$indexOfArray should match decimal128 to int", ), ] # Success: nested mixed arrays NESTED_MIXED_ARRAY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="nested_find_object_in_mixed", + "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", + msg="$indexOfArray should find object in mixed array", ), IndexOfArrayTest( - id="nested_find_array_in_mixed", + "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", + msg="$indexOfArray should find array in mixed array", ), IndexOfArrayTest( - id="nested_find_bool_in_mixed", + "nested_find_bool_in_mixed", array=[1, "two", {"a": 1}, [3, 4], True], search=True, expected=4, - msg="Should find bool in mixed array", + msg="$indexOfArray should find bool in mixed array", ), IndexOfArrayTest( - id="nested_find_null_in_mixed", + "nested_find_null_in_mixed", array=[1, None, "three", [4], {"b": 2}], search=None, expected=1, - msg="Should find null in mixed array", + msg="$indexOfArray should find null in mixed array", ), IndexOfArrayTest( - id="nested_find_deep_object", + "nested_find_deep_object", array=[[1, 2], {"a": {"b": 3}}, "x"], search={"a": {"b": 3}}, expected=1, - msg="Should find deep object", + msg="$indexOfArray should find deep object", ), IndexOfArrayTest( - id="nested_find_array_with_mixed_types", + "nested_find_array_with_mixed_types", array=[1, [None, "a", 2], "b"], search=[None, "a", 2], expected=1, - msg="Should find mixed-type subarray", + msg="$indexOfArray should find mixed-type subarray", ), IndexOfArrayTest( - id="nested_find_empty_object_in_mixed", + "nested_find_empty_object_in_mixed", array=[1, {}, [2], "a"], search={}, expected=1, - msg="Should find empty object", + msg="$indexOfArray should find empty object", ), IndexOfArrayTest( - id="nested_find_empty_array_in_mixed", + "nested_find_empty_array_in_mixed", array=[1, {}, [], "a"], search=[], expected=2, - msg="Should find empty array", + msg="$indexOfArray should find empty array", ), IndexOfArrayTest( - id="nested_find_datetime_in_mixed", + "nested_find_datetime_in_mixed", 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", + msg="$indexOfArray should find datetime in mixed array", ), IndexOfArrayTest( - id="nested_find_objectid_in_mixed", + "nested_find_objectid_in_mixed", array=[1, ObjectId("000000000000000000000001"), "x", [2]], search=ObjectId("000000000000000000000001"), expected=1, - msg="Should find objectid in mixed array", + msg="$indexOfArray should find objectid in mixed array", ), IndexOfArrayTest( - id="nested_find_decimal128_in_mixed", + "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", + msg="$indexOfArray should find decimal128 in mixed array", ), IndexOfArrayTest( - id="nested_find_binary_in_mixed", + "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", + msg="$indexOfArray should find binary in mixed array", ), IndexOfArrayTest( - id="nested_find_subarray_binary_decimal128", + "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", + msg="$indexOfArray should find subarray with binary and decimal128", ), IndexOfArrayTest( - id="nested_find_subarray_object_array", + "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", + msg="$indexOfArray should find subarray with object and array", ), IndexOfArrayTest( - id="nested_find_subarray_null_bool_int", + "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", + msg="$indexOfArray should find subarray with null bool int", ), IndexOfArrayTest( - id="nested_find_subarray_datetime_objectid", + "nested_find_subarray_datetime_objectid", array=[ 0, [datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], @@ -334,56 +334,56 @@ ], search=[datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], expected=1, - msg="Should find subarray with datetime and objectid", + msg="$indexOfArray should find subarray with datetime and objectid", ), IndexOfArrayTest( - id="nested_find_subarray_minkey_maxkey", + "nested_find_subarray_minkey_maxkey", array=[[MinKey(), MaxKey()], 1, "a"], search=[MinKey(), MaxKey()], expected=0, - msg="Should find subarray with minkey and maxkey", + msg="$indexOfArray should find subarray with minkey and maxkey", ), IndexOfArrayTest( - id="nested_3_levels_deep", + "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", + msg="$indexOfArray should find 3-level nested array", ), IndexOfArrayTest( - id="nested_4_levels_deep", + "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", + msg="$indexOfArray should find 4-level nested array", ), IndexOfArrayTest( - id="nested_deep_mixed_bson", + "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", + msg="$indexOfArray should find deeply nested mixed BSON", ), IndexOfArrayTest( - id="nested_inner_not_outer", + "nested_inner_not_outer", array=[[1, [2, 3]], [2, 3], 4], search=[2, 3], expected=1, - msg="Should find inner array match", + msg="$indexOfArray should find inner array match", ), IndexOfArrayTest( - id="nested_5_levels_deep", + "nested_5_levels_deep", array=[[[[[99]]]], "other"], search=[[[[99]]]], expected=0, - msg="Should find 5-level nested array", + msg="$indexOfArray should find 5-level nested array", ), IndexOfArrayTest( - id="nested_deep_not_found", + "nested_deep_not_found", array=[[1, [2, 3]], [4, 5]], search=[2, 3], expected=-1, - msg="Should not find array at wrong nesting level", + msg="$indexOfArray should not find array at wrong nesting level", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py index 856eb3e1e..d4a97b551 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py @@ -24,484 +24,488 @@ # Success: basic search — value found BASIC_FOUND_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="found_first", + "found_first", array=[1, 2, 3], search=1, expected=0, - msg="Should find first element at index 0", + msg="$indexOfArray should find first element at index 0", ), IndexOfArrayTest( - id="found_middle", + "found_middle", array=[1, 2, 3], search=2, expected=1, - msg="Should find middle element at index 1", + msg="$indexOfArray should find middle element at index 1", ), IndexOfArrayTest( - id="found_last", + "found_last", array=[1, 2, 3], search=3, expected=2, - msg="Should find last element at index 2", + msg="$indexOfArray should find last element at index 2", ), IndexOfArrayTest( - id="found_string", + "found_string", array=["a", "b", "c"], search="b", expected=1, - msg="Should find string in array", + msg="$indexOfArray should find string in array", ), IndexOfArrayTest( - id="found_bool_true", + "found_bool_true", array=[True, False], search=True, expected=0, - msg="Should find true at index 0", + msg="$indexOfArray should find true at index 0", ), IndexOfArrayTest( - id="found_bool_false", + "found_bool_false", array=[True, False], search=False, expected=1, - msg="Should find false at index 1", + msg="$indexOfArray should find false at index 1", ), IndexOfArrayTest( - id="found_null", + "found_null", array=[None, 1, 2], search=None, expected=0, - msg="Should find null at index 0", + msg="$indexOfArray should find null at index 0", ), IndexOfArrayTest( - id="found_nested_array", + "found_nested_array", array=[[1, 2], [3, 4]], search=[3, 4], expected=1, - msg="Should find nested array", + msg="$indexOfArray should find nested array", ), IndexOfArrayTest( - id="found_object", + "found_object", array=[{"a": 1}, {"b": 2}], search={"a": 1}, expected=0, - msg="Should find object in array", + msg="$indexOfArray should find object in array", ), IndexOfArrayTest( - id="found_single_element", + "found_single_element", array=[42], search=42, expected=0, - msg="Should find value in single-element array", + msg="$indexOfArray should find value in single-element array", ), IndexOfArrayTest( - id="first_occurrence", + "first_occurrence", array=[1, 2, 1, 2], search=1, expected=0, - msg="Should return first occurrence index", + msg="$indexOfArray should return first occurrence index", ), IndexOfArrayTest( - id="duplicate_values", + "duplicate_values", array=[5, 5, 5], search=5, expected=0, - msg="Should return first index for duplicates", + msg="$indexOfArray should return first index for duplicates", ), ] # Success: value not found → -1 NOT_FOUND_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="not_found_int", + "not_found_int", array=[1, 2, 3], search=4, expected=-1, - msg="Should return -1 for absent int", + msg="$indexOfArray should return -1 for absent int", ), IndexOfArrayTest( - id="not_found_string", + "not_found_string", array=["a", "b"], search="z", expected=-1, - msg="Should return -1 for absent string", + msg="$indexOfArray should return -1 for absent string", ), IndexOfArrayTest( - id="empty_array", array=[], search=1, expected=-1, msg="Should return -1 for empty array" + "empty_array", + array=[], + search=1, + expected=-1, + msg="$indexOfArray should return -1 for empty array", ), IndexOfArrayTest( - id="type_mismatch_search", + "type_mismatch_search", array=[1, 2, 3], search="1", expected=-1, - msg="Should return -1 for type mismatch", + msg="$indexOfArray should return -1 for type mismatch", ), IndexOfArrayTest( - id="bool_vs_int", + "bool_vs_int", array=[1, 0], search=True, expected=-1, - msg="Should return -1 for bool vs int", + msg="$indexOfArray should return -1 for bool vs int", ), IndexOfArrayTest( - id="not_found_null", + "not_found_null", array=[1, 2, 3], search=None, expected=-1, - msg="Should return -1 for null not in array", + msg="$indexOfArray should return -1 for null not in array", ), IndexOfArrayTest( - id="not_found_partial_array", + "not_found_partial_array", array=[[1, 2], [3, 4]], search=[1], expected=-1, - msg="Should return -1 for partial array match", + msg="$indexOfArray should return -1 for partial array match", ), IndexOfArrayTest( - id="not_found_partial_object", + "not_found_partial_object", array=[{"a": 1, "b": 2}], search={"a": 1}, expected=-1, - msg="Should return -1 for partial object match", + msg="$indexOfArray should return -1 for partial object match", ), ] # Success: with start index START_INDEX_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="start_skips_first", + "start_skips_first", array=[1, 2, 1, 2], search=1, start=1, expected=2, - msg="Should skip first match with start index", + msg="$indexOfArray should skip first match with start index", ), IndexOfArrayTest( - id="start_at_match", + "start_at_match", array=[10, 20, 30], search=20, start=1, expected=1, - msg="Should find at start index", + msg="$indexOfArray should find at start index", ), IndexOfArrayTest( - id="start_past_match", + "start_past_match", array=[10, 20, 30], search=10, start=1, expected=-1, - msg="Should return -1 when start is past match", + msg="$indexOfArray should return -1 when start is past match", ), IndexOfArrayTest( - id="start_at_zero", + "start_at_zero", array=[1, 2, 3], search=1, start=0, expected=0, - msg="Should find with start at zero", + msg="$indexOfArray should find with start at zero", ), IndexOfArrayTest( - id="start_beyond_array", + "start_beyond_array", array=[1, 2, 3], search=1, start=20, expected=-1, - msg="Should return -1 when start beyond array", + msg="$indexOfArray should return -1 when start beyond array", ), IndexOfArrayTest( - id="start_int64", + "start_int64", array=[10, 20, 30], search=20, start=Int64(1), expected=1, - msg="Should accept Int64 start index", + msg="$indexOfArray should accept Int64 start index", ), IndexOfArrayTest( - id="start_double_integral", + "start_double_integral", array=[10, 20, 30], search=30, start=2.0, expected=2, - msg="Should accept integral double start index", + msg="$indexOfArray should accept integral double start index", ), IndexOfArrayTest( - id="start_decimal128_integral", + "start_decimal128_integral", array=[10, 20, 30], search=20, start=Decimal128("1"), expected=1, - msg="Should accept Decimal128 start index", + msg="$indexOfArray should accept Decimal128 start index", ), IndexOfArrayTest( - id="start_decimal128_10E_neg1", + "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", + msg="$indexOfArray should accept Decimal128 10E-1 as start index 1", ), IndexOfArrayTest( - id="end_decimal128_30E_neg1", + "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", + msg="$indexOfArray 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", + "range_found", array=["a", "b", "c", "b"], search="b", start=1, end=3, expected=1, - msg="Should find in range", + msg="$indexOfArray should find in range", ), IndexOfArrayTest( - id="range_not_found_exclusive_end", + "range_not_found_exclusive_end", array=["a", "b", "c"], search="c", start=0, end=2, expected=-1, - msg="Should not find at exclusive end", + msg="$indexOfArray should not find at exclusive end", ), IndexOfArrayTest( - id="range_end_equals_start", + "range_end_equals_start", array=[1, 2, 3], search=1, start=0, end=0, expected=-1, - msg="Should return -1 when end equals start", + msg="$indexOfArray should return -1 when end equals start", ), IndexOfArrayTest( - id="range_end_less_than_start", + "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", + msg="$indexOfArray should return -1 when end less than start", ), IndexOfArrayTest( - id="range_end_beyond_array", + "range_end_beyond_array", array=[1, 2, 3], search=3, start=0, end=100, expected=2, - msg="Should find when end beyond array", + msg="$indexOfArray should find when end beyond array", ), IndexOfArrayTest( - id="range_full_array", + "range_full_array", array=[1, 2, 3], search=2, start=0, end=3, expected=1, - msg="Should find in full array range", + msg="$indexOfArray should find in full array range", ), IndexOfArrayTest( - id="range_int64_bounds", + "range_int64_bounds", array=[10, 20, 30], search=20, start=Int64(0), end=Int64(3), expected=1, - msg="Should accept Int64 bounds", + msg="$indexOfArray should accept Int64 bounds", ), IndexOfArrayTest( - id="range_double_integral_bounds", + "range_double_integral_bounds", array=[10, 20, 30], search=20, start=0.0, end=3.0, expected=1, - msg="Should accept integral double bounds", + msg="$indexOfArray should accept integral double bounds", ), IndexOfArrayTest( - id="range_decimal128_bounds", + "range_decimal128_bounds", array=[10, 20, 30], search=20, start=Decimal128("0"), end=Decimal128("3"), expected=1, - msg="Should accept Decimal128 bounds", + msg="$indexOfArray 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", + "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", + msg="$indexOfArray should find third occurrence of 5 when start=3", ), IndexOfArrayTest( - id="dup_skip_different_value", + "dup_skip_different_value", array=[5, 3, 5, 3, 5], search=3, start=2, expected=3, - msg="Should find second 3 when start=2", + msg="$indexOfArray should find second 3 when start=2", ), ] # Success: detailed range semantics RANGE_SEMANTICS_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="end_exclusive_includes_before_boundary", + "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)", + msg="$indexOfArray element before end boundary should be included in [0,2)", ), IndexOfArrayTest( - id="end_exclusive_excludes_at_boundary", + "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)", + msg="$indexOfArray element at end boundary should be excluded from [0,1)", ), IndexOfArrayTest( - id="empty_range_at_array_length", + "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", + msg="$indexOfArray empty range [len,len) at array boundary should return -1", ), IndexOfArrayTest( - id="range_finds_dup_in_subrange", + "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", + msg="$indexOfArray should find value within range skipping earlier occurrences", ), IndexOfArrayTest( - id="start_at_array_length", + "start_at_array_length", array=["a", "b", "c"], search="a", start=3, expected=-1, - msg="Should return -1 when start equals array length", + msg="$indexOfArray should return -1 when start equals array length", ), IndexOfArrayTest( - id="start_and_end_both_beyond_array", + "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", + msg="$indexOfArray should return -1 when both start and end are beyond array", ), IndexOfArrayTest( - id="end_before_match_position", + "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", + msg="$indexOfArray 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", + "single_not_found", array=[1], search=2, expected=-1, - msg="Should return -1 when single element doesn't match", + msg="$indexOfArray should return -1 when single element doesn't match", ), IndexOfArrayTest( - id="single_found_in_range", + "single_found_in_range", array=[42], search=42, start=0, end=1, expected=0, - msg="Single element found in range [0,1)", + msg="$indexOfArray single element found in range [0,1)", ), IndexOfArrayTest( - id="single_empty_range", + "single_empty_range", array=[42], search=42, start=0, end=0, expected=-1, - msg="Single element not found in empty range [0,0)", + msg="$indexOfArray single element not found in empty range [0,0)", ), IndexOfArrayTest( - id="single_start_past_element", + "single_start_past_element", array=[42], search=42, start=1, expected=-1, - msg="Single element not found when start past it", + msg="$indexOfArray single element not found when start past it", ), IndexOfArrayTest( - id="all_null_from_start", + "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", + msg="$indexOfArray should find null at index 1 from start=1 in all-null array", ), IndexOfArrayTest( - id="all_null_search_different_type", + "all_null_search_different_type", array=[None, None, None], search=1, expected=-1, - msg="Should not find int in all-null array", + msg="$indexOfArray should not find int in all-null array", ), IndexOfArrayTest( - id="all_true_search_false", + "all_true_search_false", array=[True, True, True], search=False, expected=-1, - msg="Should not find false in all-true array", + msg="$indexOfArray should not find false in all-true array", ), ] # Success: mixed types in array MIXED_TYPE_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="mixed_find_string", + "mixed_find_string", array=[1, "2", True, None, [1]], search="2", expected=1, - msg="Should find string in mixed array", + msg="$indexOfArray should find string in mixed array", ), IndexOfArrayTest( - id="mixed_find_null", + "mixed_find_null", array=[1, "2", True, None, [1]], search=None, expected=3, - msg="Should find null in mixed array", + msg="$indexOfArray should find null in mixed array", ), IndexOfArrayTest( - id="mixed_find_array", + "mixed_find_array", array=[1, "2", True, None, [1]], search=[1], expected=4, - msg="Should find array in mixed array", + msg="$indexOfArray should find array in mixed array", ), ] @@ -509,61 +513,61 @@ LARGE_ARRAY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="large_array_first", + "large_array_first", array=list(range(20_000)), search=0, expected=0, - msg="Should find first in large array", + msg="$indexOfArray should find first in large array", ), IndexOfArrayTest( - id="large_array_last", + "large_array_last", array=list(range(20_000)), search=19_999, expected=19_999, - msg="Should find last in large array", + msg="$indexOfArray should find last in large array", ), IndexOfArrayTest( - id="large_array_middle", + "large_array_middle", array=list(range(20_000)), search=10_000, expected=10_000, - msg="Should find middle in large array", + msg="$indexOfArray should find middle in large array", ), IndexOfArrayTest( - id="large_array_not_found", + "large_array_not_found", array=list(range(20_000)), search=-1, expected=-1, - msg="Should return -1 for absent value in large array", + msg="$indexOfArray should return -1 for absent value in large array", ), IndexOfArrayTest( - id="large_array_with_start", + "large_array_with_start", array=list(range(20_000)), search=19_999, start=19_998, expected=19_999, - msg="Should find with start in large array", + msg="$indexOfArray should find with start in large array", ), ] # Negative zero treated as equivalent to positive zero in search NEGATIVE_ZERO_SEARCH_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="search_double_neg_zero_in_zeros", + "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", + "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", + "search_decimal128_neg_zero", array=[0, 1, 2], search=Decimal128("-0"), expected=0, @@ -574,67 +578,67 @@ # Boundary values for start/end indices BOUNDARY_INDEX_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="start_int32_max", + "start_int32_max", array=[1, 2, 3], search=1, start=INT32_MAX, expected=-1, - msg="Should return -1 with INT32_MAX start", + msg="$indexOfArray should return -1 with INT32_MAX start", ), IndexOfArrayTest( - id="end_int32_max", + "end_int32_max", array=[1, 2, 3], search=2, start=0, end=INT32_MAX, expected=1, - msg="Should find with INT32_MAX end", + msg="$indexOfArray should find with INT32_MAX end", ), IndexOfArrayTest( - id="start_and_end_int32_max", + "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", + msg="$indexOfArray should return -1 with both INT32_MAX", ), IndexOfArrayTest( - id="start_int32_max_minus_1", + "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", + msg="$indexOfArray 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", + "double_neg_zero_start", array=[10, 20, 30], search=10, start=-0.0, expected=0, - msg="Should treat -0.0 start as 0", + msg="$indexOfArray should treat -0.0 start as 0", ), IndexOfArrayTest( - id="decimal128_neg_zero_start", + "decimal128_neg_zero_start", array=[10, 20, 30], search=10, start=DECIMAL128_NEGATIVE_ZERO, expected=0, - msg="Should treat decimal128 -0 start as 0", + msg="$indexOfArray should treat decimal128 -0 start as 0", ), IndexOfArrayTest( - id="double_neg_zero_end", + "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", + msg="$indexOfArray should treat -0.0 end as 0", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py index d74c3cada..60df1d011 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py @@ -15,6 +15,9 @@ build_args, build_insert_args, ) +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, @@ -35,418 +38,418 @@ # Error: INT64_MAX start/end index (not representable as int32) BOUNDARY_ERROR_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="start_int64_max", + "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", + msg="$indexOfArray should reject INT64_MAX start", ), IndexOfArrayTest( - id="end_int64_max", + "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", + msg="$indexOfArray should reject INT64_MAX end", ), ] # Error: first argument not an array (and not null) NOT_ARRAY_ERROR_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="string_as_array", + "string_as_array", array="hello", search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject string as array", + msg="$indexOfArray should reject string as array", ), IndexOfArrayTest( - id="int_as_array", + "int_as_array", array=42, search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject int as array", + msg="$indexOfArray should reject int as array", ), IndexOfArrayTest( - id="double_as_array", + "double_as_array", array=3.14, search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject double as array", + msg="$indexOfArray should reject double as array", ), IndexOfArrayTest( - id="bool_true_as_array", + "bool_true_as_array", array=True, search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject bool true as array", + msg="$indexOfArray should reject bool true as array", ), IndexOfArrayTest( - id="bool_false_as_array", + "bool_false_as_array", array=False, search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject bool false as array", + msg="$indexOfArray should reject bool false as array", ), IndexOfArrayTest( - id="object_as_array", + "object_as_array", array={"a": 1}, search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject object as array", + msg="$indexOfArray should reject object as array", ), IndexOfArrayTest( - id="decimal128_as_array", + "decimal128_as_array", array=Decimal128("1"), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject decimal128 as array", + msg="$indexOfArray should reject decimal128 as array", ), IndexOfArrayTest( - id="int64_as_array", + "int64_as_array", array=Int64(1), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject int64 as array", + msg="$indexOfArray should reject int64 as array", ), IndexOfArrayTest( - id="binary_as_array", + "binary_as_array", array=Binary(b"x", 0), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject binary as array", + msg="$indexOfArray should reject binary as array", ), IndexOfArrayTest( - id="datetime_as_array", + "datetime_as_array", array=datetime(2024, 1, 1, tzinfo=timezone.utc), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject datetime as array", + msg="$indexOfArray should reject datetime as array", ), IndexOfArrayTest( - id="objectid_as_array", + "objectid_as_array", array=ObjectId(), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject objectid as array", + msg="$indexOfArray should reject objectid as array", ), IndexOfArrayTest( - id="regex_as_array", + "regex_as_array", array=Regex("x"), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject regex as array", + msg="$indexOfArray should reject regex as array", ), IndexOfArrayTest( - id="maxkey_as_array", + "maxkey_as_array", array=MaxKey(), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject maxkey as array", + msg="$indexOfArray should reject maxkey as array", ), IndexOfArrayTest( - id="minkey_as_array", + "minkey_as_array", array=MinKey(), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject minkey as array", + msg="$indexOfArray should reject minkey as array", ), IndexOfArrayTest( - id="timestamp_as_array", + "timestamp_as_array", array=Timestamp(0, 0), search=1, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, - msg="Should reject timestamp as array", + msg="$indexOfArray should reject timestamp as array", ), ] # Error: start index not integral START_NOT_INTEGRAL_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="start_fractional_double", + "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", + msg="$indexOfArray should reject fractional double start", ), IndexOfArrayTest( - id="start_fractional_decimal128", + "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", + msg="$indexOfArray should reject fractional decimal128 start", ), IndexOfArrayTest( - id="start_nan", + "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", + msg="$indexOfArray should reject NaN start", ), IndexOfArrayTest( - id="start_inf", + "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", + msg="$indexOfArray should reject infinity start", ), IndexOfArrayTest( - id="start_neg_inf", + "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", + msg="$indexOfArray should reject -infinity start", ), IndexOfArrayTest( - id="start_decimal128_nan", + "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", + msg="$indexOfArray should reject decimal128 NaN start", ), IndexOfArrayTest( - id="start_decimal128_inf", + "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", + msg="$indexOfArray should reject decimal128 infinity start", ), IndexOfArrayTest( - id="start_string", + "start_string", array=[1, 2, 3], search=1, start="0", error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject string start", + msg="$indexOfArray should reject string start", ), IndexOfArrayTest( - id="start_bool", + "start_bool", array=[1, 2, 3], search=1, start=True, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject bool start", + msg="$indexOfArray should reject bool start", ), IndexOfArrayTest( - id="start_array", + "start_array", array=[1, 2, 3], search=1, start=[0], error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject array start", + msg="$indexOfArray should reject array start", ), IndexOfArrayTest( - id="start_object", + "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", + msg="$indexOfArray should reject object start", ), ] # Error: end index not integral END_NOT_INTEGRAL_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="end_fractional_double", + "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", + msg="$indexOfArray should reject fractional double end", ), IndexOfArrayTest( - id="end_fractional_decimal128", + "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", + msg="$indexOfArray should reject fractional decimal128 end", ), IndexOfArrayTest( - id="end_nan", + "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", + msg="$indexOfArray should reject NaN end", ), IndexOfArrayTest( - id="end_inf", + "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", + msg="$indexOfArray should reject infinity end", ), IndexOfArrayTest( - id="end_string", + "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", + msg="$indexOfArray should reject string end", ), IndexOfArrayTest( - id="end_bool", + "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", + msg="$indexOfArray should reject bool end", ), IndexOfArrayTest( - id="end_neg_inf", + "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", + msg="$indexOfArray should reject -infinity end", ), IndexOfArrayTest( - id="end_decimal128_nan", + "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", + msg="$indexOfArray should reject decimal128 NaN end", ), IndexOfArrayTest( - id="end_decimal128_inf", + "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", + msg="$indexOfArray should reject decimal128 infinity end", ), IndexOfArrayTest( - id="end_array", + "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", + msg="$indexOfArray should reject array end", ), IndexOfArrayTest( - id="end_object", + "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", + msg="$indexOfArray should reject object end", ), ] # Error: negative start index START_NEGATIVE_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="start_neg_one", + "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", + msg="$indexOfArray should reject negative start -1", ), IndexOfArrayTest( - id="start_neg_large", + "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", + msg="$indexOfArray should reject negative start -100", ), IndexOfArrayTest( - id="start_neg_int64", + "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", + msg="$indexOfArray should reject negative Int64 start", ), IndexOfArrayTest( - id="start_neg_double", + "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", + msg="$indexOfArray should reject negative double start", ), IndexOfArrayTest( - id="start_neg_decimal128", + "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", + msg="$indexOfArray should reject negative decimal128 start", ), ] # Error: negative end index END_NEGATIVE_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="end_neg_one", + "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", + msg="$indexOfArray should reject negative end -1", ), IndexOfArrayTest( - id="end_neg_large", + "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", + msg="$indexOfArray should reject negative end -100", ), IndexOfArrayTest( - id="end_neg_int64", + "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", + msg="$indexOfArray should reject negative Int64 end", ), IndexOfArrayTest( - id="end_neg_double", + "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", + msg="$indexOfArray should reject negative double end", ), IndexOfArrayTest( - id="end_neg_decimal128", + "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", + msg="$indexOfArray should reject negative decimal128 end", ), ] @@ -462,21 +465,21 @@ LITERAL_ONLY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="start_missing_field", + "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", + msg="$indexOfArray should reject missing field as start", ), IndexOfArrayTest( - id="end_missing_field", + "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", + msg="$indexOfArray should reject missing field as end", ), ] @@ -506,19 +509,36 @@ 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"), - pytest.param({"$indexOfArray": [[1, 2, 3], 1, 0, 3, 99]}, id="five_args"), +# Property [Arity]: $indexOfArray requires two to four arguments. +ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_args", + expression={"$indexOfArray": []}, + error_code=EXPRESSION_ARITY_ERROR, + msg="$indexOfArray should reject zero arguments", + ), + ExpressionTestCase( + "one_arg", + expression={"$indexOfArray": [[1, 2, 3]]}, + error_code=EXPRESSION_ARITY_ERROR, + msg="$indexOfArray should reject one argument", + ), + ExpressionTestCase( + "five_args", + expression={"$indexOfArray": [[1, 2, 3], 1, 0, 3, 99]}, + error_code=EXPRESSION_ARITY_ERROR, + msg="$indexOfArray should reject five arguments", + ), ] -@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) +@pytest.mark.parametrize("test", pytest_params(ARITY_ERROR_TESTS)) +def test_indexOfArray_arity_error(collection, test): + """Test $indexOfArray arity error cases.""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) # Error: null as literal start/end index @@ -528,7 +548,9 @@ 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" + result, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="$indexOfArray should reject null end", ) @@ -540,5 +562,5 @@ def test_indexOfArray_null_start(collection): assert_expression_result( result, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, - msg="Null start should fail", + msg="$indexOfArray null start should fail", ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py index 96d32a0b0..474111a0d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py @@ -20,62 +20,62 @@ # Success: null/missing array → null (runs both literal and insert) NULL_ARRAY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="null_array", + "null_array", array=None, search=1, expected=None, - msg="Should return null for null array", + msg="$indexOfArray should return null for null array", ), IndexOfArrayTest( - id="null_array_with_start", + "null_array_with_start", array=None, search=1, start=0, expected=None, - msg="Should return null for null array with start", + msg="$indexOfArray 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", + "null_value_in_array", array=[1, None, 3], search=None, expected=1, - msg="Should find null in array", + msg="$indexOfArray should find null in array", ), IndexOfArrayTest( - id="null_value_not_in_array", + "null_value_not_in_array", array=[1, 2, 3], search=None, expected=-1, - msg="Should return -1 for null not in array", + msg="$indexOfArray should return -1 for null not in array", ), ] # Literal only: MISSING field refs LITERAL_ONLY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( - id="missing_array", + "missing_array", array=MISSING, search=1, expected=None, - msg="Should return null for missing array", + msg="$indexOfArray should return null for missing array", ), IndexOfArrayTest( - id="missing_value", + "missing_value", array=[1, 2, 3], search=MISSING, expected=-1, - msg="Should return -1 for missing search value", + msg="$indexOfArray should return -1 for missing search value", ), IndexOfArrayTest( - id="missing_value_null_in_array", + "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", + msg="$indexOfArray should return -1 for missing search even with null in array", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_indexOfArray.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_indexOfArray.py index d92b4fb31..3b2492b85 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_indexOfArray.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_smoke_indexOfArray.py @@ -31,4 +31,6 @@ 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, ignore_doc_order=True, msg="Should support $indexOfArray expression" + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py index b1d430550..5fda698b0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py @@ -40,133 +40,133 @@ # Arrays containing specific BSON types → true BSON_ARRAY_TRUE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="bindata_array", + "bindata_array", doc={"val": [Binary(b"\x00", 0)]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for BinData array", + msg="$isArray should return true for BinData array", ), ExpressionTestCase( - id="timestamp_array", + "timestamp_array", doc={"val": [Timestamp(0, 0)]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Timestamp array", + msg="$isArray should return true for Timestamp array", ), ExpressionTestCase( - id="int64_array", + "int64_array", doc={"val": [Int64(1)]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Int64 array", + msg="$isArray should return true for Int64 array", ), ExpressionTestCase( - id="decimal128_array", + "decimal128_array", doc={"val": [Decimal128("1")]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Decimal128 array", + msg="$isArray should return true for Decimal128 array", ), ExpressionTestCase( - id="objectid_array", + "objectid_array", doc={"val": [ObjectId()]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for ObjectId array", + msg="$isArray should return true for ObjectId array", ), ExpressionTestCase( - id="datetime_array", + "datetime_array", doc={"val": [datetime(2024, 1, 1, tzinfo=timezone.utc)]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for datetime array", + msg="$isArray should return true for datetime array", ), ExpressionTestCase( - id="minkey_array", + "minkey_array", doc={"val": [MinKey()]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for MinKey array", + msg="$isArray should return true for MinKey array", ), ExpressionTestCase( - id="maxkey_array", + "maxkey_array", doc={"val": [MaxKey()]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for MaxKey array", + msg="$isArray should return true for MaxKey array", ), ExpressionTestCase( - id="regex_array", + "regex_array", doc={"val": [Regex(".*")]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Regex array", + msg="$isArray should return true for Regex array", ), ExpressionTestCase( - id="nan_array", + "nan_array", doc={"val": [float("nan")]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for NaN array", + msg="$isArray should return true for NaN array", ), ExpressionTestCase( - id="inf_array", + "inf_array", doc={"val": [float("inf")]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Infinity array", + msg="$isArray should return true for Infinity array", ), ExpressionTestCase( - id="decimal128_nan_array", + "decimal128_nan_array", doc={"val": [Decimal128("NaN")]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Decimal128 NaN array", + msg="$isArray should return true for Decimal128 NaN array", ), ExpressionTestCase( - id="decimal128_inf_array", + "decimal128_inf_array", doc={"val": [Decimal128("Infinity")]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Decimal128 Infinity array", + msg="$isArray should return true for Decimal128 Infinity array", ), ExpressionTestCase( - id="decimal128_neg_nan_array", + "decimal128_neg_nan_array", doc={"val": [Decimal128("-NaN")]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Decimal128 -NaN array", + msg="$isArray should return true for Decimal128 -NaN array", ), ExpressionTestCase( - id="decimal128_neg_inf_array", + "decimal128_neg_inf_array", doc={"val": [DECIMAL128_NEGATIVE_INFINITY]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Decimal128 -Infinity array", + msg="$isArray should return true for Decimal128 -Infinity array", ), ExpressionTestCase( - id="neg_inf_array", + "neg_inf_array", doc={"val": [FLOAT_NEGATIVE_INFINITY]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for -Infinity array", + msg="$isArray should return true for -Infinity array", ), ExpressionTestCase( - id="neg_zero_array", + "neg_zero_array", doc={"val": [DOUBLE_NEGATIVE_ZERO]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for negative zero array", + msg="$isArray should return true for negative zero array", ), ExpressionTestCase( - id="decimal128_neg_zero_array", + "decimal128_neg_zero_array", doc={"val": [DECIMAL128_NEGATIVE_ZERO]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for Decimal128 -0 array", + msg="$isArray should return true for Decimal128 -0 array", ), ExpressionTestCase( - id="nested_mixed_bson_array", + "nested_mixed_bson_array", doc={ "val": [ MinKey(), @@ -178,187 +178,187 @@ }, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for nested mixed BSON array", + msg="$isArray should return true for nested mixed BSON array", ), ] # Non-array BSON types → false BSON_FALSE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int64", + "int64", doc={"val": Int64(1)}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Int64", + msg="$isArray should return false for Int64", ), ExpressionTestCase( - id="decimal128", + "decimal128", doc={"val": Decimal128("1")}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Decimal128", + msg="$isArray should return false for Decimal128", ), ExpressionTestCase( - id="objectid", + "objectid", doc={"val": ObjectId("000000000000000000000001")}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for ObjectId", + msg="$isArray should return false for ObjectId", ), ExpressionTestCase( - id="datetime", + "datetime", doc={"val": datetime(2024, 1, 1, tzinfo=timezone.utc)}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for datetime", + msg="$isArray should return false for datetime", ), ExpressionTestCase( - id="binary", + "binary", doc={"val": Binary(b"\x01", 0)}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Binary", + msg="$isArray should return false for Binary", ), ExpressionTestCase( - id="regex", + "regex", doc={"val": Regex("^abc")}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Regex", + msg="$isArray should return false for Regex", ), ExpressionTestCase( - id="timestamp", + "timestamp", doc={"val": Timestamp(1, 1)}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Timestamp", + msg="$isArray should return false for Timestamp", ), ExpressionTestCase( - id="minkey", + "minkey", doc={"val": MinKey()}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for MinKey", + msg="$isArray should return false for MinKey", ), ExpressionTestCase( - id="maxkey", + "maxkey", doc={"val": MaxKey()}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for MaxKey", + msg="$isArray should return false for MaxKey", ), ] # Special numeric values → false SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nan", + "nan", doc={"val": FLOAT_NAN}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for NaN", + msg="$isArray should return false for NaN", ), ExpressionTestCase( - id="inf", + "inf", doc={"val": FLOAT_INFINITY}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Infinity", + msg="$isArray should return false for Infinity", ), ExpressionTestCase( - id="neg_inf", + "neg_inf", doc={"val": FLOAT_NEGATIVE_INFINITY}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for -Infinity", + msg="$isArray should return false for -Infinity", ), ExpressionTestCase( - id="neg_zero", + "neg_zero", doc={"val": DOUBLE_NEGATIVE_ZERO}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for negative zero", + msg="$isArray should return false for negative zero", ), ExpressionTestCase( - id="decimal128_nan", + "decimal128_nan", doc={"val": DECIMAL128_NAN}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Decimal128 NaN", + msg="$isArray should return false for Decimal128 NaN", ), ExpressionTestCase( - id="decimal128_neg_nan", + "decimal128_neg_nan", doc={"val": Decimal128("-NaN")}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Decimal128 -NaN", + msg="$isArray should return false for Decimal128 -NaN", ), ExpressionTestCase( - id="decimal128_inf", + "decimal128_inf", doc={"val": DECIMAL128_INFINITY}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Decimal128 Infinity", + msg="$isArray should return false for Decimal128 Infinity", ), ExpressionTestCase( - id="decimal128_neg_inf", + "decimal128_neg_inf", doc={"val": DECIMAL128_NEGATIVE_INFINITY}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Decimal128 -Infinity", + msg="$isArray should return false for Decimal128 -Infinity", ), ExpressionTestCase( - id="decimal128_neg_zero", + "decimal128_neg_zero", doc={"val": DECIMAL128_NEGATIVE_ZERO}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for Decimal128 -0", + msg="$isArray should return false for Decimal128 -0", ), ] # Boundary values → false BOUNDARY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int32_max", + "int32_max", doc={"val": INT32_MAX}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for INT32_MAX", + msg="$isArray should return false for INT32_MAX", ), ExpressionTestCase( - id="int32_min", + "int32_min", doc={"val": INT32_MIN}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for INT32_MIN", + msg="$isArray should return false for INT32_MIN", ), ExpressionTestCase( - id="int64_max", + "int64_max", doc={"val": INT64_MAX}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for INT64_MAX", + msg="$isArray should return false for INT64_MAX", ), ExpressionTestCase( - id="int64_min", + "int64_min", doc={"val": INT64_MIN}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for INT64_MIN", + msg="$isArray should return false for INT64_MIN", ), ExpressionTestCase( - id="decimal128_max", + "decimal128_max", doc={"val": DECIMAL128_MAX}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for DECIMAL128_MAX", + msg="$isArray should return false for DECIMAL128_MAX", ), ExpressionTestCase( - id="decimal128_min", + "decimal128_min", doc={"val": DECIMAL128_MIN}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for DECIMAL128_MIN", + msg="$isArray should return false for DECIMAL128_MIN", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py index ba05d3a46..a2cddcdee 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py @@ -20,208 +20,208 @@ # Success: arrays → true IS_ARRAY_TRUE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="simple_array", + "simple_array", doc={"val": [1, 2, 3]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for simple array", + msg="$isArray should return true for simple array", ), ExpressionTestCase( - id="empty_array", + "empty_array", doc={"val": []}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for empty array", + msg="$isArray should return true for empty array", ), ExpressionTestCase( - id="single_element", + "single_element", doc={"val": [42]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for single-element array", + msg="$isArray should return true for single-element array", ), ExpressionTestCase( - id="nested_array", + "nested_array", doc={"val": [[1, 2], [3, 4]]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for nested array", + msg="$isArray should return true for nested array", ), ExpressionTestCase( - id="mixed_type_array", + "mixed_type_array", doc={"val": [1, "two", True, None, {"a": 1}]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for mixed-type array", + msg="$isArray should return true for mixed-type array", ), ExpressionTestCase( - id="array_of_objects", + "array_of_objects", doc={"val": [{"a": 1}, {"b": 2}]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for array of objects", + msg="$isArray should return true for array of objects", ), ExpressionTestCase( - id="array_of_nulls", + "array_of_nulls", doc={"val": [None, None]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for array of nulls", + msg="$isArray should return true for array of nulls", ), ExpressionTestCase( - id="string_array", + "string_array", doc={"val": ["a", "b", "c"]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for string array", + msg="$isArray should return true for string array", ), ExpressionTestCase( - id="bool_array", + "bool_array", doc={"val": [True]}, expression={"$isArray": "$val"}, expected=True, - msg="Should return true for bool array", + msg="$isArray should return true for bool array", ), ExpressionTestCase( - id="large_array_10k", + "large_array_10k", doc={"val": list(range(10000))}, expression={"$isArray": "$val"}, expected=True, - msg="10K element array returns true", + msg="$isArray 10K element array returns true", ), ExpressionTestCase( - id="deeply_nested_array", + "deeply_nested_array", doc={"val": [[[[[[1]]]]]]}, expression={"$isArray": "$val"}, expected=True, - msg="Deeply nested array returns true", + msg="$isArray deeply nested array returns true", ), ExpressionTestCase( - id="large_array_of_arrays", + "large_array_of_arrays", doc={"val": [[i] for i in range(10000)]}, expression={"$isArray": "$val"}, expected=True, - msg="10K nested arrays returns true", + msg="$isArray 10K nested arrays returns true", ), ] # Success: non-arrays → false IS_ARRAY_FALSE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string", + "string", doc={"val": "hello"}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for string", + msg="$isArray should return false for string", ), ExpressionTestCase( - id="int", + "int", doc={"val": 42}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for int", + msg="$isArray should return false for int", ), ExpressionTestCase( - id="double", + "double", doc={"val": 3.14}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for double", + msg="$isArray should return false for double", ), ExpressionTestCase( - id="bool_true", + "bool_true", doc={"val": True}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for true", + msg="$isArray should return false for true", ), ExpressionTestCase( - id="bool_false", + "bool_false", doc={"val": False}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for false", + msg="$isArray should return false for false", ), ExpressionTestCase( - id="null", + "null", doc={"val": None}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for null", + msg="$isArray should return false for null", ), ExpressionTestCase( - id="object", + "object", doc={"val": {"a": 1}}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for object", + msg="$isArray should return false for object", ), ExpressionTestCase( - id="empty_string", + "empty_string", doc={"val": ""}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for empty string", + msg="$isArray should return false for empty string", ), ExpressionTestCase( - id="empty_object", + "empty_object", doc={"val": {}}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for empty object", + msg="$isArray should return false for empty object", ), ExpressionTestCase( - id="zero", + "zero", doc={"val": 0}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for zero", + msg="$isArray should return false for zero", ), ExpressionTestCase( - id="negative_int", + "negative_int", doc={"val": -123}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for negative int", + msg="$isArray should return false for negative int", ), ExpressionTestCase( - id="negative_double", + "negative_double", doc={"val": -1.23}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for negative double", + msg="$isArray should return false for negative double", ), ] # Array-like edge cases → false ARRAY_LIKE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string_brackets", + "string_brackets", doc={"val": "[]"}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for string '[]'", + msg="$isArray should return false for string '[]'", ), ExpressionTestCase( - id="string_array_repr", + "string_array_repr", doc={"val": "[1, 2, 3]"}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for string '[1, 2, 3]'", + msg="$isArray should return false for string '[1, 2, 3]'", ), ExpressionTestCase( - id="array_like_object", + "array_like_object", doc={"val": {"0": "a", "1": "b"}}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for array-like object", + msg="$isArray should return false for array-like object", ), ExpressionTestCase( - id="length_object", + "length_object", doc={"val": {"length": 3}}, expression={"$isArray": "$val"}, expected=False, - msg="Should return false for object with length key", + msg="$isArray should return false for object with length key", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_errors.py index 62e865714..ade4b93ad 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_errors.py @@ -6,22 +6,45 @@ 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 EXPRESSION_TYPE_MISMATCH_ERROR +from documentdb_tests.framework.parametrize import pytest_params -# 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"), +# Property [Arity]: $isArray requires exactly one argument. +ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_args", + expression={"$isArray": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$isArray should reject zero arguments", + ), + ExpressionTestCase( + "two_args", + expression={"$isArray": [1, 2]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$isArray should reject two arguments", + ), + ExpressionTestCase( + "three_args", + expression={"$isArray": [1, 2, 3]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$isArray should reject three arguments", + ), ] +ALL_ERROR_TESTS = ARITY_ERROR_TESTS + -@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) +@pytest.mark.parametrize("test", pytest_params(ALL_ERROR_TESTS)) +def test_isArray_error(collection, test): + """Test $isArray error cases.""" + result = execute_expression(collection, test.expression) + 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_isArray_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_expressions.py index ab5e1680f..f26f7b865 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_expressions.py @@ -19,125 +19,125 @@ SELF_NESTING_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="self_nested", + "self_nested", expression={"$isArray": [{"$isArray": "$arr"}]}, doc={"arr": [1, 2]}, expected=False, - msg="Inner returns bool, outer returns false", + msg="$isArray inner returns bool, outer returns false", ), ] # Field path lookups FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_field_array", + "nested_field_array", expression={"$isArray": "$a.b"}, doc={"a": {"b": [1, 2]}}, expected=True, - msg="Nested array field", + msg="$isArray nested array field", ), ExpressionTestCase( - id="deeply_nested_field_array", + "deeply_nested_field_array", expression={"$isArray": "$a.b.c"}, doc={"a": {"b": {"c": [1]}}}, expected=True, - msg="Deeply nested array field", + msg="$isArray deeply nested array field", ), ExpressionTestCase( - id="numeric_index_on_object_key", + "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", + msg="$isArray numeric key on object resolves to array", ), ] # Composite array paths COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="composite_array_path", + "composite_array_path", expression={"$isArray": "$a.b"}, doc={"a": [{"b": 1}, {"b": 2}]}, expected=True, - msg="Composite array path should resolve to array", + msg="$isArray composite array path should resolve to array", ), ExpressionTestCase( - id="composite_empty_array", + "composite_empty_array", expression={"$isArray": "$a.b"}, doc={"a": []}, expected=True, - msg="Composite on empty array resolves to empty array", + msg="$isArray composite on empty array resolves to empty array", ), ExpressionTestCase( - id="composite_three_elements", + "composite_three_elements", expression={"$isArray": "$a.b"}, doc={"a": [{"b": 1}, {"b": 2}, {"b": 3}]}, expected=True, - msg="Three-element composite resolves to array", + msg="$isArray three-element composite resolves to array", ), ] # Deep composite array traversal DEEP_COMPOSITE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="array_at_leaf", + "array_at_leaf", expression={"$isArray": "$a.b.c.d"}, doc={"a": {"b": {"c": {"d": [1, 2, 3]}}}}, expected=True, - msg="Array at leaf level", + msg="$isArray array at leaf level", ), ExpressionTestCase( - id="array_at_c", + "array_at_c", expression={"$isArray": "$a.b.c.d"}, doc={"a": {"b": {"c": [{"d": 1}]}}}, expected=True, - msg="Array at c level", + msg="$isArray array at c level", ), ] # Null and missing handling NULL_MISSING_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="missing_field", + "missing_field", expression={"$isArray": "$nonexistent"}, doc={"other": 1}, expected=False, - msg="Missing field should return false", + msg="$isArray missing field should return false", ), ExpressionTestCase( - id="missing_nested_partial_path", + "missing_nested_partial_path", expression={"$isArray": "$a.b"}, doc={"a": 1}, expected=False, - msg="Nested field on scalar parent returns false", + msg="$isArray nested field on scalar parent returns false", ), ExpressionTestCase( - id="missing_nested_empty_doc", + "missing_nested_empty_doc", expression={"$isArray": "$a.b"}, doc={"x": 1}, expected=False, - msg="Missing nested field returns false", + msg="$isArray missing nested field returns false", ), ] # System variables SYSTEM_VAR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="root_variable", + "root_variable", expression={"$isArray": "$$ROOT"}, doc={"a": 1}, expected=False, msg="$$ROOT is object, returns false", ), ExpressionTestCase( - id="current_variable", + "current_variable", expression={"$isArray": "$$CURRENT"}, doc={"a": 1}, expected=False, msg="$$CURRENT is object, returns false", ), ExpressionTestCase( - id="let_array_variable", + "let_array_variable", expression={"$let": {"vars": {"x": "$arr"}, "in": {"$isArray": "$$x"}}}, doc={"arr": [1, 2]}, expected=True, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_isArray.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_isArray.py index c16ab7d55..e705f17a9 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_isArray.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_smoke_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, ignore_doc_order=True, msg="Should support $isArray expression") 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 8d742302e..d34effa94 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 @@ -19,71 +19,71 @@ ARRAY_ELEM_AT_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="arrayElemAt_index_from_indexOfArray", + "arrayElemAt_index_from_indexOfArray", expression={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 30]}]}, expected=30, - msg="Should use $indexOfArray result as index", + msg="$indexOfArray should use $indexOfArray result as index", ), ExpressionTestCase( - id="arrayElemAt_last_element_via_size", + "arrayElemAt_last_element_via_size", expression={"$arrayElemAt": [[10, 20, 30], {"$subtract": [{"$size": [[10, 20, 30]]}, 1]}]}, expected=30, - msg="Should access last element via $size - 1", + msg="$indexOfArray should access last element via $size - 1", ), ExpressionTestCase( - id="arrayElemAt_elem_from_slice", + "arrayElemAt_elem_from_slice", expression={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], -2]}, 0]}, expected=30, - msg="Should access element from $slice result", + msg="$indexOfArray should access element from $slice result", ), ExpressionTestCase( - id="arrayElemAt_elem_from_slice_3arg", + "arrayElemAt_elem_from_slice_3arg", expression={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], 1, 2]}, 1]}, expected=30, - msg="Should access element from $slice 3-arg result", + msg="$indexOfArray should access element from $slice 3-arg result", ), ExpressionTestCase( - id="arrayElemAt_elem_from_reverseArray", + "arrayElemAt_elem_from_reverseArray", expression={"$arrayElemAt": [{"$reverseArray": [[10, 20, 30]]}, 0]}, expected=30, - msg="Should access element from $reverseArray result", + msg="$indexOfArray should access element from $reverseArray result", ), ExpressionTestCase( - id="arrayElemAt_elem_from_concatArrays", + "arrayElemAt_elem_from_concatArrays", expression={"$arrayElemAt": [{"$concatArrays": [[10, 20], [30, 40]]}, 2]}, expected=30, - msg="Should access element from $concatArrays result", + msg="$indexOfArray should access element from $concatArrays result", ), ExpressionTestCase( - id="arrayElemAt_computed_index", + "arrayElemAt_computed_index", expression={"$arrayElemAt": [[10, 20, 30], {"$subtract": [3, 1]}]}, expected=30, - msg="Should use computed index from $subtract", + msg="$indexOfArray should use computed index from $subtract", ), ] # $in combinations IN_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="in_value_from_add", + "in_value_from_add", expression={"$in": [{"$add": [1, 1]}, [1, 2, 3]]}, expected=True, - msg="Should find value computed by $add", + msg="$indexOfArray should find value computed by $add", ), ExpressionTestCase( - id="in_array_from_concatArrays", + "in_array_from_concatArrays", expression={"$in": [3, {"$concatArrays": [[1, 2], [3, 4]]}]}, expected=True, - msg="Should search in $concatArrays result", + msg="$indexOfArray should search in $concatArrays result", ), ExpressionTestCase( - id="in_value_from_arrayElemAt", + "in_value_from_arrayElemAt", expression={"$in": [{"$arrayElemAt": [[10, 20, 30], 1]}, [5, 20, 35]]}, expected=True, - msg="Should find value from $arrayElemAt", + msg="$indexOfArray should find value from $arrayElemAt", ), ExpressionTestCase( - id="in_array_from_filter", + "in_array_from_filter", expression={ "$in": [ 4, @@ -97,10 +97,10 @@ ] }, expected=True, - msg="Should search in $filter result", + msg="$indexOfArray should search in $filter result", ), ExpressionTestCase( - id="in_array_from_map", + "in_array_from_map", expression={ "$in": [ 20, @@ -114,34 +114,34 @@ ] }, expected=True, - msg="Should search in $map result", + msg="$indexOfArray should search in $map result", ), ExpressionTestCase( - id="in_array_from_reverseArray", + "in_array_from_reverseArray", expression={"$in": [1, {"$reverseArray": [[1, 2, 3]]}]}, expected=True, - msg="Should search in $reverseArray result", + msg="$indexOfArray should search in $reverseArray result", ), ExpressionTestCase( - id="in_cond_with_inner_in", + "in_cond_with_inner_in", expression={"$in": [5, {"$cond": [{"$in": ["a", ["a", "b"]]}, [5, 6], [7, 8]]}]}, expected=True, - msg="Should search in $cond-selected array", + msg="$indexOfArray should search in $cond-selected array", ), ExpressionTestCase( - id="in_inside_cond", + "in_inside_cond", expression={"$cond": [{"$in": [2, [1, 2, 3]]}, "found", "not_found"]}, expected="found", - msg="Should use $in result in $cond", + msg="$indexOfArray should use $in result in $cond", ), ExpressionTestCase( - id="in_value_from_indexOfArray", + "in_value_from_indexOfArray", expression={"$in": [{"$indexOfArray": [[10, 20, 30], 20]}, [0, 1, 2]]}, expected=True, - msg="Should find $indexOfArray result in array", + msg="$indexOfArray should find $indexOfArray result in array", ), ExpressionTestCase( - id="in_nested_decimal128", + "in_nested_decimal128", expression={ "$in": [ {"$arrayElemAt": [[Decimal128("1.1"), Decimal128("2.2")], 1]}, @@ -149,32 +149,32 @@ ] }, expected=True, - msg="Should find Decimal128 from $arrayElemAt in array", + msg="$indexOfArray should find Decimal128 from $arrayElemAt in array", ), ] # $indexOfArray combinations INDEX_OF_ARRAY_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="indexOfArray_result_as_arrayElemAt_index", + "indexOfArray_result_as_arrayElemAt_index", expression={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 20]}]}, expected=20, - msg="Should use $indexOfArray result as $arrayElemAt index", + msg="$indexOfArray should use $indexOfArray result as $arrayElemAt index", ), ExpressionTestCase( - id="indexOfArray_search_from_add", + "indexOfArray_search_from_add", expression={"$indexOfArray": [[1, 2, 3], {"$add": [1, 1]}]}, expected=1, - msg="Should search for value computed by $add", + msg="$indexOfArray should search for value computed by $add", ), ExpressionTestCase( - id="indexOfArray_array_from_concatArrays", + "indexOfArray_array_from_concatArrays", expression={"$indexOfArray": [{"$concatArrays": [[1, 2], [3, 4]]}, 3]}, expected=2, - msg="Should search in $concatArrays result", + msg="$indexOfArray should search in $concatArrays result", ), ExpressionTestCase( - id="indexOfArray_array_from_filter", + "indexOfArray_array_from_filter", expression={ "$indexOfArray": [ {"$filter": {"input": [1, 2, 3, 4, 5], "cond": {"$gt": ["$$this", 2]}}}, @@ -182,24 +182,24 @@ ] }, expected=1, - msg="Should search in $filter result", + msg="$indexOfArray should search in $filter result", ), ExpressionTestCase( - id="indexOfArray_result_in_cond", + "indexOfArray_result_in_cond", expression={ "$cond": [{"$gte": [{"$indexOfArray": [[1, 2, 3], 2]}, 0]}, "found", "not_found"] }, expected="found", - msg="Should use $indexOfArray result in $cond", + msg="$indexOfArray should use $indexOfArray result in $cond", ), ExpressionTestCase( - id="indexOfArray_start_from_subtract", + "indexOfArray_start_from_subtract", expression={"$indexOfArray": [[1, 2, 1, 2], 1, {"$subtract": [3, 1]}]}, expected=2, - msg="Should use $subtract result as start index", + msg="$indexOfArray should use $subtract result as start index", ), ExpressionTestCase( - id="indexOfArray_via_arrayElemAt", + "indexOfArray_via_arrayElemAt", expression={ "$indexOfArray": [ ["a", "b", "c", "d"], @@ -212,10 +212,10 @@ ] }, expected=1, - msg="Should search for value from nested $arrayElemAt/$indexOfArray", + msg="$indexOfArray should search for value from nested $arrayElemAt/$indexOfArray", ), ExpressionTestCase( - id="indexOfArray_subarray_mixed_bson", + "indexOfArray_subarray_mixed_bson", expression={ "$indexOfArray": [ [[MinKey(), MaxKey()], [1, 2], "x"], @@ -228,10 +228,10 @@ ] }, expected=1, - msg="Should find mixed BSON subarray via nested operators", + msg="$indexOfArray should find mixed BSON subarray via nested operators", ), ExpressionTestCase( - id="indexOfArray_triple_nested_decimal128", + "indexOfArray_triple_nested_decimal128", expression={ "$indexOfArray": [ [Decimal128("1.1"), Decimal128("2.2"), Decimal128("3.3")], @@ -249,26 +249,26 @@ ] }, expected=2, - msg="Should resolve triple-nested Decimal128 operators", + msg="$indexOfArray should resolve triple-nested Decimal128 operators", ), ] # $slice combinations SLICE_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="slice_array_from_concatArrays", + "slice_array_from_concatArrays", expression={"$slice": [{"$concatArrays": [[1, 2], [3, 4, 5]]}, 3]}, expected=[1, 2, 3], - msg="Should slice $concatArrays result", + msg="$indexOfArray should slice $concatArrays result", ), ExpressionTestCase( - id="slice_n_from_subtract", + "slice_n_from_subtract", expression={"$slice": [[1, 2, 3, 4, 5], {"$subtract": [5, 2]}]}, expected=[1, 2, 3], - msg="Should use $subtract result as n", + msg="$indexOfArray should use $subtract result as n", ), ExpressionTestCase( - id="slice_array_from_filter", + "slice_array_from_filter", expression={ "$slice": [ { @@ -282,10 +282,10 @@ ] }, expected=[3, 4], - msg="Should slice $filter result", + msg="$indexOfArray should slice $filter result", ), ExpressionTestCase( - id="slice_position_from_indexOfArray", + "slice_position_from_indexOfArray", expression={ "$slice": [ [10, 20, 30, 40, 50], @@ -294,10 +294,10 @@ ] }, expected=[30, 40], - msg="Should use $indexOfArray result as position", + msg="$indexOfArray should use $indexOfArray result as position", ), ExpressionTestCase( - id="slice_array_from_map", + "slice_array_from_map", expression={ "$slice": [ { @@ -311,21 +311,21 @@ ] }, expected=[10, 20], - msg="Should slice $map result", + msg="$indexOfArray should slice $map result", ), ExpressionTestCase( - id="slice_array_from_reverseArray", + "slice_array_from_reverseArray", expression={"$slice": [{"$reverseArray": [[1, 2, 3, 4, 5]]}, 3]}, expected=[5, 4, 3], - msg="Should slice $reverseArray result", + msg="$indexOfArray should slice $reverseArray result", ), ExpressionTestCase( - id="slice_n_from_size", + "slice_n_from_size", 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", + msg="$indexOfArray should use $size-based computation as n", ), ] 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 index 87a7d2cba..73a5b7873 100644 --- 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 @@ -15,14 +15,14 @@ FILTER_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="filter_then_size", + "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", + msg="$filter size of filtered array", ), ExpressionTestCase( - id="map_then_filter", + "map_then_filter", expression={ "$filter": { "input": {"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, @@ -31,17 +31,17 @@ }, doc={"arr": [1, 2, 3, 4, 5]}, expected=[6, 8, 10], - msg="Should filter mapped array", + msg="$filter should filter mapped array", ), ExpressionTestCase( - id="isArray_on_filter_result", + "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", + "filter_result_into_reduce", expression={ "$reduce": { "input": {"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 3]}}}, 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 index 5c7566b68..bb74169da 100644 --- 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 @@ -15,28 +15,28 @@ ISARRAY_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="isarray_guard_array", + "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", + "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", + "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", + "isarray_on_non_array_expression", expression={"$isArray": {"$add": ["$x", "$y"]}}, doc={"x": 1, "y": 2}, expected=False, From 32a26c345b5387be02aa40aa42f79526cd5aa60e Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 15:35:41 -0700 Subject: [PATCH 16/21] convert standalone to use ExpressionTestCase Signed-off-by: Alina (Xi) Li --- .../array/in/test_in_expressions.py | 151 ++++++++------- .../indexOfArray/test_indexOfArray_errors.py | 35 ++-- .../test_indexOfArray_expressions.py | 175 ++++++++++-------- ...array_arrayElemAt_indexOfArray_in_slice.py | 30 +-- 4 files changed, 221 insertions(+), 170 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_expressions.py index bdf92b5e6..bd6e4efe9 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_expressions.py @@ -7,80 +7,107 @@ 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, execute_expression_with_insert, ) from documentdb_tests.framework.error_codes import EXPRESSION_IN_NOT_ARRAY_ERROR +from documentdb_tests.framework.parametrize import pytest_params +# Property [Nested Expressions]: $in evaluates nested expressions as arguments. +NESTED_EXPRESSION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_in_in", + expression={"$in": [{"$in": [2, [1, 2, 3]]}, [True, False]]}, + expected=True, + msg="$in should accept nested $in result as search value", + ), +] -# 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) +# Property [Field Path Resolution]: $in resolves nested and deeply nested field paths. +FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_field_found", + expression={"$in": [20, "$a.b"]}, + doc={"a": {"b": [10, 20, 30]}}, + expected=True, + msg="$in should find value in nested field path array", + ), + ExpressionTestCase( + "nested_field_not_found", + expression={"$in": [99, "$a.b"]}, + doc={"a": {"b": [10, 20, 30]}}, + expected=False, + msg="$in should not find absent value in nested field path array", + ), + ExpressionTestCase( + "deeply_nested_field", + expression={"$in": [7, "$a.b.c"]}, + doc={"a": {"b": {"c": [5, 6, 7]}}}, + expected=True, + msg="$in should resolve deeply nested field path", + ), +] +# Property [Missing Field]: $in handles missing array and value fields correctly. +MISSING_FIELD_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nonexistent_array_field", + expression={"$in": [1, "$nonexistent"]}, + doc={"other": 1}, + error_code=EXPRESSION_IN_NOT_ARRAY_ERROR, + msg="$in should error when array field does not exist", + ), + ExpressionTestCase( + "nonexistent_value_array_contains_null", + expression={"$in": ["$nonexistent", "$arr"]}, + doc={"arr": [1, None, 3]}, + expected=False, + msg="$in should return false for missing value even when array contains null", + ), + ExpressionTestCase( + "nonexistent_value_array_without_null", + expression={"$in": ["$nonexistent", "$arr"]}, + doc={"arr": [1, 2, 3]}, + expected=False, + msg="$in should return false for missing value in array without null", + ), +] -# 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) +# Property [Composite Paths]: $in resolves composite array paths from array-of-objects. +COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "composite_array_as_array", + expression={"$in": [20, "$x.y"]}, + doc={"x": [{"y": 10}, {"y": 20}, {"y": 30}]}, + expected=True, + msg="$in should find value in composite array path", + ), + ExpressionTestCase( + "composite_array_as_value", + expression={"$in": ["$x.y", [[10, 20, 30], "other"]]}, + doc={"x": [{"y": 10}, {"y": 20}, {"y": 30}]}, + expected=True, + msg="$in should match composite array as search value", + ), +] - -# 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=["array_contains_null", "array_without_null"], +ALL_EXPRESSION_TESTS = ( + NESTED_EXPRESSION_TESTS + FIELD_LOOKUP_TESTS + MISSING_FIELD_TESTS + COMPOSITE_PATH_TESTS ) -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}]}, +@pytest.mark.parametrize("test", pytest_params(ALL_EXPRESSION_TESTS)) +def test_in_expression(collection, test): + """Test $in with expressions, field paths, and composite paths.""" + if test.doc is None: + result = execute_expression(collection, test.expression) + else: + 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 ) - assert_expression_result(result, expected=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py index 60df1d011..407131726 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py @@ -541,26 +541,27 @@ def test_indexOfArray_arity_error(collection, test): ) -# 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, +# Property [Null Index]: $indexOfArray rejects null as start or end index. +NULL_INDEX_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_end", + expression={"$indexOfArray": [[1, 2, 3], 1, 0, None]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject null end", - ) - - -def test_indexOfArray_null_start(collection): - """Test $indexOfArray with null as start index errors.""" + ), + ExpressionTestCase( + "null_start", + expression={"$indexOfArray": [[1, 2, 3], 1, None]}, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="$indexOfArray should reject null start", + ), +] - result = execute_expression(collection, {"$indexOfArray": [[1, 2, 3], 1, None]}) +@pytest.mark.parametrize("test", pytest_params(NULL_INDEX_ERROR_TESTS)) +def test_indexOfArray_null_index_error(collection, test): + """Test $indexOfArray rejects null as start or end index.""" + result = execute_expression(collection, test.expression) assert_expression_result( - result, - error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, - msg="$indexOfArray null start should fail", + 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_indexOfArray_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_expressions.py index d367d62cd..4f7d6662c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_expressions.py @@ -7,93 +7,112 @@ 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, execute_expression_with_insert, ) +from documentdb_tests.framework.parametrize import pytest_params +# Property [Nested Expressions]: $indexOfArray evaluates nested expressions as arguments. +NESTED_EXPRESSION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_2_level", + expression={ + "$indexOfArray": [ + [0, 1, 2, 3], + {"$indexOfArray": [[10, 20, 30], 30]}, + ] + }, + expected=2, + msg="$indexOfArray should use inner result as search value", + ), + ExpressionTestCase( + "nested_3_level", + expression={ + "$indexOfArray": [ + [0, 1, 2], + { + "$indexOfArray": [ + [0, 1, 2], + {"$indexOfArray": [[5, 10, 15], 10]}, + ] + }, + ] + }, + expected=1, + msg="$indexOfArray should support triple nested composition", + ), + ExpressionTestCase( + "nested_start_index", + expression={ + "$indexOfArray": [ + [1, 2, 1, 2], + 1, + {"$indexOfArray": [[10, 20, 30], 20]}, + ] + }, + expected=2, + msg="$indexOfArray should use nested result as start index", + ), +] -# 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) +# Property [Field Path Resolution]: $indexOfArray resolves nested and deeply nested field paths. +FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_field_path", + expression={"$indexOfArray": ["$a.b", 20]}, + doc={"a": {"b": [10, 20, 30]}}, + expected=1, + msg="$indexOfArray should resolve nested field path as array", + ), + ExpressionTestCase( + "nonexistent_field_null", + expression={"$indexOfArray": ["$a.nonexistent", 0]}, + doc={"a": {"missing": 1}}, + expected=None, + msg="$indexOfArray should return null for non-existent field", + ), + ExpressionTestCase( + "deeply_nested_field", + expression={"$indexOfArray": ["$a.b.c", 7]}, + doc={"a": {"b": {"c": [5, 6, 7]}}}, + expected=2, + msg="$indexOfArray should resolve deeply nested field path", + ), +] +# Property [Composite Paths]: $indexOfArray resolves composite array paths from array-of-objects. +COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "composite_array_as_array", + expression={"$indexOfArray": ["$x.y", 20]}, + doc={"x": [{"y": 10}, {"y": 20}, {"y": 30}]}, + expected=1, + msg="$indexOfArray should find value in composite array path", + ), + ExpressionTestCase( + "composite_array_as_search", + expression={"$indexOfArray": [[[10, 20], [30, 40]], "$x.y"]}, + doc={"x": [{"y": 30}, {"y": 40}]}, + expected=1, + msg="$indexOfArray should match composite array as search value", + ), +] -# 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) - - -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) +ALL_EXPRESSION_TESTS = NESTED_EXPRESSION_TESTS + FIELD_LOOKUP_TESTS + COMPOSITE_PATH_TESTS -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}]}, +@pytest.mark.parametrize("test", pytest_params(ALL_EXPRESSION_TESTS)) +def test_indexOfArray_expression(collection, test): + """Test $indexOfArray with expressions, field paths, and composite paths.""" + if test.doc is None: + result = execute_expression(collection, test.expression) + else: + 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 ) - assert_expression_result(result, expected=1) 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 d34effa94..e0ce66eaf 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 @@ -329,12 +329,29 @@ ), ] +# Property [Type Preservation]: $arrayElemAt preserves element type and reports missing for OOB. +ARRAY_ELEM_AT_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "arrayElemAt_oob_is_missing", + expression={"$type": {"$arrayElemAt": [[1, 2, 3], 10]}}, + expected="missing", + msg="$arrayElemAt out-of-bounds should produce missing, not null", + ), + ExpressionTestCase( + "arrayElemAt_regex_type_preserved", + expression={"$type": {"$arrayElemAt": [[Regex("abc")], 0]}}, + expected="regex", + msg="$arrayElemAt should preserve the regex element type", + ), +] + # Aggregate all combination tests ALL_COMBINATION_TESTS = ( ARRAY_ELEM_AT_COMBINATION_TESTS + IN_COMBINATION_TESTS + INDEX_OF_ARRAY_COMBINATION_TESTS + SLICE_COMBINATION_TESTS + + ARRAY_ELEM_AT_TYPE_TESTS ) @@ -343,16 +360,3 @@ def test_combination_expression(collection, test): """Test array operators composed with other operators.""" 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]}}) - 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") From d6c6974038d3285659f6d005171fde814827eb11 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 15:52:49 -0700 Subject: [PATCH 17/21] replace float() with FLOAT_ Signed-off-by: Alina (Xi) Li --- .../array/filter/test_filter_as_errors.py | 8 ++++++-- .../array/filter/test_filter_errors.py | 6 +++--- .../indexOfArray/test_indexOfArray_errors.py | 15 +++++++++------ .../array/isArray/test_isArray_bson_types.py | 4 ++-- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_as_errors.py index b1d2089ea..8e9ba4e4f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_as_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_as_errors.py @@ -18,6 +18,10 @@ ) from documentdb_tests.framework.error_codes import FAILED_TO_PARSE_ERROR from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + FLOAT_INFINITY, + FLOAT_NAN, +) INVALID_AS_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -112,13 +116,13 @@ ), ExpressionTestCase( "type_nan", - expression={"$filter": {"input": [1, 2, 3], "as": float("nan"), "cond": True}}, + expression={"$filter": {"input": [1, 2, 3], "as": FLOAT_NAN, "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, msg="$filter should reject NaN as variable name", ), ExpressionTestCase( "type_infinity", - expression={"$filter": {"input": [1, 2, 3], "as": float("inf"), "cond": True}}, + expression={"$filter": {"input": [1, 2, 3], "as": FLOAT_INFINITY, "cond": True}}, error_code=FAILED_TO_PARSE_ERROR, msg="$filter should reject inf as variable name", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py index 178b67d4f..258cb915a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py @@ -363,21 +363,21 @@ ), ExpressionTestCase( "nan_limit", - expression={"$filter": {"input": "$arr", "cond": True, "limit": float("nan")}}, + expression={"$filter": {"input": "$arr", "cond": True, "limit": FLOAT_NAN}}, doc={"arr": [1, 2, 3]}, error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, msg="$filter naN should error", ), ExpressionTestCase( "inf_limit", - expression={"$filter": {"input": "$arr", "cond": True, "limit": float("inf")}}, + expression={"$filter": {"input": "$arr", "cond": True, "limit": FLOAT_INFINITY}}, doc={"arr": [1, 2, 3]}, error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, msg="$filter infinity should error", ), ExpressionTestCase( "neg_inf_limit", - expression={"$filter": {"input": "$arr", "cond": True, "limit": float("-inf")}}, + expression={"$filter": {"input": "$arr", "cond": True, "limit": FLOAT_NEGATIVE_INFINITY}}, doc={"arr": [1, 2, 3]}, error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, msg="$filter -Infinity should error", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py index 407131726..214b371e0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py @@ -31,6 +31,9 @@ ) from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, INT64_MAX, MISSING, ) @@ -187,7 +190,7 @@ "start_nan", array=[1, 2, 3], search=1, - start=float("nan"), + start=FLOAT_NAN, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject NaN start", ), @@ -195,7 +198,7 @@ "start_inf", array=[1, 2, 3], search=1, - start=float("inf"), + start=FLOAT_INFINITY, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject infinity start", ), @@ -203,7 +206,7 @@ "start_neg_inf", array=[1, 2, 3], search=1, - start=float("-inf"), + start=FLOAT_NEGATIVE_INFINITY, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject -infinity start", ), @@ -282,7 +285,7 @@ array=[1, 2, 3], search=1, start=0, - end=float("nan"), + end=FLOAT_NAN, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject NaN end", ), @@ -291,7 +294,7 @@ array=[1, 2, 3], search=1, start=0, - end=float("inf"), + end=FLOAT_INFINITY, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject infinity end", ), @@ -318,7 +321,7 @@ array=[1, 2, 3], search=1, start=0, - end=float("-inf"), + end=FLOAT_NEGATIVE_INFINITY, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject -infinity end", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py index 5fda698b0..eba14d5e5 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py @@ -104,14 +104,14 @@ ), ExpressionTestCase( "nan_array", - doc={"val": [float("nan")]}, + doc={"val": [FLOAT_NAN]}, expression={"$isArray": "$val"}, expected=True, msg="$isArray should return true for NaN array", ), ExpressionTestCase( "inf_array", - doc={"val": [float("inf")]}, + doc={"val": [FLOAT_INFINITY]}, expression={"$isArray": "$val"}, expected=True, msg="$isArray should return true for Infinity array", From c7196e286ac0b69f723aef6ade291a627542c643 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 16:01:13 -0700 Subject: [PATCH 18/21] clean up comments Signed-off-by: Alina (Xi) Li --- .../operator/expressions/array/in/test_in_null_missing.py | 4 ++-- .../array/indexOfArray/test_indexOfArray_null_missing.py | 4 ++-- .../expressions/array/isArray/test_isArray_core_behavior.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py index 0a53ef7c9..710449768 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_null_missing.py @@ -14,7 +14,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) +# Property [Null/Missing]: $in returns null when value or array is null or missing. NULL_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "null_value_in_array", @@ -32,7 +32,7 @@ ), ] -# Success: missing value handling (literal only, MISSING is a field ref) +# Property [Missing Value]: $in handles missing value field correctly. LITERAL_ONLY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "missing_value", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py index 474111a0d..ecc8fd3ac 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py @@ -17,7 +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) +# Property [Null Array]: $indexOfArray returns null when array is null or missing. NULL_ARRAY_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( "null_array", @@ -36,7 +36,7 @@ ), ] -# Success: null/missing as search value (runs both literal and insert) +# Property [Null Search]: $indexOfArray handles null and missing search values. NULL_SEARCH_TESTS: list[IndexOfArrayTest] = [ IndexOfArrayTest( "null_value_in_array", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py index a2cddcdee..e321fc076 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py @@ -2,7 +2,7 @@ Core behavior tests for $isArray expression. Tests that arrays return true, non-arrays return false, -with basic types via both literal and insert paths. +with basic types. """ import pytest From 42d4a45fa75b55a1bc5dce3368849d87f78875a5 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 9 Jul 2026 11:57:30 -0700 Subject: [PATCH 19/21] convert IndexOfArrayTest to use ExpressionTestCase Signed-off-by: Alina (Xi) Li --- .../test_indexOfArray_bson_types.py | 370 +++++++----- .../test_indexOfArray_core_behavior.py | 564 ++++++++---------- .../indexOfArray/test_indexOfArray_errors.py | 407 ++++++------- .../test_indexOfArray_null_missing.py | 76 +-- .../array/indexOfArray/utils/__init__.py | 0 .../indexOfArray/utils/indexOfArray_common.py | 41 -- 6 files changed, 685 insertions(+), 773 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/indexOfArray_common.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py index 7b9b6849c..c57d3b39d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py @@ -11,10 +11,8 @@ 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.expression_test_case import ( # noqa: E501 + ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -32,356 +30,389 @@ ) # Success: search for various BSON types -BSON_TYPE_SEARCH_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +BSON_TYPE_SEARCH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "search_int64", - array=[Int64(99), 1], - search=Int64(99), + doc={"arr": [Int64(99), 1], "search": Int64(99)}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Int64 value", ), - IndexOfArrayTest( + ExpressionTestCase( "search_decimal128", - array=[Decimal128("1.5"), 2], - search=Decimal128("1.5"), + doc={"arr": [Decimal128("1.5"), 2], "search": Decimal128("1.5")}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Decimal128 value", ), - IndexOfArrayTest( + ExpressionTestCase( "search_datetime", - array=[datetime(2024, 1, 1, tzinfo=timezone.utc), 1], - search=datetime(2024, 1, 1, tzinfo=timezone.utc), + doc={ + "arr": [datetime(2024, 1, 1, tzinfo=timezone.utc), 1], + "search": datetime(2024, 1, 1, tzinfo=timezone.utc), + }, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find datetime value", ), - IndexOfArrayTest( + ExpressionTestCase( "search_objectid", - array=[ObjectId("000000000000000000000001"), 1], - search=ObjectId("000000000000000000000001"), + doc={ + "arr": [ObjectId("000000000000000000000001"), 1], + "search": ObjectId("000000000000000000000001"), + }, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find ObjectId value", ), - IndexOfArrayTest( + ExpressionTestCase( "search_binary", - array=[Binary(b"\x01\x02", 0), 1], - search=Binary(b"\x01\x02", 0), + doc={"arr": [Binary(b"\x01\x02", 0), 1], "search": Binary(b"\x01\x02", 0)}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Binary value", ), - IndexOfArrayTest( + ExpressionTestCase( "search_regex", - array=[Regex("^abc", "i"), 1], - search=Regex("^abc", "i"), + doc={"arr": [Regex("^abc", "i"), 1], "search": Regex("^abc", "i")}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Regex value", ), - IndexOfArrayTest( + ExpressionTestCase( "search_timestamp", - array=[Timestamp(1, 1), 1], - search=Timestamp(1, 1), + doc={"arr": [Timestamp(1, 1), 1], "search": Timestamp(1, 1)}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Timestamp value", ), - IndexOfArrayTest( + ExpressionTestCase( "search_minkey", - array=[MinKey(), 1], - search=MinKey(), + doc={"arr": [MinKey(), 1], "search": MinKey()}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find MinKey value", ), - IndexOfArrayTest( + ExpressionTestCase( "search_maxkey", - array=[1, MaxKey()], - search=MaxKey(), + doc={"arr": [1, MaxKey()], "search": MaxKey()}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find MaxKey value", ), - IndexOfArrayTest( + ExpressionTestCase( "search_uuid", - array=[Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), 1], - search=Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), + doc={ + "arr": [Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), 1], + "search": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), + }, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find UUID binary value", ), # Special float values - IndexOfArrayTest( + ExpressionTestCase( "search_infinity", - array=[FLOAT_INFINITY, 1], - search=FLOAT_INFINITY, + doc={"arr": [FLOAT_INFINITY, 1], "search": FLOAT_INFINITY}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Infinity", ), - IndexOfArrayTest( + ExpressionTestCase( "search_neg_infinity", - array=[FLOAT_NEGATIVE_INFINITY, 1], - search=FLOAT_NEGATIVE_INFINITY, + doc={"arr": [FLOAT_NEGATIVE_INFINITY, 1], "search": FLOAT_NEGATIVE_INFINITY}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find -Infinity", ), - IndexOfArrayTest( + ExpressionTestCase( "search_infinity_not_found", - array=[1, 2, 3], - search=FLOAT_INFINITY, + doc={"arr": [1, 2, 3], "search": FLOAT_INFINITY}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should not find Infinity in regular array", ), # Special Decimal128 values - IndexOfArrayTest( + ExpressionTestCase( "search_decimal128_infinity", - array=[DECIMAL128_INFINITY, 1], - search=DECIMAL128_INFINITY, + doc={"arr": [DECIMAL128_INFINITY, 1], "search": DECIMAL128_INFINITY}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Decimal128 Infinity", ), - IndexOfArrayTest( + ExpressionTestCase( "search_decimal128_neg_infinity", - array=[DECIMAL128_NEGATIVE_INFINITY, 1], - search=DECIMAL128_NEGATIVE_INFINITY, + doc={ + "arr": [DECIMAL128_NEGATIVE_INFINITY, 1], + "search": DECIMAL128_NEGATIVE_INFINITY, + }, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Decimal128 -Infinity", ), # NaN equality: NaN == NaN in BSON comparison (unlike IEEE 754) - IndexOfArrayTest( + ExpressionTestCase( "search_nan_found", - array=[FLOAT_NAN, 1], - search=FLOAT_NAN, + doc={"arr": [FLOAT_NAN, 1], "search": FLOAT_NAN}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find NaN (BSON equality)", ), - IndexOfArrayTest( + ExpressionTestCase( "search_decimal128_nan_found", - array=[DECIMAL128_NAN, 1], - search=DECIMAL128_NAN, + doc={"arr": [DECIMAL128_NAN, 1], "search": DECIMAL128_NAN}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Decimal128 NaN (BSON equality)", ), # Cross-type NaN matching - IndexOfArrayTest( + ExpressionTestCase( "search_decimal128_nan_matches_float_nan", - array=[FLOAT_NAN, 1], - search=DECIMAL128_NAN, + doc={"arr": [FLOAT_NAN, 1], "search": DECIMAL128_NAN}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray decimal128 NaN should match float NaN cross-type", ), - IndexOfArrayTest( + ExpressionTestCase( "search_decimal128_neg_nan_matches_nan", - array=[DECIMAL128_NAN, 1], - search=Decimal128("-NaN"), + doc={"arr": [DECIMAL128_NAN, 1], "search": Decimal128("-NaN")}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray decimal128 -NaN should match Decimal128 NaN", ), ] # Success: numeric type equivalence in search -NUMERIC_EQUIVALENCE_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +NUMERIC_EQUIVALENCE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "int_matches_double", - array=[1.0, 2.0], - search=1, + doc={"arr": [1.0, 2.0], "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should match int to double", ), - IndexOfArrayTest( + ExpressionTestCase( "int_matches_long", - array=[Int64(1), 2], - search=1, + doc={"arr": [Int64(1), 2], "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should match int to long", ), - IndexOfArrayTest( + ExpressionTestCase( "int_matches_decimal128", - array=[Decimal128("1"), 2], - search=1, + doc={"arr": [Decimal128("1"), 2], "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should match int to decimal128", ), - IndexOfArrayTest( + ExpressionTestCase( "double_matches_int", - array=[1, 2], - search=1.0, + doc={"arr": [1, 2], "search": 1.0}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should match double to int", ), - IndexOfArrayTest( + ExpressionTestCase( "long_matches_int", - array=[1, 2], - search=Int64(1), + doc={"arr": [1, 2], "search": Int64(1)}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should match long to int", ), - IndexOfArrayTest( + ExpressionTestCase( "decimal128_matches_int", - array=[1, 2], - search=Decimal128("1"), + doc={"arr": [1, 2], "search": Decimal128("1")}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should match decimal128 to int", ), ] # Success: nested mixed arrays -NESTED_MIXED_ARRAY_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +NESTED_MIXED_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "nested_find_object_in_mixed", - array=[1, "two", {"a": 1}, [3, 4], True], - search={"a": 1}, + doc={"arr": [1, "two", {"a": 1}, [3, 4], True], "search": {"a": 1}}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=2, msg="$indexOfArray should find object in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_array_in_mixed", - array=[1, "two", {"a": 1}, [3, 4], True], - search=[3, 4], + doc={"arr": [1, "two", {"a": 1}, [3, 4], True], "search": [3, 4]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=3, msg="$indexOfArray should find array in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_bool_in_mixed", - array=[1, "two", {"a": 1}, [3, 4], True], - search=True, + doc={"arr": [1, "two", {"a": 1}, [3, 4], True], "search": True}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=4, msg="$indexOfArray should find bool in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_null_in_mixed", - array=[1, None, "three", [4], {"b": 2}], - search=None, + doc={"arr": [1, None, "three", [4], {"b": 2}], "search": None}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find null in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_deep_object", - array=[[1, 2], {"a": {"b": 3}}, "x"], - search={"a": {"b": 3}}, + doc={"arr": [[1, 2], {"a": {"b": 3}}, "x"], "search": {"a": {"b": 3}}}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find deep object", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_array_with_mixed_types", - array=[1, [None, "a", 2], "b"], - search=[None, "a", 2], + doc={"arr": [1, [None, "a", 2], "b"], "search": [None, "a", 2]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find mixed-type subarray", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_empty_object_in_mixed", - array=[1, {}, [2], "a"], - search={}, + doc={"arr": [1, {}, [2], "a"], "search": {}}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find empty object", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_empty_array_in_mixed", - array=[1, {}, [], "a"], - search=[], + doc={"arr": [1, {}, [], "a"], "search": []}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=2, msg="$indexOfArray should find empty array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_datetime_in_mixed", - array=["a", datetime(2024, 1, 1, tzinfo=timezone.utc), 3, [4]], - search=datetime(2024, 1, 1, tzinfo=timezone.utc), + doc={ + "arr": ["a", datetime(2024, 1, 1, tzinfo=timezone.utc), 3, [4]], + "search": datetime(2024, 1, 1, tzinfo=timezone.utc), + }, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find datetime in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_objectid_in_mixed", - array=[1, ObjectId("000000000000000000000001"), "x", [2]], - search=ObjectId("000000000000000000000001"), + doc={ + "arr": [1, ObjectId("000000000000000000000001"), "x", [2]], + "search": ObjectId("000000000000000000000001"), + }, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find objectid in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_decimal128_in_mixed", - array=["a", [1], Decimal128("3.14"), None], - search=Decimal128("3.14"), + doc={"arr": ["a", [1], Decimal128("3.14"), None], "search": Decimal128("3.14")}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=2, msg="$indexOfArray should find decimal128 in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_binary_in_mixed", - array=[1, Binary(b"\x01\x02", 0), "x", [3]], - search=Binary(b"\x01\x02", 0), + doc={"arr": [1, Binary(b"\x01\x02", 0), "x", [3]], "search": Binary(b"\x01\x02", 0)}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find binary in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "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")], + doc={ + "arr": [1, [Binary(b"\x01\x02", 0), Decimal128("3.14")], "x", [3]], + "search": [Binary(b"\x01\x02", 0), Decimal128("3.14")], + }, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find subarray with binary and decimal128", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_subarray_object_array", - array=["a", [{"k": 1}, [2, 3]], None, 4], - search=[{"k": 1}, [2, 3]], + doc={"arr": ["a", [{"k": 1}, [2, 3]], None, 4], "search": [{"k": 1}, [2, 3]]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find subarray with object and array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_subarray_null_bool_int", - array=[[None, True, 42], "x", 1], - search=[None, True, 42], + doc={"arr": [[None, True, 42], "x", 1], "search": [None, True, 42]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find subarray with null bool int", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_subarray_datetime_objectid", - array=[ - 0, - [datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], - "end", - ], - search=[datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], + doc={ + "arr": [ + 0, + [ + datetime(2024, 1, 1, tzinfo=timezone.utc), + ObjectId("000000000000000000000001"), + ], + "end", + ], + "search": [ + datetime(2024, 1, 1, tzinfo=timezone.utc), + ObjectId("000000000000000000000001"), + ], + }, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find subarray with datetime and objectid", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_find_subarray_minkey_maxkey", - array=[[MinKey(), MaxKey()], 1, "a"], - search=[MinKey(), MaxKey()], + doc={"arr": [[MinKey(), MaxKey()], 1, "a"], "search": [MinKey(), MaxKey()]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find subarray with minkey and maxkey", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_3_levels_deep", - array=[1, [[2, 3], [4, 5]], "end"], - search=[[2, 3], [4, 5]], + doc={"arr": [1, [[2, 3], [4, 5]], "end"], "search": [[2, 3], [4, 5]]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find 3-level nested array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_4_levels_deep", - array=["a", [[[1, 2], 3], 4], None], - search=[[[1, 2], 3], 4], + doc={"arr": ["a", [[[1, 2], 3], 4], None], "search": [[[1, 2], 3], 4]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find 4-level nested array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_deep_mixed_bson", - array=[0, [[MinKey(), {"a": [Decimal128("1.5")]}], True], "x"], - search=[[MinKey(), {"a": [Decimal128("1.5")]}], True], + doc={ + "arr": [0, [[MinKey(), {"a": [Decimal128("1.5")]}], True], "x"], + "search": [[MinKey(), {"a": [Decimal128("1.5")]}], True], + }, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find deeply nested mixed BSON", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_inner_not_outer", - array=[[1, [2, 3]], [2, 3], 4], - search=[2, 3], + doc={"arr": [[1, [2, 3]], [2, 3], 4], "search": [2, 3]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find inner array match", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_5_levels_deep", - array=[[[[[99]]]], "other"], - search=[[[[99]]]], + doc={"arr": [[[[[99]]]], "other"], "search": [[[[99]]]]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find 5-level nested array", ), - IndexOfArrayTest( + ExpressionTestCase( "nested_deep_not_found", - array=[[1, [2, 3]], [4, 5]], - search=[2, 3], + doc={"arr": [[1, [2, 3]], [4, 5]], "search": [2, 3]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should not find array at wrong nesting level", ), @@ -390,17 +421,33 @@ # 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 +# Property [Literal Evaluation]: BSON type search with inline literal values. +TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ + ExpressionTestCase( + "search_int64", + expression={"$indexOfArray": [[Int64(99), 1], Int64(99)]}, + expected=0, + msg="$indexOfArray should find Int64 value", + ), + ExpressionTestCase( + "int_matches_double", + expression={"$indexOfArray": [[1.0, 2.0], 1]}, + expected=0, + msg="$indexOfArray should match int to double", + ), + ExpressionTestCase( + "nested_find_object_in_mixed", + expression={"$indexOfArray": [[1, "two", {"a": 1}, [3, 4], True], {"a": 1}]}, + expected=2, + msg="$indexOfArray should find object in mixed array", + ), ] @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)}) + result = execute_expression(collection, test.expression) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -409,8 +456,7 @@ def test_indexOfArray_literal(collection, test): @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) + 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/indexOfArray/test_indexOfArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py index d4a97b551..db7768fb1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py @@ -8,10 +8,8 @@ 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.expression_test_case import ( # noqa: E501 + ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -22,621 +20,564 @@ from documentdb_tests.framework.test_constants import DECIMAL128_NEGATIVE_ZERO, INT32_MAX # Success: basic search — value found -BASIC_FOUND_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +BASIC_FOUND_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "found_first", - array=[1, 2, 3], - search=1, + doc={"arr": [1, 2, 3], "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find first element at index 0", ), - IndexOfArrayTest( + ExpressionTestCase( "found_middle", - array=[1, 2, 3], - search=2, + doc={"arr": [1, 2, 3], "search": 2}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find middle element at index 1", ), - IndexOfArrayTest( + ExpressionTestCase( "found_last", - array=[1, 2, 3], - search=3, + doc={"arr": [1, 2, 3], "search": 3}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=2, msg="$indexOfArray should find last element at index 2", ), - IndexOfArrayTest( + ExpressionTestCase( "found_string", - array=["a", "b", "c"], - search="b", + doc={"arr": ["a", "b", "c"], "search": "b"}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find string in array", ), - IndexOfArrayTest( + ExpressionTestCase( "found_bool_true", - array=[True, False], - search=True, + doc={"arr": [True, False], "search": True}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find true at index 0", ), - IndexOfArrayTest( + ExpressionTestCase( "found_bool_false", - array=[True, False], - search=False, + doc={"arr": [True, False], "search": False}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find false at index 1", ), - IndexOfArrayTest( + ExpressionTestCase( "found_null", - array=[None, 1, 2], - search=None, + doc={"arr": [None, 1, 2], "search": None}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find null at index 0", ), - IndexOfArrayTest( + ExpressionTestCase( "found_nested_array", - array=[[1, 2], [3, 4]], - search=[3, 4], + doc={"arr": [[1, 2], [3, 4]], "search": [3, 4]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find nested array", ), - IndexOfArrayTest( + ExpressionTestCase( "found_object", - array=[{"a": 1}, {"b": 2}], - search={"a": 1}, + doc={"arr": [{"a": 1}, {"b": 2}], "search": {"a": 1}}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find object in array", ), - IndexOfArrayTest( + ExpressionTestCase( "found_single_element", - array=[42], - search=42, + doc={"arr": [42], "search": 42}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find value in single-element array", ), - IndexOfArrayTest( + ExpressionTestCase( "first_occurrence", - array=[1, 2, 1, 2], - search=1, + doc={"arr": [1, 2, 1, 2], "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should return first occurrence index", ), - IndexOfArrayTest( + ExpressionTestCase( "duplicate_values", - array=[5, 5, 5], - search=5, + doc={"arr": [5, 5, 5], "search": 5}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should return first index for duplicates", ), ] # Success: value not found → -1 -NOT_FOUND_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +NOT_FOUND_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "not_found_int", - array=[1, 2, 3], - search=4, + doc={"arr": [1, 2, 3], "search": 4}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for absent int", ), - IndexOfArrayTest( + ExpressionTestCase( "not_found_string", - array=["a", "b"], - search="z", + doc={"arr": ["a", "b"], "search": "z"}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for absent string", ), - IndexOfArrayTest( + ExpressionTestCase( "empty_array", - array=[], - search=1, + doc={"arr": [], "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for empty array", ), - IndexOfArrayTest( + ExpressionTestCase( "type_mismatch_search", - array=[1, 2, 3], - search="1", + doc={"arr": [1, 2, 3], "search": "1"}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for type mismatch", ), - IndexOfArrayTest( + ExpressionTestCase( "bool_vs_int", - array=[1, 0], - search=True, + doc={"arr": [1, 0], "search": True}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for bool vs int", ), - IndexOfArrayTest( + ExpressionTestCase( "not_found_null", - array=[1, 2, 3], - search=None, + doc={"arr": [1, 2, 3], "search": None}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for null not in array", ), - IndexOfArrayTest( + ExpressionTestCase( "not_found_partial_array", - array=[[1, 2], [3, 4]], - search=[1], + doc={"arr": [[1, 2], [3, 4]], "search": [1]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for partial array match", ), - IndexOfArrayTest( + ExpressionTestCase( "not_found_partial_object", - array=[{"a": 1, "b": 2}], - search={"a": 1}, + doc={"arr": [{"a": 1, "b": 2}], "search": {"a": 1}}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for partial object match", ), ] # Success: with start index -START_INDEX_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +START_INDEX_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "start_skips_first", - array=[1, 2, 1, 2], - search=1, - start=1, + doc={"arr": [1, 2, 1, 2], "search": 1, "start": 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=2, msg="$indexOfArray should skip first match with start index", ), - IndexOfArrayTest( + ExpressionTestCase( "start_at_match", - array=[10, 20, 30], - search=20, - start=1, + doc={"arr": [10, 20, 30], "search": 20, "start": 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=1, msg="$indexOfArray should find at start index", ), - IndexOfArrayTest( + ExpressionTestCase( "start_past_match", - array=[10, 20, 30], - search=10, - start=1, + doc={"arr": [10, 20, 30], "search": 10, "start": 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=-1, msg="$indexOfArray should return -1 when start is past match", ), - IndexOfArrayTest( + ExpressionTestCase( "start_at_zero", - array=[1, 2, 3], - search=1, - start=0, + doc={"arr": [1, 2, 3], "search": 1, "start": 0}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=0, msg="$indexOfArray should find with start at zero", ), - IndexOfArrayTest( + ExpressionTestCase( "start_beyond_array", - array=[1, 2, 3], - search=1, - start=20, + doc={"arr": [1, 2, 3], "search": 1, "start": 20}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=-1, msg="$indexOfArray should return -1 when start beyond array", ), - IndexOfArrayTest( + ExpressionTestCase( "start_int64", - array=[10, 20, 30], - search=20, - start=Int64(1), + doc={"arr": [10, 20, 30], "search": 20, "start": Int64(1)}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=1, msg="$indexOfArray should accept Int64 start index", ), - IndexOfArrayTest( + ExpressionTestCase( "start_double_integral", - array=[10, 20, 30], - search=30, - start=2.0, + doc={"arr": [10, 20, 30], "search": 30, "start": 2.0}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=2, msg="$indexOfArray should accept integral double start index", ), - IndexOfArrayTest( + ExpressionTestCase( "start_decimal128_integral", - array=[10, 20, 30], - search=20, - start=Decimal128("1"), + doc={"arr": [10, 20, 30], "search": 20, "start": Decimal128("1")}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=1, msg="$indexOfArray should accept Decimal128 start index", ), - IndexOfArrayTest( + ExpressionTestCase( "start_decimal128_10E_neg1", - array=[10, 20, 30], - search=20, - start=Decimal128("10E-1"), + doc={"arr": [10, 20, 30], "search": 20, "start": Decimal128("10E-1")}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=1, msg="$indexOfArray should accept Decimal128 10E-1 as start index 1", ), - IndexOfArrayTest( + ExpressionTestCase( "end_decimal128_30E_neg1", - array=[10, 20, 30], - search=20, - start=0, - end=Decimal128("30E-1"), + doc={"arr": [10, 20, 30], "search": 20, "start": 0, "end": Decimal128("30E-1")}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray should accept Decimal128 30E-1 as end index 3", ), ] # Success: with start and end index -START_END_INDEX_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +START_END_INDEX_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "range_found", - array=["a", "b", "c", "b"], - search="b", - start=1, - end=3, + doc={"arr": ["a", "b", "c", "b"], "search": "b", "start": 1, "end": 3}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray should find in range", ), - IndexOfArrayTest( + ExpressionTestCase( "range_not_found_exclusive_end", - array=["a", "b", "c"], - search="c", - start=0, - end=2, + doc={"arr": ["a", "b", "c"], "search": "c", "start": 0, "end": 2}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray should not find at exclusive end", ), - IndexOfArrayTest( + ExpressionTestCase( "range_end_equals_start", - array=[1, 2, 3], - search=1, - start=0, - end=0, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": 0}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray should return -1 when end equals start", ), - IndexOfArrayTest( + ExpressionTestCase( "range_end_less_than_start", - array=["a", "b", "c"], - search="b", - start=2, - end=1, + doc={"arr": ["a", "b", "c"], "search": "b", "start": 2, "end": 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray should return -1 when end less than start", ), - IndexOfArrayTest( + ExpressionTestCase( "range_end_beyond_array", - array=[1, 2, 3], - search=3, - start=0, - end=100, + doc={"arr": [1, 2, 3], "search": 3, "start": 0, "end": 100}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=2, msg="$indexOfArray should find when end beyond array", ), - IndexOfArrayTest( + ExpressionTestCase( "range_full_array", - array=[1, 2, 3], - search=2, - start=0, - end=3, + doc={"arr": [1, 2, 3], "search": 2, "start": 0, "end": 3}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray should find in full array range", ), - IndexOfArrayTest( + ExpressionTestCase( "range_int64_bounds", - array=[10, 20, 30], - search=20, - start=Int64(0), - end=Int64(3), + doc={"arr": [10, 20, 30], "search": 20, "start": Int64(0), "end": Int64(3)}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray should accept Int64 bounds", ), - IndexOfArrayTest( + ExpressionTestCase( "range_double_integral_bounds", - array=[10, 20, 30], - search=20, - start=0.0, - end=3.0, + doc={"arr": [10, 20, 30], "search": 20, "start": 0.0, "end": 3.0}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray should accept integral double bounds", ), - IndexOfArrayTest( + ExpressionTestCase( "range_decimal128_bounds", - array=[10, 20, 30], - search=20, - start=Decimal128("0"), - end=Decimal128("3"), + doc={ + "arr": [10, 20, 30], + "search": 20, + "start": Decimal128("0"), + "end": Decimal128("3"), + }, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray should accept Decimal128 bounds", ), ] # Success: first occurrence from start with multiple duplicates -FIRST_FROM_START_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +FIRST_FROM_START_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "dup_skip_to_third_occurrence", - array=[5, 3, 5, 3, 5], - search=5, - start=3, + doc={"arr": [5, 3, 5, 3, 5], "search": 5, "start": 3}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=4, msg="$indexOfArray should find third occurrence of 5 when start=3", ), - IndexOfArrayTest( + ExpressionTestCase( "dup_skip_different_value", - array=[5, 3, 5, 3, 5], - search=3, - start=2, + doc={"arr": [5, 3, 5, 3, 5], "search": 3, "start": 2}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=3, msg="$indexOfArray should find second 3 when start=2", ), ] # Success: detailed range semantics -RANGE_SEMANTICS_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +RANGE_SEMANTICS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "end_exclusive_includes_before_boundary", - array=["a", "b", "c"], - search="b", - start=0, - end=2, + doc={"arr": ["a", "b", "c"], "search": "b", "start": 0, "end": 2}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray element before end boundary should be included in [0,2)", ), - IndexOfArrayTest( + ExpressionTestCase( "end_exclusive_excludes_at_boundary", - array=["a", "b", "c"], - search="b", - start=0, - end=1, + doc={"arr": ["a", "b", "c"], "search": "b", "start": 0, "end": 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray element at end boundary should be excluded from [0,1)", ), - IndexOfArrayTest( + ExpressionTestCase( "empty_range_at_array_length", - array=[1, 2, 3], - search=3, - start=3, - end=3, + doc={"arr": [1, 2, 3], "search": 3, "start": 3, "end": 3}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray empty range [len,len) at array boundary should return -1", ), - IndexOfArrayTest( + ExpressionTestCase( "range_finds_dup_in_subrange", - array=[1, 2, 3, 2, 1, 2, 3], - search=2, - start=2, - end=4, + doc={"arr": [1, 2, 3, 2, 1, 2, 3], "search": 2, "start": 2, "end": 4}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=3, msg="$indexOfArray should find value within range skipping earlier occurrences", ), - IndexOfArrayTest( + ExpressionTestCase( "start_at_array_length", - array=["a", "b", "c"], - search="a", - start=3, + doc={"arr": ["a", "b", "c"], "search": "a", "start": 3}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=-1, msg="$indexOfArray should return -1 when start equals array length", ), - IndexOfArrayTest( + ExpressionTestCase( "start_and_end_both_beyond_array", - array=["a", "b", "c"], - search="a", - start=100, - end=200, + doc={"arr": ["a", "b", "c"], "search": "a", "start": 100, "end": 200}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray should return -1 when both start and end are beyond array", ), - IndexOfArrayTest( + ExpressionTestCase( "end_before_match_position", - array=["a", "abc", "b"], - search="b", - start=0, - end=1, + doc={"arr": ["a", "abc", "b"], "search": "b", "start": 0, "end": 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray should return -1 when end is before the matching element", ), ] # Success: degenerate and single-element edge cases -DEGENERATE_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +DEGENERATE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "single_not_found", - array=[1], - search=2, + doc={"arr": [1], "search": 2}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 when single element doesn't match", ), - IndexOfArrayTest( + ExpressionTestCase( "single_found_in_range", - array=[42], - search=42, - start=0, - end=1, + doc={"arr": [42], "search": 42, "start": 0, "end": 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=0, msg="$indexOfArray single element found in range [0,1)", ), - IndexOfArrayTest( + ExpressionTestCase( "single_empty_range", - array=[42], - search=42, - start=0, - end=0, + doc={"arr": [42], "search": 42, "start": 0, "end": 0}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray single element not found in empty range [0,0)", ), - IndexOfArrayTest( + ExpressionTestCase( "single_start_past_element", - array=[42], - search=42, - start=1, + doc={"arr": [42], "search": 42, "start": 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=-1, msg="$indexOfArray single element not found when start past it", ), - IndexOfArrayTest( + ExpressionTestCase( "all_null_from_start", - array=[None, None, None], - search=None, - start=1, + doc={"arr": [None, None, None], "search": None, "start": 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=1, msg="$indexOfArray should find null at index 1 from start=1 in all-null array", ), - IndexOfArrayTest( + ExpressionTestCase( "all_null_search_different_type", - array=[None, None, None], - search=1, + doc={"arr": [None, None, None], "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should not find int in all-null array", ), - IndexOfArrayTest( + ExpressionTestCase( "all_true_search_false", - array=[True, True, True], - search=False, + doc={"arr": [True, True, True], "search": False}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should not find false in all-true array", ), ] # Success: mixed types in array -MIXED_TYPE_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "mixed_find_string", - array=[1, "2", True, None, [1]], - search="2", + doc={"arr": [1, "2", True, None, [1]], "search": "2"}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find string in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "mixed_find_null", - array=[1, "2", True, None, [1]], - search=None, + doc={"arr": [1, "2", True, None, [1]], "search": None}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=3, msg="$indexOfArray should find null in mixed array", ), - IndexOfArrayTest( + ExpressionTestCase( "mixed_find_array", - array=[1, "2", True, None, [1]], - search=[1], + doc={"arr": [1, "2", True, None, [1]], "search": [1]}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=4, msg="$indexOfArray should find array in mixed array", ), ] # Success: large array - -LARGE_ARRAY_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "large_array_first", - array=list(range(20_000)), - search=0, + doc={"arr": list(range(20_000)), "search": 0}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find first in large array", ), - IndexOfArrayTest( + ExpressionTestCase( "large_array_last", - array=list(range(20_000)), - search=19_999, + doc={"arr": list(range(20_000)), "search": 19_999}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=19_999, msg="$indexOfArray should find last in large array", ), - IndexOfArrayTest( + ExpressionTestCase( "large_array_middle", - array=list(range(20_000)), - search=10_000, + doc={"arr": list(range(20_000)), "search": 10_000}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=10_000, msg="$indexOfArray should find middle in large array", ), - IndexOfArrayTest( + ExpressionTestCase( "large_array_not_found", - array=list(range(20_000)), - search=-1, + doc={"arr": list(range(20_000)), "search": -1}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for absent value in large array", ), - IndexOfArrayTest( + ExpressionTestCase( "large_array_with_start", - array=list(range(20_000)), - search=19_999, - start=19_998, + doc={"arr": list(range(20_000)), "search": 19_999, "start": 19_998}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=19_999, msg="$indexOfArray should find with start in large array", ), ] # Negative zero treated as equivalent to positive zero in search -NEGATIVE_ZERO_SEARCH_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +NEGATIVE_ZERO_SEARCH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "search_double_neg_zero_in_zeros", - array=[0, 1, 2], - search=-0.0, + doc={"arr": [0, 1, 2], "search": -0.0}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find -0.0 at index of 0", ), - IndexOfArrayTest( + ExpressionTestCase( "search_zero_finds_neg_zero", - array=[-0.0, 1, 2], - search=0, + doc={"arr": [-0.0, 1, 2], "search": 0}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find 0 matching -0.0 in array", ), - IndexOfArrayTest( + ExpressionTestCase( "search_decimal128_neg_zero", - array=[0, 1, 2], - search=Decimal128("-0"), + doc={"arr": [0, 1, 2], "search": Decimal128("-0")}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Decimal128 -0 at index of 0", ), ] # Boundary values for start/end indices -BOUNDARY_INDEX_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +BOUNDARY_INDEX_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "start_int32_max", - array=[1, 2, 3], - search=1, - start=INT32_MAX, + doc={"arr": [1, 2, 3], "search": 1, "start": INT32_MAX}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=-1, msg="$indexOfArray should return -1 with INT32_MAX start", ), - IndexOfArrayTest( + ExpressionTestCase( "end_int32_max", - array=[1, 2, 3], - search=2, - start=0, - end=INT32_MAX, + doc={"arr": [1, 2, 3], "search": 2, "start": 0, "end": INT32_MAX}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray should find with INT32_MAX end", ), - IndexOfArrayTest( + ExpressionTestCase( "start_and_end_int32_max", - array=[1, 2, 3], - search=1, - start=INT32_MAX, - end=INT32_MAX, + doc={"arr": [1, 2, 3], "search": 1, "start": INT32_MAX, "end": INT32_MAX}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray should return -1 with both INT32_MAX", ), - IndexOfArrayTest( + ExpressionTestCase( "start_int32_max_minus_1", - array=[1, 2, 3], - search=1, - start=INT32_MAX - 1, + doc={"arr": [1, 2, 3], "search": 1, "start": INT32_MAX - 1}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=-1, msg="$indexOfArray should return -1 with INT32_MAX-1 start", ), ] # Negative zero as start/end index treated as 0 -NEGATIVE_ZERO_INDEX_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +NEGATIVE_ZERO_INDEX_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "double_neg_zero_start", - array=[10, 20, 30], - search=10, - start=-0.0, + doc={"arr": [10, 20, 30], "search": 10, "start": -0.0}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=0, msg="$indexOfArray should treat -0.0 start as 0", ), - IndexOfArrayTest( + ExpressionTestCase( "decimal128_neg_zero_start", - array=[10, 20, 30], - search=10, - start=DECIMAL128_NEGATIVE_ZERO, + doc={"arr": [10, 20, 30], "search": 10, "start": DECIMAL128_NEGATIVE_ZERO}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=0, msg="$indexOfArray should treat decimal128 -0 start as 0", ), - IndexOfArrayTest( + ExpressionTestCase( "double_neg_zero_end", - array=[10, 20, 30], - search=10, - start=0, - end=-0.0, + doc={"arr": [10, 20, 30], "search": 10, "start": 0, "end": -0.0}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray should treat -0.0 end as 0", ), @@ -658,17 +599,33 @@ + NEGATIVE_ZERO_INDEX_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 +# Property [Literal Evaluation]: $indexOfArray evaluates correctly with inline literal values. +TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ + ExpressionTestCase( + "found_first", + expression={"$indexOfArray": [[1, 2, 3], 1]}, + expected=0, + msg="$indexOfArray should find first element at index 0", + ), + ExpressionTestCase( + "not_found_int", + expression={"$indexOfArray": [[1, 2, 3], 4]}, + expected=-1, + msg="$indexOfArray should return -1 for absent int", + ), + ExpressionTestCase( + "range_found", + expression={"$indexOfArray": [["a", "b", "c", "b"], "b", 1, 3]}, + expected=1, + msg="$indexOfArray should find in range", + ), ] @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)}) + result = execute_expression(collection, test.expression) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -677,8 +634,7 @@ def test_indexOfArray_literal(collection, test): @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) + 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/indexOfArray/test_indexOfArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py index 214b371e0..304ef5a18 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py @@ -10,11 +10,6 @@ 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.expression_test_case import ( # noqa: E501 ExpressionTestCase, ) @@ -39,418 +34,367 @@ ) # Error: INT64_MAX start/end index (not representable as int32) -BOUNDARY_ERROR_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +BOUNDARY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "start_int64_max", - array=[1, 2, 3], - search=1, - start=INT64_MAX, + doc={"arr": [1, 2, 3], "search": 1, "start": INT64_MAX}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject INT64_MAX start", ), - IndexOfArrayTest( + ExpressionTestCase( "end_int64_max", - array=[1, 2, 3], - search=2, - start=0, - end=INT64_MAX, + doc={"arr": [1, 2, 3], "search": 2, "start": 0, "end": INT64_MAX}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject INT64_MAX end", ), ] # Error: first argument not an array (and not null) -NOT_ARRAY_ERROR_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +NOT_ARRAY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "string_as_array", - array="hello", - search=1, + doc={"arr": "hello", "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject string as array", ), - IndexOfArrayTest( + ExpressionTestCase( "int_as_array", - array=42, - search=1, + doc={"arr": 42, "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject int as array", ), - IndexOfArrayTest( + ExpressionTestCase( "double_as_array", - array=3.14, - search=1, + doc={"arr": 3.14, "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject double as array", ), - IndexOfArrayTest( + ExpressionTestCase( "bool_true_as_array", - array=True, - search=1, + doc={"arr": True, "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject bool true as array", ), - IndexOfArrayTest( + ExpressionTestCase( "bool_false_as_array", - array=False, - search=1, + doc={"arr": False, "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject bool false as array", ), - IndexOfArrayTest( + ExpressionTestCase( "object_as_array", - array={"a": 1}, - search=1, + doc={"arr": {"a": 1}, "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject object as array", ), - IndexOfArrayTest( + ExpressionTestCase( "decimal128_as_array", - array=Decimal128("1"), - search=1, + doc={"arr": Decimal128("1"), "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject decimal128 as array", ), - IndexOfArrayTest( + ExpressionTestCase( "int64_as_array", - array=Int64(1), - search=1, + doc={"arr": Int64(1), "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject int64 as array", ), - IndexOfArrayTest( + ExpressionTestCase( "binary_as_array", - array=Binary(b"x", 0), - search=1, + doc={"arr": Binary(b"x", 0), "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject binary as array", ), - IndexOfArrayTest( + ExpressionTestCase( "datetime_as_array", - array=datetime(2024, 1, 1, tzinfo=timezone.utc), - search=1, + doc={"arr": datetime(2024, 1, 1, tzinfo=timezone.utc), "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject datetime as array", ), - IndexOfArrayTest( + ExpressionTestCase( "objectid_as_array", - array=ObjectId(), - search=1, + doc={"arr": ObjectId(), "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject objectid as array", ), - IndexOfArrayTest( + ExpressionTestCase( "regex_as_array", - array=Regex("x"), - search=1, + doc={"arr": Regex("x"), "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject regex as array", ), - IndexOfArrayTest( + ExpressionTestCase( "maxkey_as_array", - array=MaxKey(), - search=1, + doc={"arr": MaxKey(), "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject maxkey as array", ), - IndexOfArrayTest( + ExpressionTestCase( "minkey_as_array", - array=MinKey(), - search=1, + doc={"arr": MinKey(), "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject minkey as array", ), - IndexOfArrayTest( + ExpressionTestCase( "timestamp_as_array", - array=Timestamp(0, 0), - search=1, + doc={"arr": Timestamp(0, 0), "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, msg="$indexOfArray should reject timestamp as array", ), ] # Error: start index not integral -START_NOT_INTEGRAL_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +START_NOT_INTEGRAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "start_fractional_double", - array=[1, 2, 3], - search=1, - start=1.5, + doc={"arr": [1, 2, 3], "search": 1, "start": 1.5}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject fractional double start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_fractional_decimal128", - array=[1, 2, 3], - search=1, - start=Decimal128("0.5"), + doc={"arr": [1, 2, 3], "search": 1, "start": Decimal128("0.5")}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject fractional decimal128 start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_nan", - array=[1, 2, 3], - search=1, - start=FLOAT_NAN, + doc={"arr": [1, 2, 3], "search": 1, "start": FLOAT_NAN}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject NaN start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_inf", - array=[1, 2, 3], - search=1, - start=FLOAT_INFINITY, + doc={"arr": [1, 2, 3], "search": 1, "start": FLOAT_INFINITY}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject infinity start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_neg_inf", - array=[1, 2, 3], - search=1, - start=FLOAT_NEGATIVE_INFINITY, + doc={"arr": [1, 2, 3], "search": 1, "start": FLOAT_NEGATIVE_INFINITY}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject -infinity start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_decimal128_nan", - array=[1, 2, 3], - search=1, - start=Decimal128("NaN"), + doc={"arr": [1, 2, 3], "search": 1, "start": Decimal128("NaN")}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject decimal128 NaN start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_decimal128_inf", - array=[1, 2, 3], - search=1, - start=Decimal128("Infinity"), + doc={"arr": [1, 2, 3], "search": 1, "start": Decimal128("Infinity")}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject decimal128 infinity start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_string", - array=[1, 2, 3], - search=1, - start="0", + doc={"arr": [1, 2, 3], "search": 1, "start": "0"}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject string start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_bool", - array=[1, 2, 3], - search=1, - start=True, + doc={"arr": [1, 2, 3], "search": 1, "start": True}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject bool start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_array", - array=[1, 2, 3], - search=1, - start=[0], + doc={"arr": [1, 2, 3], "search": 1, "start": [0]}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject array start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_object", - array=[1, 2, 3], - search=1, - start={"a": 0}, + doc={"arr": [1, 2, 3], "search": 1, "start": {"a": 0}}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject object start", ), ] # Error: end index not integral -END_NOT_INTEGRAL_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +END_NOT_INTEGRAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "end_fractional_double", - array=[1, 2, 3], - search=1, - start=0, - end=1.5, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": 1.5}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject fractional double end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_fractional_decimal128", - array=[1, 2, 3], - search=1, - start=0, - end=Decimal128("0.5"), + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": Decimal128("0.5")}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject fractional decimal128 end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_nan", - array=[1, 2, 3], - search=1, - start=0, - end=FLOAT_NAN, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": FLOAT_NAN}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject NaN end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_inf", - array=[1, 2, 3], - search=1, - start=0, - end=FLOAT_INFINITY, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": FLOAT_INFINITY}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject infinity end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_string", - array=[1, 2, 3], - search=1, - start=0, - end="3", + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": "3"}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject string end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_bool", - array=[1, 2, 3], - search=1, - start=0, - end=True, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": True}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject bool end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_neg_inf", - array=[1, 2, 3], - search=1, - start=0, - end=FLOAT_NEGATIVE_INFINITY, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": FLOAT_NEGATIVE_INFINITY}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject -infinity end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_decimal128_nan", - array=[1, 2, 3], - search=1, - start=0, - end=Decimal128("NaN"), + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": Decimal128("NaN")}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject decimal128 NaN end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_decimal128_inf", - array=[1, 2, 3], - search=1, - start=0, - end=Decimal128("Infinity"), + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": Decimal128("Infinity")}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject decimal128 infinity end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_array", - array=[1, 2, 3], - search=1, - start=0, - end=[3], + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": [3]}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject array end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_object", - array=[1, 2, 3], - search=1, - start=0, - end={"a": 0}, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": {"a": 0}}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject object end", ), ] # Error: negative start index -START_NEGATIVE_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +START_NEGATIVE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "start_neg_one", - array=[1, 2, 3], - search=1, - start=-1, + doc={"arr": [1, 2, 3], "search": 1, "start": -1}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative start -1", ), - IndexOfArrayTest( + ExpressionTestCase( "start_neg_large", - array=[1, 2, 3], - search=1, - start=-100, + doc={"arr": [1, 2, 3], "search": 1, "start": -100}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative start -100", ), - IndexOfArrayTest( + ExpressionTestCase( "start_neg_int64", - array=[1, 2, 3], - search=1, - start=Int64(-1), + doc={"arr": [1, 2, 3], "search": 1, "start": Int64(-1)}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative Int64 start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_neg_double", - array=[1, 2, 3], - search=1, - start=-1.0, + doc={"arr": [1, 2, 3], "search": 1, "start": -1.0}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative double start", ), - IndexOfArrayTest( + ExpressionTestCase( "start_neg_decimal128", - array=[1, 2, 3], - search=1, - start=Decimal128("-1"), + doc={"arr": [1, 2, 3], "search": 1, "start": Decimal128("-1")}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative decimal128 start", ), ] # Error: negative end index -END_NEGATIVE_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +END_NEGATIVE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "end_neg_one", - array=[1, 2, 3], - search=1, - start=0, - end=-1, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": -1}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative end -1", ), - IndexOfArrayTest( + ExpressionTestCase( "end_neg_large", - array=[1, 2, 3], - search=1, - start=0, - end=-100, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": -100}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative end -100", ), - IndexOfArrayTest( + ExpressionTestCase( "end_neg_int64", - array=[1, 2, 3], - search=1, - start=0, - end=Int64(-1), + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": Int64(-1)}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative Int64 end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_neg_double", - array=[1, 2, 3], - search=1, - start=0, - end=-1.0, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": -1.0}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative double end", ), - IndexOfArrayTest( + ExpressionTestCase( "end_neg_decimal128", - array=[1, 2, 3], - search=1, - start=0, - end=Decimal128("-1"), + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": Decimal128("-1")}, + expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, msg="$indexOfArray should reject negative decimal128 end", ), @@ -466,37 +410,45 @@ + END_NEGATIVE_TESTS ) -LITERAL_ONLY_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +# Property [Literal Evaluation]: error cases with inline literal values. +TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ + ExpressionTestCase( + "string_as_array", + expression={"$indexOfArray": ["hello", 1]}, + error_code=INDEX_OF_ARRAY_NOT_ARRAY_ERROR, + msg="$indexOfArray should reject string as array", + ), + ExpressionTestCase( + "start_fractional_double", + expression={"$indexOfArray": [[1, 2, 3], 1, 1.5]}, + error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, + msg="$indexOfArray should reject fractional double start", + ), + ExpressionTestCase( + "start_neg_one", + expression={"$indexOfArray": [[1, 2, 3], 1, -1]}, + error_code=INDEX_OF_ARRAY_INDEX_NEGATIVE_ERROR, + msg="$indexOfArray should reject negative start -1", + ), + ExpressionTestCase( "start_missing_field", - array=[1, 2, 3], - search=1, - start=MISSING, + expression={"$indexOfArray": [[1, 2, 3], 1, MISSING]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject missing field as start", ), - IndexOfArrayTest( + ExpressionTestCase( "end_missing_field", - array=[1, 2, 3], - search=1, - start=0, - end=MISSING, + expression={"$indexOfArray": [[1, 2, 3], 1, 0, MISSING]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray 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)}) + result = execute_expression(collection, test.expression) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -505,8 +457,7 @@ def test_indexOfArray_literal(collection, test): @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) + 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/indexOfArray/test_indexOfArray_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py index ecc8fd3ac..df42559dc 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_null_missing.py @@ -4,10 +4,8 @@ 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.expression_test_case import ( # noqa: E501 + ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -18,62 +16,70 @@ from documentdb_tests.framework.test_constants import MISSING # Property [Null Array]: $indexOfArray returns null when array is null or missing. -NULL_ARRAY_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +NULL_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "null_array", - array=None, - search=1, + doc={"arr": None, "search": 1}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=None, msg="$indexOfArray should return null for null array", ), - IndexOfArrayTest( + ExpressionTestCase( "null_array_with_start", - array=None, - search=1, - start=0, + doc={"arr": None, "search": 1, "start": 0}, + expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=None, msg="$indexOfArray should return null for null array with start", ), ] # Property [Null Search]: $indexOfArray handles null and missing search values. -NULL_SEARCH_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +NULL_SEARCH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( "null_value_in_array", - array=[1, None, 3], - search=None, + doc={"arr": [1, None, 3], "search": None}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, msg="$indexOfArray should find null in array", ), - IndexOfArrayTest( + ExpressionTestCase( "null_value_not_in_array", - array=[1, 2, 3], - search=None, + doc={"arr": [1, 2, 3], "search": None}, + expression={"$indexOfArray": ["$arr", "$search"]}, expected=-1, msg="$indexOfArray should return -1 for null not in array", ), ] -# Literal only: MISSING field refs -LITERAL_ONLY_TESTS: list[IndexOfArrayTest] = [ - IndexOfArrayTest( +# Property [Literal Evaluation]: null/missing handling with inline literal values. +TEST_SUBSET_FOR_LITERAL: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_array", + expression={"$indexOfArray": [None, 1]}, + expected=None, + msg="$indexOfArray should return null for null array", + ), + ExpressionTestCase( + "null_value_in_array", + expression={"$indexOfArray": [[1, None, 3], None]}, + expected=1, + msg="$indexOfArray should find null in array", + ), + ExpressionTestCase( "missing_array", - array=MISSING, - search=1, + expression={"$indexOfArray": [MISSING, 1]}, expected=None, msg="$indexOfArray should return null for missing array", ), - IndexOfArrayTest( + ExpressionTestCase( "missing_value", - array=[1, 2, 3], - search=MISSING, + expression={"$indexOfArray": [[1, 2, 3], MISSING]}, expected=-1, msg="$indexOfArray should return -1 for missing search value", ), - IndexOfArrayTest( + ExpressionTestCase( "missing_value_null_in_array", - array=[1, None, 3], - search=MISSING, + expression={"$indexOfArray": [[1, None, 3], MISSING]}, expected=-1, msg="$indexOfArray should return -1 for missing search even with null in array", ), @@ -82,16 +88,11 @@ # 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)}) + result = execute_expression(collection, test.expression) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -100,8 +101,7 @@ def test_indexOfArray_literal(collection, test): @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) + 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/indexOfArray/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 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 deleted file mode 100644 index 55876abf1..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/utils/indexOfArray_common.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -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 From 1229d4fae6d48f14e8496c281e2110a69585fa45 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 9 Jul 2026 12:26:31 -0700 Subject: [PATCH 20/21] update to use constants Signed-off-by: Alina (Xi) Li --- .../array/filter/test_filter_bson_types.py | 27 +++++++++++-------- .../array/filter/test_filter_core_behavior.py | 11 +++++--- .../array/filter/test_filter_errors.py | 14 +++++----- .../array/in/test_in_bson_types.py | 5 ++-- .../array/in/test_in_nested_arrays.py | 5 ++-- .../test_indexOfArray_bson_types.py | 10 ++++--- .../test_indexOfArray_core_behavior.py | 25 ++++++++++------- .../indexOfArray/test_indexOfArray_errors.py | 15 ++++++----- .../array/isArray/test_isArray_bson_types.py | 16 ++++++----- 9 files changed, 79 insertions(+), 49 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_bson_types.py index 52e43899d..2b15aa58d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_bson_types.py @@ -25,6 +25,9 @@ DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ONE_AND_HALF, + DECIMAL128_TRAILING_ZERO, + DECIMAL128_TWO_AND_HALF, DOUBLE_NEGATIVE_ZERO, FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY, @@ -46,8 +49,8 @@ ExpressionTestCase( "decimal128_values", expression={"$filter": {"input": "$arr", "cond": True}}, - doc={"arr": [Decimal128("1.5"), Decimal128("2.5")]}, - expected=[Decimal128("1.5"), Decimal128("2.5")], + doc={"arr": [DECIMAL128_ONE_AND_HALF, DECIMAL128_TWO_AND_HALF]}, + expected=[DECIMAL128_ONE_AND_HALF, DECIMAL128_TWO_AND_HALF], msg="$filter should preserve Decimal128 values", ), ExpressionTestCase( @@ -189,8 +192,8 @@ ExpressionTestCase( "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")], + doc={"arr": [DECIMAL128_TRAILING_ZERO, Decimal128("1.00"), Decimal128("1.000")]}, + expected=[DECIMAL128_TRAILING_ZERO, Decimal128("1.00"), Decimal128("1.000")], msg="$filter decimal128 trailing zeros preserved", ), ExpressionTestCase( @@ -213,9 +216,11 @@ ), ExpressionTestCase( "filter_decimal128", - expression={"$filter": {"input": "$arr", "cond": {"$eq": ["$$this", Decimal128("2.5")]}}}, - doc={"arr": [Decimal128("1.5"), Decimal128("2.5")]}, - expected=[Decimal128("2.5")], + expression={ + "$filter": {"input": "$arr", "cond": {"$eq": ["$$this", DECIMAL128_TWO_AND_HALF]}} + }, + doc={"arr": [DECIMAL128_ONE_AND_HALF, DECIMAL128_TWO_AND_HALF]}, + expected=[DECIMAL128_TWO_AND_HALF], msg="$filter should filter and preserve Decimal128", ), ExpressionTestCase( @@ -280,22 +285,22 @@ ExpressionTestCase( "filter_decimal128_nan_not_gte", expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 1]}}}, - doc={"arr": [Decimal128("NaN")]}, + doc={"arr": [DECIMAL128_NAN]}, expected=[], msg="$filter decimal128 NaN not >= 1", ), ExpressionTestCase( "filter_decimal128_neg_inf_not_gte", expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 1]}}}, - doc={"arr": [Decimal128("-Infinity")]}, + doc={"arr": [DECIMAL128_NEGATIVE_INFINITY]}, expected=[], msg="$filter decimal128 -Infinity not >= 1", ), ExpressionTestCase( "filter_decimal128_inf_gte", expression={"$filter": {"input": "$arr", "cond": {"$gte": ["$$this", 1]}}}, - doc={"arr": [Decimal128("Infinity")]}, - expected=[Decimal128("Infinity")], + doc={"arr": [DECIMAL128_INFINITY]}, + expected=[DECIMAL128_INFINITY], msg="$filter decimal128 Infinity >= 1", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_core_behavior.py index 2d15db2b7..5c1db1f96 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_core_behavior.py @@ -17,7 +17,12 @@ execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import INT32_MAX +from documentdb_tests.framework.test_constants import ( + DECIMAL128_ZERO, + DOUBLE_ZERO, + INT32_MAX, + INT64_ZERO, +) # Success: basic filtering BASIC_TESTS: list[ExpressionTestCase] = [ @@ -390,8 +395,8 @@ ExpressionTestCase( "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")], + doc={"arr": [0, INT64_ZERO, DOUBLE_ZERO, DECIMAL128_ZERO]}, + expected=[0, INT64_ZERO, DOUBLE_ZERO, DECIMAL128_ZERO], msg="$filter all numeric representations of 0 should match", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py index 258cb915a..c658b98ed 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/filter/test_filter_errors.py @@ -26,11 +26,13 @@ ) from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( + DECIMAL128_HALF, DECIMAL128_INFINITY, DECIMAL128_MAX, DECIMAL128_MIN, DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_NAN, DECIMAL128_NEGATIVE_ZERO, DOUBLE_NEGATIVE_ZERO, FLOAT_INFINITY, @@ -205,7 +207,7 @@ ExpressionTestCase( "decimal128_neg_nan_input", expression={"$filter": {"input": "$arr", "cond": True}}, - doc={"arr": Decimal128("-NaN")}, + doc={"arr": DECIMAL128_NEGATIVE_NAN}, error_code=FILTER_INPUT_NOT_ARRAY_ERROR, msg="$filter should reject Decimal128 -NaN input", ), @@ -356,7 +358,7 @@ ), ExpressionTestCase( "fractional_dec_0_5_limit", - expression={"$filter": {"input": "$arr", "cond": True, "limit": Decimal128("0.5")}}, + expression={"$filter": {"input": "$arr", "cond": True, "limit": DECIMAL128_HALF}}, doc={"arr": [1, 2, 3]}, error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, msg="$filter fractional decimal128 0.5 should error", @@ -384,28 +386,28 @@ ), ExpressionTestCase( "decimal128_nan_limit", - expression={"$filter": {"input": "$arr", "cond": True, "limit": Decimal128("NaN")}}, + expression={"$filter": {"input": "$arr", "cond": True, "limit": DECIMAL128_NAN}}, doc={"arr": [1, 2, 3]}, error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, msg="$filter decimal128 NaN should error", ), ExpressionTestCase( "decimal128_inf_limit", - expression={"$filter": {"input": "$arr", "cond": True, "limit": Decimal128("Infinity")}}, + expression={"$filter": {"input": "$arr", "cond": True, "limit": DECIMAL128_INFINITY}}, doc={"arr": [1, 2, 3]}, error_code=FILTER_LIMIT_NOT_INTEGRAL_ERROR, msg="$filter decimal128 Infinity should error", ), ExpressionTestCase( "neg_zero_double_limit", - expression={"$filter": {"input": "$arr", "cond": True, "limit": -0.0}}, + expression={"$filter": {"input": "$arr", "cond": True, "limit": DOUBLE_NEGATIVE_ZERO}}, doc={"arr": [1, 2, 3]}, error_code=FILTER_LIMIT_NOT_POSITIVE_ERROR, msg="$filter -0.0 limit should error", ), ExpressionTestCase( "neg_zero_decimal128_limit", - expression={"$filter": {"input": "$arr", "cond": True, "limit": Decimal128("-0")}}, + expression={"$filter": {"input": "$arr", "cond": True, "limit": DECIMAL128_NEGATIVE_ZERO}}, doc={"arr": [1, 2, 3]}, error_code=FILTER_LIMIT_NOT_POSITIVE_ERROR, msg="$filter decimal128 -0 limit should error", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py index 61b0af003..4d852a11e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_bson_types.py @@ -23,6 +23,7 @@ DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ONE_AND_HALF, DOUBLE_NEGATIVE_ZERO, FLOAT_INFINITY, FLOAT_NAN, @@ -40,7 +41,7 @@ ), ExpressionTestCase( "bson_decimal128", - doc={"val": Decimal128("1.5"), "arr": [Decimal128("1.5"), 2]}, + doc={"val": DECIMAL128_ONE_AND_HALF, "arr": [DECIMAL128_ONE_AND_HALF, 2]}, expression={"$in": ["$val", "$arr"]}, expected=True, msg="$in should find Decimal128 in array", @@ -232,7 +233,7 @@ ), ExpressionTestCase( "decimal128_in_doubles", - doc={"val": Decimal128("1.5"), "arr": [1.5, 2.5]}, + doc={"val": DECIMAL128_ONE_AND_HALF, "arr": [1.5, 2.5]}, expression={"$in": ["$val", "$arr"]}, expected=True, msg="$in should find decimal128 in doubles via numeric equivalence", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py index fe6d864d3..16e9946a5 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/in/test_in_nested_arrays.py @@ -18,6 +18,7 @@ execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DECIMAL128_ONE_AND_HALF # Success: nested mixed arrays as search targets NESTED_MIXED_ARRAY_TESTS: list[ExpressionTestCase] = [ @@ -125,8 +126,8 @@ ExpressionTestCase( "nested_deep_mixed_bson", doc={ - "val": [[MinKey(), {"a": [Decimal128("1.5")]}], True], - "arr": [0, [[MinKey(), {"a": [Decimal128("1.5")]}], True], "x"], + "val": [[MinKey(), {"a": [DECIMAL128_ONE_AND_HALF]}], True], + "arr": [0, [[MinKey(), {"a": [DECIMAL128_ONE_AND_HALF]}], True], "x"], }, expression={"$in": ["$val", "$arr"]}, expected=True, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py index c57d3b39d..431b06594 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_bson_types.py @@ -24,6 +24,8 @@ DECIMAL128_INFINITY, DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_NAN, + DECIMAL128_ONE_AND_HALF, FLOAT_INFINITY, FLOAT_NAN, FLOAT_NEGATIVE_INFINITY, @@ -40,7 +42,7 @@ ), ExpressionTestCase( "search_decimal128", - doc={"arr": [Decimal128("1.5"), 2], "search": Decimal128("1.5")}, + doc={"arr": [DECIMAL128_ONE_AND_HALF, 2], "search": DECIMAL128_ONE_AND_HALF}, expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Decimal128 value", @@ -175,7 +177,7 @@ ), ExpressionTestCase( "search_decimal128_neg_nan_matches_nan", - doc={"arr": [DECIMAL128_NAN, 1], "search": Decimal128("-NaN")}, + doc={"arr": [DECIMAL128_NAN, 1], "search": DECIMAL128_NEGATIVE_NAN}, expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray decimal128 -NaN should match Decimal128 NaN", @@ -388,8 +390,8 @@ ExpressionTestCase( "nested_deep_mixed_bson", doc={ - "arr": [0, [[MinKey(), {"a": [Decimal128("1.5")]}], True], "x"], - "search": [[MinKey(), {"a": [Decimal128("1.5")]}], True], + "arr": [0, [[MinKey(), {"a": [DECIMAL128_ONE_AND_HALF]}], True], "x"], + "search": [[MinKey(), {"a": [DECIMAL128_ONE_AND_HALF]}], True], }, expression={"$indexOfArray": ["$arr", "$search"]}, expected=1, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py index db7768fb1..e3cbdba6e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_core_behavior.py @@ -17,7 +17,14 @@ execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import DECIMAL128_NEGATIVE_ZERO, INT32_MAX +from documentdb_tests.framework.test_constants import ( + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + INT32_MAX, + INT64_ZERO, +) # Success: basic search — value found BASIC_FOUND_TESTS: list[ExpressionTestCase] = [ @@ -287,14 +294,14 @@ ), ExpressionTestCase( "range_int64_bounds", - doc={"arr": [10, 20, 30], "search": 20, "start": Int64(0), "end": Int64(3)}, + doc={"arr": [10, 20, 30], "search": 20, "start": INT64_ZERO, "end": Int64(3)}, expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray should accept Int64 bounds", ), ExpressionTestCase( "range_double_integral_bounds", - doc={"arr": [10, 20, 30], "search": 20, "start": 0.0, "end": 3.0}, + doc={"arr": [10, 20, 30], "search": 20, "start": DOUBLE_ZERO, "end": 3.0}, expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=1, msg="$indexOfArray should accept integral double bounds", @@ -304,7 +311,7 @@ doc={ "arr": [10, 20, 30], "search": 20, - "start": Decimal128("0"), + "start": DECIMAL128_ZERO, "end": Decimal128("3"), }, expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, @@ -505,21 +512,21 @@ NEGATIVE_ZERO_SEARCH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "search_double_neg_zero_in_zeros", - doc={"arr": [0, 1, 2], "search": -0.0}, + doc={"arr": [0, 1, 2], "search": DOUBLE_NEGATIVE_ZERO}, expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find -0.0 at index of 0", ), ExpressionTestCase( "search_zero_finds_neg_zero", - doc={"arr": [-0.0, 1, 2], "search": 0}, + doc={"arr": [DOUBLE_NEGATIVE_ZERO, 1, 2], "search": 0}, expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find 0 matching -0.0 in array", ), ExpressionTestCase( "search_decimal128_neg_zero", - doc={"arr": [0, 1, 2], "search": Decimal128("-0")}, + doc={"arr": [0, 1, 2], "search": DECIMAL128_NEGATIVE_ZERO}, expression={"$indexOfArray": ["$arr", "$search"]}, expected=0, msg="$indexOfArray should find Decimal128 -0 at index of 0", @@ -562,7 +569,7 @@ NEGATIVE_ZERO_INDEX_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "double_neg_zero_start", - doc={"arr": [10, 20, 30], "search": 10, "start": -0.0}, + doc={"arr": [10, 20, 30], "search": 10, "start": DOUBLE_NEGATIVE_ZERO}, expression={"$indexOfArray": ["$arr", "$search", "$start"]}, expected=0, msg="$indexOfArray should treat -0.0 start as 0", @@ -576,7 +583,7 @@ ), ExpressionTestCase( "double_neg_zero_end", - doc={"arr": [10, 20, 30], "search": 10, "start": 0, "end": -0.0}, + doc={"arr": [10, 20, 30], "search": 10, "start": 0, "end": DOUBLE_NEGATIVE_ZERO}, expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, expected=-1, msg="$indexOfArray should treat -0.0 end as 0", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py index 304ef5a18..5fcf66bcd 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/indexOfArray/test_indexOfArray_errors.py @@ -26,6 +26,9 @@ ) from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( + DECIMAL128_HALF, + DECIMAL128_INFINITY, + DECIMAL128_NAN, FLOAT_INFINITY, FLOAT_NAN, FLOAT_NEGATIVE_INFINITY, @@ -171,7 +174,7 @@ ), ExpressionTestCase( "start_fractional_decimal128", - doc={"arr": [1, 2, 3], "search": 1, "start": Decimal128("0.5")}, + doc={"arr": [1, 2, 3], "search": 1, "start": DECIMAL128_HALF}, expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject fractional decimal128 start", @@ -199,14 +202,14 @@ ), ExpressionTestCase( "start_decimal128_nan", - doc={"arr": [1, 2, 3], "search": 1, "start": Decimal128("NaN")}, + doc={"arr": [1, 2, 3], "search": 1, "start": DECIMAL128_NAN}, expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject decimal128 NaN start", ), ExpressionTestCase( "start_decimal128_inf", - doc={"arr": [1, 2, 3], "search": 1, "start": Decimal128("Infinity")}, + doc={"arr": [1, 2, 3], "search": 1, "start": DECIMAL128_INFINITY}, expression={"$indexOfArray": ["$arr", "$search", "$start"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject decimal128 infinity start", @@ -252,7 +255,7 @@ ), ExpressionTestCase( "end_fractional_decimal128", - doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": Decimal128("0.5")}, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": DECIMAL128_HALF}, expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject fractional decimal128 end", @@ -294,14 +297,14 @@ ), ExpressionTestCase( "end_decimal128_nan", - doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": Decimal128("NaN")}, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": DECIMAL128_NAN}, expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject decimal128 NaN end", ), ExpressionTestCase( "end_decimal128_inf", - doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": Decimal128("Infinity")}, + doc={"arr": [1, 2, 3], "search": 1, "start": 0, "end": DECIMAL128_INFINITY}, expression={"$indexOfArray": ["$arr", "$search", "$start", "$end"]}, error_code=INDEX_OF_ARRAY_INDEX_NOT_INTEGRAL_ERROR, msg="$indexOfArray should reject decimal128 infinity end", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py index eba14d5e5..dc15c40f4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_bson_types.py @@ -26,7 +26,9 @@ DECIMAL128_MIN, DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_NAN, DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ONE_AND_HALF, DOUBLE_NEGATIVE_ZERO, FLOAT_INFINITY, FLOAT_NAN, @@ -118,21 +120,21 @@ ), ExpressionTestCase( "decimal128_nan_array", - doc={"val": [Decimal128("NaN")]}, + doc={"val": [DECIMAL128_NAN]}, expression={"$isArray": "$val"}, expected=True, msg="$isArray should return true for Decimal128 NaN array", ), ExpressionTestCase( "decimal128_inf_array", - doc={"val": [Decimal128("Infinity")]}, + doc={"val": [DECIMAL128_INFINITY]}, expression={"$isArray": "$val"}, expected=True, msg="$isArray should return true for Decimal128 Infinity array", ), ExpressionTestCase( "decimal128_neg_nan_array", - doc={"val": [Decimal128("-NaN")]}, + doc={"val": [DECIMAL128_NEGATIVE_NAN]}, expression={"$isArray": "$val"}, expected=True, msg="$isArray should return true for Decimal128 -NaN array", @@ -170,7 +172,7 @@ doc={ "val": [ MinKey(), - {"a": [Decimal128("1.5")]}, + {"a": [DECIMAL128_ONE_AND_HALF]}, Int64(1), datetime(2024, 1, 1, tzinfo=timezone.utc), Binary(b"\x01", 0), @@ -288,7 +290,7 @@ ), ExpressionTestCase( "decimal128_neg_nan", - doc={"val": Decimal128("-NaN")}, + doc={"val": DECIMAL128_NEGATIVE_NAN}, expression={"$isArray": "$val"}, expected=False, msg="$isArray should return false for Decimal128 -NaN", @@ -375,7 +377,9 @@ ExpressionTestCase( "literal_nested_mixed_bson_array", doc=None, - expression={"$isArray": [{"$literal": [MinKey(), {"a": [Decimal128("1.5")]}, Int64(1)]}]}, + expression={ + "$isArray": [{"$literal": [MinKey(), {"a": [DECIMAL128_ONE_AND_HALF]}, Int64(1)]}] + }, expected=True, msg="$isArray should return true for literal nested mixed BSON array", ), From 904414027ab83e732d62dd80bba50ca2a082f220 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 9 Jul 2026 14:54:16 -0700 Subject: [PATCH 21/21] wrap large array with lazy() Signed-off-by: Alina (Xi) Li --- .../expressions/array/isArray/test_isArray_core_behavior.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py index e321fc076..dd0725c80 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/isArray/test_isArray_core_behavior.py @@ -15,6 +15,7 @@ execute_expression, execute_expression_with_insert, ) +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params # Success: arrays → true @@ -98,7 +99,7 @@ ), ExpressionTestCase( "large_array_of_arrays", - doc={"val": [[i] for i in range(10000)]}, + doc=lazy(lambda: {"val": [[i] for i in range(10000)]}), expression={"$isArray": "$val"}, expected=True, msg="$isArray 10K nested arrays returns true",